目录
- 别人的总结
- 第十八节 网络游戏的架构基础
- 第十九节 网络游戏的进阶架构
- Character Movement Replication
- Hit Registration
- MMOG Network Architecture
- Bandwidth Optimization
- Anti-Cheat
- Build a Scalable World
别人的总结
https://peng00bo00.github.io/archive.html
第十八节 网络游戏的架构基础
网络游戏的出现是游戏发展历程中的重要里程碑,基于互联网玩家可以同时和全球的其它玩家一起进行游玩。
网络游戏和单机游戏相比有很多难点,比如说如何保证每个玩家的游戏状态是一致的、如何进行网络同步、如何处理延迟和丢包、如何检测玩家的作弊行为、如何处理不同的设备和系统、如何设计高性能的服务器系统等等。这些网络游戏专有的问题都为游戏引擎的开发提出了更大的挑战。
所以网络游戏是一个非常复杂庞大的系统,在这一部分我们同样分为两节来介绍相关的技术。
Network Protocols
在介绍网络游戏相关的内容前我们先来介绍一下网络协议(network protocols)。实际上互联网的概念最早是由 Vint 和 Robert 提出的,他们设计了 TCP/IP 协议从而实现了不同设备基于电话线和卫星的通信。
The Problem of Communication
网络协议要解决的核心问题是如何实现两台计算机之间的数据通信,当软件应用和硬件连接不断地变得复杂时直接进行通信是非常困难的。
因此人们提出了中间层(intermediate layer)的概念来隔绝掉应用和硬件,使得开发者可以专注于程序本身而不是具体的通信过程。
在现代计算机网络中人们设计了 OSI 模型(OSI model)来对通信过程进行封装和抽象。
对七层协议还是要了解的,因为了解之后可能发现有些协议有冗余的地方,就可以优化
Socket
对于网络游戏开发来说一般不需要接触到很底层的通信协议,在大多数情况下只需要知道如何使用 socket 建立连接即可。
socket 是一个非常简单的结构体,我们只需要知道对方的 IP 地址和端口号就可以使用 socket 建立连接来发送和接收数据。
建立连接时需要额外注意到底是使用 IPv4 还是 IPv6,或者都需要,使用 TCP 还是 UDP 协议等问题。
TCP
TCP 是最经典也是著名的网络协议,它可以确保发送端发送的数据包都按照顺序被接收端接收。
TCP 的核心是 retransmission mechanism,这个机制要求接收端收到消息后会向发送端发送一个 ACK,而发送端只有接收到这个 ACK 之后才会继续发包。当然 TCP 实际使用的机制要比上述的过程复杂得多。
当 TCP 出现网络拥堵时会主动调节单次发包的数量。如果发包都能顺利地接收到则会提高发包数量以提升效率,反之则会减少发包数量以提升稳定性。
因为 TCP 需要流量控制,所以带宽不稳定
UDP
除了 TCP 之外人们还开发出了 UDP 这样的轻量级网络协议。UDP 的本质是一个端到端的网络协议,它不需要建立长时间的连接,也不要求发送数据的顺序,因此 UDP 要比 TCP 简单得多。
在现代网络游戏中根据游戏类型的不同使用合适的网络协议,比如说对于实时性要求比较高的游戏会优先选择 UDP,而策略类的游戏则会考虑使用 TCP。在大型网络游戏中还可能会使用复合类型的协议来支持游戏中不同系统的通信需求。
Reliable UDP
同时现代网络游戏中往往还会对网络协议进行定制。以 TCP 为例,虽然 TCP 协议比较稳定但是效率过于低了,而且网络游戏中出现一定的丢包是可以接受的;而对于 UDP 来说它虽然非常高效但是却不够稳定。
因此现代网络游戏中往往会基于 UDP 来定制一个网络协议,这样既可以利用 UDP 的高效性又可以保证数据通信的有序性。
ACK 及其相关技术是保证数据可靠通信的基本方法。
Automatic Repeat Request
ARQ(automatic repeat request)是基于 ACK 的错误控制方法,所有的通信算法都要事项 ARQ 的功能。
Sliding Window Protocol
滑窗协议(sliding window protocol)是经典的 ARQ 实现方法,它在发送数据时每次发送窗口大小的包然后检验回复的 ACK 来判断是否出现丢包的情况。
滑窗协议的一个缺陷在于它需要等待接收端的 ACK 才能继续发送数据,因此在很多情况下它无法完全利用带宽。
Go-Back-N ARQ
对滑窗协议的一种改进方法是Go-Back-N ARQ,当出现丢包时它只会把窗口内的包重新发送。
Selective Repeat ARQ
另一种改进方法是 selective repeat ARQ,它只会重新发送丢失或损坏的包从而进一步提升带宽的利用率。
Forward Error Correction
在网络游戏中需要额外处理丢包的问题,因此我们在自定义网络协议时一般会结合 forward error correction(FEC) 的方法来避免数据的反复发送。
目前常用的 FEC 算法包括异或校验位以及 Reed-Solomon codes 两大类。
XOR FEC
异或校验位是使用异或(XOR)运算来恢复丢失数据的方法。这里需要注意的是当同时有多个包丢失时,使用异或校验位是无法恢复数据的。
Reed-Solomon Codes
Reed-Solomon codes 是经典的信息传输算法,它利用 Vandemode 矩阵及其逆阵来恢复丢失的数据。
首先对于发出的信息,构造一个 B 矩阵,矩阵上面是单位对角阵,下面是人为构造的,要求这些人为构造的信息使得 B 矩阵抽掉任意行都能得到一个可逆矩阵
那么 B 和信息 D 相乘得到一个带有冗余的信息 G
发送 G,假设其中有一些信息丢失了,那么从 B 矩阵从抽出丢失的信息序号对应的行,得到 B’
B’ 乘以原始信息 = 收到的丢失了信息的 G
对”B’ 乘以原始信息 = 收到的丢失了信息的 G“这个式子两边乘以 B’^(-1)
得到 原始信息 = B’^(-1) * 收到的丢失了信息的 G
也就是我可以求出原始信息
总结一下,在自定义 UDP 时需要考虑 ARQ 和 FEC 两类问题。
可以自由组合网络协议的体验
Clock Synchronization
有了网络协议后就可以开始对网络游戏进行开发了,不过在具体设计游戏前我们还需要考虑不同玩家之间的时钟同步(clock synchronization)问题。
Round Trip Time
由于网络通信延迟的存在,客户端向服务器端发送一个包后都需要等待一定的时间才能收到回包,这个间隔的时间称为 round-trip time(RTT)。RTT 的概念类似于 ping,不过它们的区别在于 ping 更加偏向于底层而 RTT 则位于顶部的应用层。
Network Time Protocol
利用 RTT 就可以实现不同设备之间的时间同步。实际上不仅仅是网络游戏,现实生活中的各种电子设备进行同步都使用了 RTT 的技术。
Time Server Stratums
在实际设备的时间同步过程中一般会利用层次化的结构来进行实现。
NTP Algorithm
NTP 的算法实际上非常简单,我们只需要从客户端发送请求然后从服务器接收一个时刻就好,这样就可以得到 4 个时间戳。如果我们进一步假定网络上行和下行的延迟是一致的,我们可以直接计算出 RTT 的时间长短以及两个设备之间的时间偏差。当然需要注意的是在实际中网络上行和下行的带宽往往是不一致的,因此这个算法也不是十分的严谨。
Stream-Based Time Synchronization with Elimination of Higher Order Modes
实际上我们可以证明在不可靠的通信中是无法严格校准时间的。不过在实践中我们可以通过不断的使用 NTP 算法来得到一系列 RTT 值,然后把高于平均值 50% 的部分丢弃,剩下的 RTT 平均值的 1.5 倍就可以作为真实 RTT 的估计。
Remote Procedure Call (RPC)
Socket Programming
尽管利用 socket 我们可以实现客户端和服务器的通信,但对于网络游戏来说完全基于 socket 的通信是非常复杂的。这主要是因为网络游戏中客户端需要向服务器发送大量不同类型的消息,同时客户端也需要解析相应类型的反馈,传 array 需要拆,客户端和服务端操作系统不一样,这就会导致游戏逻辑变得无比复杂。
一种攻击就是发送乱码,但是服务端不知道这个是乱码,浪费了算力去解析
RPC
因此在现代网络游戏中一般会使用 RPC(remote procedure call)的方式来实现客户端和服务器的通信。基于 RPC 的技术在客户端可以像本地调用函数的方式来向服务器发送请求,这样使得开发人员可以专注于游戏逻辑而不是具体底层的实现。
Interface Definition Language
在 RPC 中会大量使用 IDL(interface definiton language)来定义不同的消息形式。
RPC stubs
然后在启动时通过 RPC stubs 来通知客户端有哪些 RPC 是可以进行调用的。
如果调用的 RPC 不存在,那么就返回错误,但是不会让应用程序 crash
因为这些错误只会导致特定的服务没有发生,不希望它们会影响到整个应用程序,所以用 RPC stubs 预先检查调用的 RPC
Stub Compiler
Real RPC Package Journey
当然真实游戏中的 RPC 在实际进行调用时还有很多的消息处理和加密工作。
Network Topology
Peer-to-Peer
在设计网络游戏时还需要考虑网络自身的架构。最经典的网络架构是P2P(peer-to-peer),此时每个客户端之间会直接建立通信。
当 P2P 需要集中所有玩家的信息时则可以选择其中一个客户端作为主机,这样其它的客户端可以通过连接主机的方式来实现联机。
很多早期经典的游戏都是使用这样的网络架构来实现联网功能。
Dedicated Server
在现代网络游戏中更多地会使用 dedicated server 这样的网络架构,此时网络中有一个专门的服务器向其它客户端提供服务。
从实践结果来看,对于小型的网络游戏 P2P 是一个足够好的架构,而对于大型的商业网络游戏则必须使用 dedicated server 这样的形式。
为了满足不同网络条件的玩家的需求,减少延迟,游戏运营商可能还需要自己建立网络线路
Game Synchronization
在前面的课程中我们介绍过游戏世界是分层的。从玩家的角度来看,玩家的操作通过输入层一路向下到达游戏逻辑层,然后通过渲染器展示给玩家。
而在网络游戏中,除了单机游戏都需要的分层外我们还需要考虑不同玩家之间的同步。在理想情况下我们希望客户端只负责处理玩家的输入,整个游戏逻辑都放在服务器端。
由于延迟的存在,不同玩家视角下的对方可能会有不同的行为表现。
因此我们需要使用游戏同步技术来保证玩家的游戏体验是一致的。目前常用的同步技术包括快照同步 snapshot、帧同步 lockstep 以及状态同步 state synchornization 等。
Snapshot Synchronization
快照同步(snapshot synchronization)是一种相对古老的同步技术。在快照同步中客户端只负责向服务器发送当前玩家的数据,由服务器完成整个游戏世界的运行。然后服务器会为游戏世界生成一张快照,再发送给每个客户端来给玩家反馈。
快照同步可以严格保证每个玩家的状态都是准确的,但其缺陷在于它给服务器提出了非常巨大的挑战。因此在实际游戏中一般会降低服务器上游戏运行的帧率来平衡带宽,然后在客户端上通过插值的方式来获得高帧率。
由于每次生成快照的成本是相对较高的,为了压缩数据我们可以使用状态的变化量来对游戏状态进行表示。
快照同步非常简单也易于实现,但它基本浪费掉了客户端上的算力同时在服务器上会产生过大的压力。因此在现代网络游戏中基本不会使用快照同步的方式。
Lockstep Synchronization
帧同步(lockstep synchronization)是现代网络游戏中非常常用的同步技术。不同于快照同步完全通过服务器来运行游戏世界,在帧同步中服务器更多地是完成数据的分发工作。玩家的操作通过客户端发送到服务器上,经过服务器汇总后将当前游戏世界的状态返还给客户端,然后在每个客户端上运行游戏世界。
Lockstep Initialization
使用帧同步时首先需要进行初始化,将客户端上所有的游戏数据与服务器进行同步。这一过程一般是在游戏 loading 阶段来实现的。
Deterministic Lockstep
在游戏过程中客户端会在每一帧将玩家数据发送到服务器上,服务器接收到所有玩家的数据后再统一转发到玩家客户端上,然后由玩家客户端执行游戏逻辑。
当然这种同步方式也存在一定的缺陷,当某个玩家的数据滞后了所有玩家都必须要进行等待。这种情况在一些早期的联网游戏中都很常见。
Bucket Synchronization
为了克服这样的问题,人们提出了 bucket synchronization 这样的策略。此时服务器只会等待 bucket 长度的时间,如果超时没有收到客户端发来的数据就越过去,看下一个 bucket 时间段能否接收到。通过这样的方式其它玩家就无需一直等待了。
bucket synchronization 本质是对玩家数据的一致性以及游戏体验进行的一种权衡。
Deterministic Difficulties
帧同步的一大难点在于它要保证不同客户端上游戏世界在相同输入的情况下有着完全一致的输出。
为了保证输出的确定性我们首先要保证浮点数在不同客户端上的一致性,这可以使用 IEEE 754 标准来实现。
其次在不同的设备上我们需要保证相关的数学运算函数有一致的行为,对于这种问题则可以使用查表的方式来避免实际的计算。
除此之外还可以使用定点数来替换浮点数,从而避免浮点数导致的各种问题。
除了浮点数之外还要考虑随机数的问题,我们要求随机数在不同的客户端上也必须是完全一致的。因此在游戏客户端和服务器进行同步时需要将随机数种子以及随机数生成算法进行同步。
总结一下,保证客户端上游戏世界模拟一致的常用方法如下:
Tracing and Debugging
现代网络游戏的逻辑往往非常复杂,在玩家进行游玩时可能无法避免地出现一些 bug,因此对于服务器来说检测客户端发送的数据是否存在 bug 就非常重要。一般来说我们会要求客户端每隔一段时间就上传本地的 log,由服务器来检查上传数据是否存在 bug。
Lag and Delay
为了处理网络延迟的问题我们还可以在客户端上缓存若干帧,当然缓存的大小会在一定程度上影响玩家的游戏体验。
另一方面我们还可以把游戏逻辑帧和渲染帧进行分离,然后通过插值的方式来获得更加平滑的渲染效果。
Reconnection Problem
由于网络的不稳定,玩家可能会不可避免地遇到断线的情况,此时我们还需要设计断线重连的机制。
实际上再进行帧同步时每个若干帧会设置一个关键帧。在关键帧进行同步时还会更新游戏世界的快照,这样可保证即使游戏崩溃了也可以从快照中恢复。
Quick Catch Up
为了实现这样的功能可以使用 quick catch up 技术,此时我们暂停游戏的渲染把所有的计算资源用来执行游戏逻辑。
Server State Snapshot Optimization
而在服务器端也可以使用类似的技术,在服务端有一个快照,从而帮助掉线的玩家快速恢复到游戏的当前状态。实际上网络游戏的观战和回放功能也是使用这样的技术来实现的。
这里的快照指的是整个世界状态的快照,而关键帧指的只是玩家输入的服务端处理结果
Lockstep Cheating Issues
网络游戏中作弊行为的检查是非常重要的。对于帧同步的游戏,玩家可以通过发送虚假的状态来实现作弊行为,这就要求我们实现一些反作弊机制。
例如所有的玩家都会发送校验码,通过校验码就可以找出是哪个玩家正在作弊
如果是只有两个玩家,那服务端就必须时刻维护一个快照,将玩家输入与自己的快照比较,检查玩家输入是否合理
还有一种可能是,因为帧同步的客户端是完全模拟了一整个游戏世界,所以实际上其他玩家的状态也被你模拟了,所以实际上可能会有一些插件记录了其他玩家的状态,规避了战争迷雾等等
所以游戏还要考虑这个问题
Lockstep Summary
总结一下,帧同步会占用更少的带宽也比较适合各种需要实时反馈的游戏。而帧同步的难点主要集中在如何保证在不同客户端上游戏运行的一致性,如何设计断线重连机制,如何防挂等。
State Synchronization
状态同步(state synchronization)是目前大型网游非常流行的同步技术,它的基本思想是把玩家的状态和事件进行同步。
进行状态同步时由客户端提交玩家的状态数据,而服务器则会在收集到所有玩家的数据后运行游戏逻辑,然后把下一时刻的状态分发给所有的客户端。
每个客户端不需要模拟一个完整的游戏世界,而只是维护自己的状态,上传状态,接受状态
服务端维护了一个完整的游戏世界,只会把部分的状态发给客户端
Authorized and Replicated Clients
状态同步中服务器称为 authorized server,它是整个游戏世界的绝对权威;而玩家的本地客户端称为 authorized client,它是玩家操作游戏角色的接口;在其他玩家视角下的同一角色则称为 replicated client,表示它们仅仅是 authorized client 的一个副本。
我在自己的电脑上控制自己的角色,别人看到的我的角色只是我的复制品
State Synchronization Example
当 authorized client 执行了某种行为时首先会向服务发送相关的数据,然后由服务器驱动游戏逻辑并把相应的状态发布给所有的玩家。当其他客户端接收到更新后的状态时,再驱动 replicated client 执行 authorized client 的行为。类似地,authorized client 行为产生的后果也是由服务器进行计算再发布给所有的客户端。这样的好处在于我们无需要求每个客户端上的模拟是严格一致的,整个游戏世界本质上仍然是由统一的服务器进行驱动。
Dumb Client Problem
由于游戏角色的所有行为都需要经过服务器的确认才能执行,状态同步会产生 dumb client 的问题,即玩家视角下角色的行为可能是滞后的。
要缓解这样的问题可以在客户端上对玩家的行为进行预测。比如说当角色需要进行移动时首先在本地移动半步,然后等服务器传来确定的消息后再进行对齐,这样就可以改善玩家的游戏体验。在守望先锋中就使用了这样的方式来保证玩家顺畅的游玩。
由于网络波动的存在,来自服务器的确认消息往往会滞后于本地的预测。因此我们可以使用一个 buffer 来缓存游戏角色的状态,这样当收到服务器的消息时首先跟 buffer 中的状态进行检验。当 buffer 中的状态和服务器的数据不一致时就需要根据服务器的数据来矫正玩家状态。
当然这样的机制对于网络条件不好的玩家是不太公平的,他们的角色状态会不断地被服务器修正。
Packet Loss
对于丢包的问题在服务器端也会维护一个小的 buffer 来储存玩家的状态。如果 buffer 被清空则说明可能出现了掉线的情况,此时服务器会复制玩家上一个输入来维持游戏的运行。
State Synchronization Vs. Lockstep Synchronization
帧同步和状态同步两种主流同步技术的对比如下:
参考资料
Network Protocols
• Internet protocol suite:
https://en.wikipedia.org/wiki/Internet_protocol_suite
• OSI Model:
https://www.practicalnetworking.net/series/packet-traveling/osi-model/
• Automatic repeat request:
https://en.wikipedia.org/wiki/Automatic_repeat_request
• Network Communication and Remote Procedure Calls (RPCs):
https://www.cs.princeton.edu/courses/archive/fall18/cos418/docs/L2-rpc.pdf
• What are TCP and UDP?
https://www.gdyunjie.cn/showinfo-114-788-0.html
Network Synchronization
• Synchronization Issues with Smart Solutions:
https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.352.6405&rep=rep1&type=pdf
• An Efficient Synchronization Mechanism for Mirrored Game Architectures:
https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.87.6043&rep=rep1&type=pdf
• The Brave New World of Multiplayer Online Games - Synchronization Issues with Smart Solutions,
Marco Roccetti, Stefano Ferretti, Claudio E. Palazzi:
https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.352.6405&rep=rep1&type=pdf
• Network Time Protocol(NTP):https://www.rfc-editor.org/rfc/rfc958.html
• Algorithms for Synchronizing Network Clocks:https://datatracker.ietf.org/doc/html/rfc956
• “Minimizing Latency in RealTime Strategy Games”, Jim Greer, Zack Booth Simpson, Game
Progamming Gems 3 chapter 5.1, 2001: https://archive.org/details/game-programming-gems-3
• JMP van Waveren, “The DOOM III Network Architecture”, 2006:
https://mrelusive.com/publications/papers/The-DOOM-III-Network-Architecture.pdf
• Christophe DIOT, Laurent GAUTIER, “A Distributed Architecture for Mult iplayer Interactive Applications
on the Internet”, IEEE, 1999:
https://ieeexplore.ieee.org/abstract/document/777437/
• Mark Terrano, Paul Bettner “Network Programming in Age of Empires and Beyond”. GDC 2001:
https://www.gamedevs.org/uploads/1500-archers-age-of-empires-network-programming.pdf
• 腾讯游戏, 腾讯游戏开发精粹. 电子工业出版社, 2019.9:
https://gameinstitute.qq.com/game-gems
• Cocos, 帧同步游戏在技术层面的实现细节, 2021
https://zhuanlan.zhihu.com/p/408734657
• IEEE 754:
https://en.wikipedia.org/wiki/IEEE_754
• QUAKE 3 SOURCE CODE REVIEW - NETWORK MODEL. 2012:
https://fabiensanglard.net/quake3/network.php
• David Aldridge, I Shot You First - Networking the Gameplay of HALO - REACH. GDC 2011:
https://www.gdcvault.com/play/1014345/I-Shot-You-First-Networking
• Timothy Ford, “Overwatch Gameplay Architecture and Netcode”. GDC 2017:
https://www.gdcvault.com/play/1024001/-Overwatch-Gameplay-Architecture-and
• Unreal engine Document:
https://docs.unrealengine.com/5.0/en-US/networking-overview-for-unreal-engine/
• Gaffer on Games: https://gafferongames.com/#posts
第十九节 网络游戏的进阶架构
Character Movement Replication
角色位移同步(character movement replication)是现代大型网络游戏必须要实现的功能。由于网络环境的不稳定,玩家操作角色在自己视角和其他玩家视角下的行为往往会有一定的延迟,即角色在其他玩家视角下的动作会滞后于操作玩家的第一视角。
Interpolation & Extrapolation
在这种情况下我们可以使用内插(interpolation)和外插(extrapolation)两种插值方法来缓解延迟。
Interpolation
内插是指利用已知的控制点来获得中间的状态。当网络存在波动时利用内插的方法可以保证角色的动作仍然是足够平滑的。
在具体进行插值时还可以人工设置一个 offset 来保证 buffer 中有足够多的控制点,这样虽然会提高一些延迟但可以获得更加光滑稳定的插值结果。
当然内插也存在一些问题。由于内插加剧了网络延迟的问题,同样的游戏世界在操作者和其他玩家的视角下会有非常大的差别。这样的问题在一些延迟要求很高的游戏中可能是不可接受的。
我感觉更像是,display 这里有一个延迟是为了能够得到 client 收到的某一个时间节点之前和之后发生的事情(或者说关键帧),这样就方便在 client 收到的前后两个时间点之间插值
而如果没有延迟的话,就只能在所有的 client 收到的过去时间点中选择一些来插值
内插的延迟导致了一件事情,就是自己的行为比同步出去的慢,所以假设一个追逐问题,逃离的人认为自己刚好逃离,追逐的人认为自己刚好追上,就会产生问题
Extrapolation
相比于内插,外插的本质是利用已有的信息来预测未来的状态。当我们对网络延迟有一定的估计时就可以通过外插的方法来推断角色的状态。
实际上外插的思想在很多领域都有非常多的应用,dead reckoning 算法那就是外插在导航领域的实践。
Projective Velocity Blending
回到游戏领域的应用中来,projective velocity blending (PVB) 是一种使用外插来更新角色位置的算法。假设 t 0 t_0 t0 时刻角色的位置、速度和加速度分别为 p 0 p_0 p0、 v 0 v_0 v0 和 a 0 a_0 a0,而收到来自服务器同步的位置、速度和加速度则是 p 0 ′ p_0' p0′、 v 0 ′ v_0' v0′ 和 a 0 ′ a_0' a0′。假设 t 0 t_0 t0 到 t B t_B tB 时间内没有意外状况发生,
则 t t t 时刻角色的位置为:
p t ′ = p 0 ′ + v 0 ′ t + 1 2 a 0 ′ t 2 p_t' = p_0' + v_0' t + \frac{1}{2} a_0' t^2 pt′=p0′+v0′t+21a0′t2
我们的目的是通过调整速度使得角色可以在 t B t_B tB 时刻到达位置 p d p_d pd,因此首先对速度进行插值:
v t = v 0 + λ ( v 0 ′ − v 0 ) v_t = v_0 + \lambda (v_0' - v_0) vt=v0+λ(v0′−v0), λ = t − t 0 t B − t 0 \lambda = \frac{t - t_0}{t_B - t_0} λ=tB−t0t−t0
上式意味着对速度进行线性插值使得 t B t_B tB 时刻具有速度 v 0 v_0 v0。接下来再对位置进行线性插值即可:
p t = p 0 + v t t + 1 2 a 0 ′ t 2 p_t = p_0 + v_t t + \frac{1}{2} a_0' t^2 pt=p0+vtt+21a0′t2, p d = p t + λ ( p t ′ − p t ) p_d = p_t + \lambda (p_t' - p_t) pd=pt+λ(pt′−pt)
不符合动力学原理,就是直接插值
Collision Issues
外插在处理碰撞时会容易产生严重的物体穿插问题。
因为外插是直接插值到一个位置,不会理会外界干扰,就是要插值到那个位置
所以如果两个使用外插的物体相撞,那么它们首先都会直接外插到一个两者相交很深的位置,然后再发生物理的很大的力的碰撞
要处理这样的情况一般需要在客户端使用外插之前先判断时候发生碰撞,如果碰撞就停止外插,切换到本地物理引擎来处理碰撞问题,比如说在看门狗 2 中就使用了这样的方法。
Usage Scenarios
简单总结一下,对于玩家操作角色经常出现瞬移或是具有很大加速度的情况比较适合内插,对于操作角色比较符合物理规律的情况比较适合外插,而在一些大型在线游戏中还会同时结合这两种插值方法来提升玩家的游戏体验。
Hit Registration
命中判定(hit registration)同样是现代网络游戏必须要提供的基本服务。以 FPS 游戏为例,从玩家开枪到击中敌人这一过程实际上有着非常大的一段延迟,在各种不确定因素的影响下如何判断玩家确实击中目标就需要一些专门的设计。
命中判定的难点之一在于确定敌人在什么位置。由于网络延迟和插值算法的存在,玩家视角下的目标是落后于服务器上目标的真实位置的。
命中判定的另一个难点在于如何判定是否击中了目标。在游戏场景中目标往往是运动的,而且往往会位于一些掩体附近。当弹道不是瞬间命中时就需要一些额外的算法来进行判断。
因此命中判定的目标是保证游戏中的玩家就是否命中的问题能够达成一个共识(consensus)。目前主流的处理方法包括在客户端上进行检测,称为 client-side hit detection,以及在服务器端进行判断的 server-side hit registration。
Client-side hit detection
在客户端上进行检测时的基本思想是一切以玩家客户端视角下的结果为准。玩家开枪后的弹道轨迹以及击中判断都先在本地进行,然后发送到服务器上再进行验证。
-
在客户端使用复制品的角色位置判断是否击中
-
如果击中,就发送击中事件给服务端
-
服务端对受到的事件进行验证
A Comparison of Hitscan Weapons versus Projectile Weapons
不同的武器的击中判定的方式不一样
A Very Simple Server Verification of Hit Event
服务端的验证:
-
客户端发送击中事件给服务端
其中包含的射线信息例如 StartPoint, HitPoint and HitObject
-
服务端验证 StartPoint 是否足够接近设计者
-
验证 HitPoint 是否足够接近 HitObject
-
确保 StartPoint 和 HitPoint 之间没有物体阻挡
当然在实际的游戏中这个验证过程是相当复杂的,涉及到大量的验证和反作弊检测。
Problem
在客户端上进行命中检测的优势在于它非常高效而且可以减轻服务器的负担,但它的核心问题在于它不够安全,一旦客户端被破解或是网络消息被劫持就需要非常复杂的反作弊系统来维持游戏平衡。
例如先断网,然后挨个爆头敌人,然后再重连
Server-Side Hit Registration
在服务器端进行检测的一个难点在于服务器上角色的位置和状态是领先于玩家视角的,当玩家开枪时目标很可能已经移动到其它的位置上了。从这样的角度来看,玩家很难命中移动中的目标。
Lag Compensation
因此我们需要对网络延迟进行一定的补偿。当服务器收到射击的消息时不会直接使用当前的游戏状态,而是根据延迟使用之前保存的游戏状态。
这就需要服务器对游戏世界存历史快照
具体延迟还有内插值的延迟
具体延迟的计算还会更复杂,也就是说 rewind 要考虑到的延迟情况会更多,例如还有物理检测的延迟等
Cover Problems
对于掩体的问题,由于网络延迟的存在可能会出现射击者优势或窥视者优势的情况。
射击者优势 shooter’s advantage
例如,被射击者已经进入了掩体,但是还是被击中了
因为被射击者移动到掩体后的动作还没有同步到服务器,而别人开枪的信息达到服务器之后,服务器延迟补偿,找到了被射击者在掩体外的那个时间点判断是否命中
这就是对高延迟的人不利的情况
探头优势 peeker’s advantage
例如敌人想要躲藏,先探头的人发现了敌人,而敌人的躲藏行为还没有同步到服务端,所以被先探头的人发现了,先探头的人就占据了先手优势
如果先探头的人的网络延迟比较大,那么先探头的人在高延迟之后才会被敌人发现,而先探头的人已经占据了先手
这就是对高延迟的人有利的情况
所以实际情况对某一个延迟的人是否有利,还取决于具体情境
Startup Frames to Ease Latency Feeling
为了缓解延迟的问题在游戏设计时还可以利用动作前摇(startup frames)来掩盖掉网络同步。
Local Forecast VFX Impacts
类似地,也可以使用各种特效来为服务器同步争取时间。
MMOG Network Architecture
对于大型多人在线游戏(massively multiplayer online game, MMOG)我们还需要一些额外的网络架构设计。
Game Sub-Systems
• User management
• Matchmaking
• Trading system
• Social system
• Data storage
• …
MMO Architecture
MMOG 中一般会有各种子系统来组成整体的玩法系统。
Services of Link Layer
连接层(link layer)是玩家和游戏建立连接的一层。在 MMO 中为了保护服务器不受攻击,我们需要单独的连接层来分离玩家和服务器数据。
login server 通过账号密码验证
gateway 将服务器的内外网隔绝开,是一个防火墙
即使你有正确的账号密码,你也可以发送各种消息,防火墙需要拦截
信息的加密和解密都在 gateway 完成
Lobby
连接到 link layer 后玩家会进入大厅(lobby)。大厅可以理解为一个特殊的游戏模式,从而方便管理等待进行游戏的玩家。
Character Server
由于 MMO 中玩家的数据往往非常巨大,玩家的数据一般会保存在一个专门的服务器上称为 character server。
Trading System
交易系统(trading system)是 MMO 的重要组成部分,在设计交易系统时需要保证系统有足够高的安全性。
Social System
玩家之间的聊天和交互功能一般会通过专门的社交系统(social system)来进行实现。
Matchmaking
很多 MMO 还需要实现玩家的匹配。
Data Storage
数据存储(data storage)是 MMO 中非常重要的问题。不同于单机游戏,在 MMO 中玩家可能会随时下线但服务器则必须要保证一直运行。因此如何存储和调用数据是一个非常重要也非常困难的问题。
Relational Data Storage
关系数据库是最基本的数据存储方式,像游戏中玩家数据和游戏数据都会使用关系数据库进行存储。现代网络游戏中往往还会结合分布式的技术进行存储。
数据特性:
-
数据结构预先确定
-
可变的查询
-
一直是连续的
例子:
-
Player Data
-
Game Data
-
Inventory
-
Item Shops/Trading
Non-Relational Data Storage
除了关系数据库外,非关系数据库也是目前非常流行的数据存储方法。像游戏中的日志还有各种 game state 就比较适合使用非关系数据库。
数据特性:
-
数据结构对于每一个实体是不一致的
-
查询目标非常明确
-
数据不一定是连续的
例子:
-
Player/Item Stats/Profile Game Data
-
Enchantments and Upgrades
-
Game States
-
Quest Data
In-Memory Data Storage
随着游戏系统变得越来越复杂,在服务器运行过程中会产生大量的中间数据存储在内存中。因此我们需要一些内存数据管理工具。
Distributed System
随着玩家数量的增长,在单个服务器上支撑所有玩家进行游玩是几乎不可能的。在这种情况下就需要使用分布式系统(distributed system)来提供游戏服务。
Challenges with Distributed systems
信息访问不能冲突
信息冗余的时候不会出错
其中一个服务器崩溃的时候整个分布式系统不崩溃
一个 bug 在整个分布式系统中不会震荡放大
业务的一致性
Load Balancing
负载均衡(load balancing)是分布式系统中非常重要的概念,我们希望在不同服务器上都有相近的负载从而避免某些服务器负载过大的问题。
Consistent Hashing
consistent hashing 是实现负载均衡的经典技术。它的核心在于为服务器以及玩家设计对应的 hashing 算法,然后把服务器和玩家映射到一个环上,当玩家需要连接服务器时只需要利用 hashing 值寻找最近的服务器即可。当某个服务器从圆环上删除时只需要把玩家连接到下一个服务器上即可,类似地添加新的服务器只需要在圆环上添加新的节点。
显然 consistent hashing 算法的效果很大程度上依赖于 hashing 函数,当服务器或是玩家在环上的分布不够均匀时是很难保证负载均衡的。为了缓解这样的问题我们可以在环上设置一些虚拟的服务器,然后再把虚拟服务器链接到真实服务器上。
Servers Management
分布式系统的另一大难点在于如何管理大量同时运行的服务。
我们可以使用 Apache 或是 etcd 这样的工具来监视和管理各种服务。
服务首先要注册在 service discovery
service discovery 里面就有这个服务的地址
需要用到服务的时候,就去 service discovery 里面查询这个服务
也可以监视这个服务
如果某个服务挂掉了,也要由 service discovery 通知订阅者
Bandwidth Optimization
带宽优化(bandwidth optimization)是现代网络游戏必须要解决的问题。网络带宽不仅仅是游戏运营成本的重要组成部分,对玩家的游戏体验也有着重要的影响。
开发商租用的服务器的流量成本
传输运行时数据包的延迟
连接可能因为信息太多而中断
玩家小区的网关可能具有带宽限制
为什么说对游戏体验有影响呢,一方面是延迟小了,一方面是数据包小了,在网络协议中包含拥塞控制,包体小了也更加不容易拥塞,使得延迟更加稳定
带宽的计算实际上非常简单,根据计算视角的不同我们有不同的衡量指标:
n = player numbers 玩家数量
f = update frequency 刷新频率
s = size of game state 游戏状态的数量
那么每秒传输的数据量
服务器传输的数据量 o(nsf)
客户端的下行数据量 o(sf)
客户端的上行数据量 o(f)
Data Compression
数据压缩(data compression)是经典的带宽优化方法。在网络游戏中我们可以把浮点数转换为低精度的定点数来减少数据量。
例如玩家的移动速度,玩家的朝向,玩家的坐标由三维变为二维
如果地图的范围比较小,那么玩家的坐标的精度也可以下降
因此我们甚至可以对游戏地图进行分区然后在小区域中使用定点数来表示位置,来缓解低精度数值带来的影响。
Object Relevance
另一种缓解带宽需求的方式是只同步和玩家相关的对象,这样可以极大地减少每次和玩家同步的数据量。
Relevance - Static Zones
我们可以把整个游戏世界划分为若干个区域,这样每个玩家都会位于某个区域中。不同区域的数据是相互隔绝的,因此在同步时只需要同步区域中的数据即可。
Relevance - Area of Interest (AOI)
在一些开放世界游戏中我们不希望出现区域的划分,此时则可以利用 area of inerest (AOI)的概念。AOI 的意义在于我们只需要关注玩家附近的情况而无需考虑更远区域的信息。
AOI - Direct Range-Query
AOI 最简单的实现方法是利用一个半径来查询附近信息。
但是这样需要遍历所有可能需要的角色,计算距离
我不太知道这里为什么是 o(n^2) 我还以为就是 o(n),毕竟是对每一个人计算一次
AOI - Spatial-Grid
当玩家数量很多时还可以利用网格划分的方法来加速查询。
每一个 entity 进入和离开格子都是触发一个事件,这样,格子内记录了其中有什么要同步的 entity,
进入和离开都是低频
AOI - Orthogonal Linked-list
除此之外我们也可以使用十字链表这样的数据结构来进行加速。
所有的 entities 在两个链表上都有自己的位置,一个链表沿着 X 轴升序排列,一个链表沿着 Y 轴升序排列
对于某一个玩家,我们知道他在这两个链表上的位置
所以从玩家开始,在每一个链表上,都向前向后找 radius 距离内的 entites
然后这两个链表各自找到的 entities 重合的部分就是真正在以玩家为中心,2 * radius 为边长的正方形内
这种方法方便处理 AOI 半径变化的情况
但是当物体插入链表的时候,需要 o(n)
物体高速移动的时候也需要重排链表,
AOI - Potentially Visible Set (PVS)
当然也可以使用PVS这样的技术来剔除不相关的对象。
Varying Update Frequency
除了上面介绍的方法我们也可以通过调整更新频率的方式来降低带宽。一种经典的策略是根据物体和玩家之间的距离来设置更新频率,使得距离玩家远的物体更新得慢一些,距离近的更新得快一些。
Anti-Cheat
反作弊(anti-cheat)同样是现代网络游戏必须要实现的功能,作弊行为对游戏运营和其他玩家都会造成非常巨大的伤害。
网络游戏中的作弊手段是相当多样的,比如说可以修改本地的代码或是内存中的数据,骇入底层三方库的接口,甚至可以通过劫持网络通信来实现作弊行为。
透明挂 覆盖了 unity 渲染的 sdk 入口函数,改成了画线框
模拟鼠标输入
劫持网络包,发假消息
Obfuscating Memory
作弊最基本的方法是修改本地的内存。对于很多在客户端进行校验的游戏只需要修改游戏内存中的数据就可以进行作弊。
为了应对这种作弊方式,我们可以对客户端套一个壳来防止侵入。类似地,也可以对内存数据进行加密来防止侵入。
Verifying Local Files by Hashing
另一种常见的作弊方法是修改本地的资源文件。
例如把贴图改成发光,改成透明;把光照修改,使得更加容易看到敌人
我们可以通过对比本地和服务器上资源的 hashing 来进行处理。
Packet Interception and Manipulation
消息的劫持和篡改是网络游戏中经常遇到的作弊方式,因此对于客户端和服务器发出的包就必须进行加密。
目前主流的加密算法包括对称加密和非对称加密两种。对称加密是指客户端和服务器共享一副秘钥,它的缺陷在于当客户端被破解后秘钥就是公开的了,此时加密也就失效了。因此在实践中更多地使用的是非对称加密算法,此时客户端只有公钥而在服务器端存有私钥。这样即使客户端被破解了也无法完全获知所有数据。
System Software Invoke
除此之外,作弊者还可以通过修改底层游戏引擎代码来进行作弊。
Valve Anti-Cheat and Easy Anti-Cheat
针对这种情况可以使用各种安全软件来检测游戏引擎是否存在注入的情况。
这些反作弊代码会检测包体哈希,检测常见作弊软件等
AI Cheat
目前随着 AI 技术的发展还出现了使用 AI 进行作弊的现象,这对下一代反作弊系统的开发又提出了新的挑战。
Counter-Strike: Overwatch
或者是在游戏中由观战玩家或者局内玩家投票决定某玩家是否作弊
Statistic-based System
通过数据统计分析发现某些玩家的战绩数据存在异常
Detecting Known Cheat Program
别人做外挂是要牟利的
所以我先买到别人的外挂,然后在游戏运行时,在内存中扫描这个外挂的特征
Build a Scalable World
构建可拓展的游戏世界可以说是每个游戏从业者的梦想。
Scalable Game Servers
1.Zoning
分为区域
分为层
区域的划分可能导致玩家分布的密度不均
Zoning - Seamless Zones
一个 zone 一个服务器
玩家在边界上的时候会跨服务器
但是玩家在边界边缘的时候,边界的两边的玩家属于不同的服务器,但是他们理论上应该看见彼此,能够互动
因此某一边的服务器会在另一边创建 ghost entity 作为对另一边的主 entity 的同步
在玩家移动的时候,把原来区域中的服务器中维护的 active entity 变为 ghost entity,把新进入的区域的服务器中维护的 ghost entity 变为 active entity
但是还需要在边界中还需要设置一个缓冲区
不是经过了一个线就立马切换,而是超过了边界线一段距离之后再切换
防止玩家在边界线来回移动的时候高频切换
Replication
一个服务器负责一个层,每一个层都是游戏世界的镜像
其他层的玩家在本层中是 ghost
Scalable Game Servers - Combination
用户聚集的时候区域会分解
但是区域不能分解得太小,所以区域分解到不能再小,但是仍然还是人数饱满的时候,也可以使用分层的方法,再创建一个镜像