[从零开始的Unity网络同步] 3.游戏中网络同步的解决方案
从第一款网络游戏发展至今将近20年了,游戏中网络同步的技术如今也在各种游戏里得到应用和发展,这次来谈一下一些网上很火的同步架构.
1.帧同步和状态同步
帧同步
帧同步是指客户端把操作上传服务端,服务端不模拟操作,把操作转发给所有的客户端
状态同步
状态同步是指客户端把操作上传服务端,服务端模拟操作,然后把模拟的结果转发给所有客户端
2.Source引擎多人模式网络同步模型
Value分享了Source引擎内置的网络同步模型,在旗下的多人游戏都有应用,包括CS:GO,Left 4 Dead 2,Team Fortress 2等等.
原文
译文
快照
1.客户端和服务端以同样的频率(操作采样频率)采样操作指令,客户端采样到指令并不会立刻就发送,而且保存进队列,按照发送频率(指令发送频率),一起发送,这样做是为了节省带宽,减少网络IO的开销.
2.服务器按照”一定的频率模拟更新”,也就是处理客户端上传来的操作指令,生成快照(Snapshot),然后按照服务器快照发送频率给每个客户端发送快照,为了节省带宽,快照的生成是增量更新的.
3.客户端收到了服务端的快照以后,不能直接应用,如果直接应用快照的话,会导致跳帧,位置和画面的变化不平滑,因此不能直接应用,而是缓存起来,等到缓存的快照数量最少两个以后,在两个快照之间做插值计算.
输入预测
由于客户端上传指令涉及到网络延迟,服务端下发快照也涉及到网络延迟,如果没有输入预测的话,那么玩家的操作,需要等到服务端的快照达到了才生效,那这样体验很不好,为了避免这样的体验,需要对客户端的指令进行预测:客户端生成了一个指令,放到上传指令队列的同时,客户端自身也模拟这个指令,这就带来一个问题,还是因为网络延迟的因素,比如:
客户端上传N号指令 => (网络延迟) => 服务器收到N号指令 => (模拟时间) => 服务器模拟N号指令,下发快照 => (网络延迟) => 客户端收到快照
可以看到这里受到两次网络延迟和模拟时间的影响,那么客户端又在预测自己,毕竟需要服务器权威,那预测失败了怎么办呢,
解决方案:
客户端模拟完自己的指令以后,缓存模拟过的指令和模拟结果,当收到服务器的快照包以后,将快照对应的指令和自己缓存的指令做对比,如果模拟结果不同,就需要使用服务器的快照,然后在此快照的基础上,模拟缓存的预测帧.
比如:
客户端在自己10帧的时候上传了当前的操作指令C10,同时客户端模拟操作,也继续采样操作模拟,从C10开始缓存模拟过的指令,当模拟到17帧的时候,收到了服务器的快照S10,这个时候客户端缓存了C10到C17的指令,这个时候,需要拿客户端C10预测结果跟S10的快照状态做比较,如果相等,那么没关系,继续从C17往前模拟,如果不等,那么就将S10设置当前状态,再从C11模拟到C17.
延迟补偿
需要服务器对游戏世界中过往的状态进行缓存,然后推测客户端指令创建时候,服务器所在的时间点,然后使用过往的缓存状态进行计算,
3.守望先锋的网络同步设计
(视频)关于守望先锋的Netcode设计(中文字幕)
GDC2017 守望先锋》架构设计与网络同步(译文)
守望先锋网络数据分析(需要科学上网)
守望先锋(以下简称OW)的技术文章强调了“Determinism(确定性)”
确定性在这里意味着客户端和服务端模拟频率必须保持一致,从一个初始状态开始执行一系列指令,得到的模拟结果必须保持一致
在讲解猎空被麦克雷晕住的例子中,文章中说到:
>客户端是一股脑的尽快接受玩家输入,尽可能地贴近现在时刻.
>一旦从服务器回包发现预测失败,我们把你的全部输入都重播一遍直至追上当前时刻。
>当客户端收到描述角色状态的数据包时,我们基本上就得把移动状态及时恢复到最近一次经过服务器验证过状态上去,而且必须重新计算之后所有的输入操作,直至追上当前时刻。
由此可以看出,OW的预测机制的实现和Source引擎的实现思路类似.
此外,OW还做了一套动态变化发包频率和指令预测的机制,来应对网络不好的状况.为了应付客户端上传的指令包丢包的情况,服务器需要延迟模拟客户端上传的指令,也就是说,服务器存储了足够多的指令以后,才开始模拟,导致的结果是,服务器要比客户端滞后,但这是值得的,这样就更能容忍指令丢包的情况发生.
从OW关于的Netcode的视频可以看出,OW游戏中一个单位存在三份状态,最早的是本地客户端的预测状态,然后是服务器模拟指令的状态,最迟的是服务端下发后的状态
为了保证本地玩家的游戏体验,使用客户端预测技术是必要的,但是击中目标的判定肯定是需要服务器验证的,所以击中的判定是服务器下发客户端后,客户端再做表现的(所以玩麦克雷,黑百合的时候,虽然是及时命中武器,但是血条也是等一会才变化)
4.王者荣耀的帧同步设计
复盘王者荣耀手游开发全过程,Unity引擎使用帧同步
这篇文章提到了如何优化卡顿的问题 buffer
>比如我现在已经收到第N帧,只有当我收到第N+1帧的时候,第N这一帧我才可以执行。服务器会按照一定的频率,不同的给大家同步帧编号,包括这一帧的输入带给客户端,如果带一帧给你的数据你拿到之后就执行,下一帧数据没来就不能执行,它的结果就是卡顿。
这个buffer的大小,会影响到延迟和卡顿。如果你的buffer越小,你的延迟就越低,你拿到以后你不需要缓冲等待,马上就可以执行。但是如果下一帧没来,buffer很小,你就不能执行,最终导致的结果你的延迟还好,但是卡顿很明显。
buffer太大的好处可以让画面流畅,但是会导致操作延迟感.
buffer太小的好处是减少操作延迟感,但是容易导致画面卡顿
这个buffer的设置是为了让客户端在模拟输入的时候,尽可能的有足够的输入指令去模拟,如果buffer空了,就会导致客户端没有指令可以操作了,那么角色自然就停止了.
5.小结
此外,还有几篇经典的介绍网络同步的文章:
Gaffer On Games - Networked Physics
DOOM3网络同步体系
DOOM III网络架构(GAD译文)
GDC2018演讲 <<火箭联盟>>的物理与网络细节(需要科学上网)
通过上述几篇技术分析文章可以了解到,想要实现稳定可靠的网络同步解决方案,不管是帧同步还是状态同步,都需要保证一个确定性,这个确定性包括:
>1. 服务端与客户端必须保持一个频率模拟游戏世界
>2. 同样的状态 + 同样的操作指令 = 同样的新状态
保证了”确定性”,那么才可以去应对不可预料的网络波动.
好,后续程序代码逻辑都将围绕着”确定性”这一原则来实现.