Lost Gate

这个世界实在太无趣了… 既然如此,那就再创造一个世界如何?

ACT类游戏 帧同步及预表现技术分享


在前段时间,把2018年末所做的一个研究重新归纳总结了下,剪辑了一段视频,上传到了B站。 毕竟B站作为一个无广告的视频平台,很适合做技术Vlog。
视频地址
https://www.bilibili.com/video/BV1Fg4y1i7ii/

有一些B友(??)对这个演示的技术细节很感兴趣,今天对此写一篇文字分享

先用一个例子,概括一下状态同步和帧同步的技术原理

我们现在要在ACT类游戏中,实现这么一个游戏情景的同步:

A 向 前方移动了3米,随后转身面向B ,然后使用技能 Skill 100 攻击了B ,B被击硬直,同时掉血1000点。

状态同步的流程:
A 向前本地移动,同时发送自身移动的行为给服务器,并伴随间歇性上报坐标,服务器对A的行为进行合法校验,判断速度,位置连续性等,校验合法后将这个结果转义后广播给周围的玩家。这个广播的内容是: A 向 角度XXX开始移动。 那么在B的屏幕上就会看到 A朝 xx角度开始移动。
A继续之后的行为, 包括转身,攻击等,都和上面的流程类似。 本身向服务器申报一个行为,服务器校验并转义,广播给周围的玩家。
而B收到攻击,属于A攻击后的结果,由服务器运算后得出并广播。
可见,在状态同步中,A是传达自身的意图,结果由服务器运算决定。当A没有操作时,不会产生通讯。(心跳包等除外)

帧同步的流程:
A 发送 W键(假定这是向前走路的按键)按下的状态给服务器,并持续30帧,随后发送 A 键 , 随后发送 J 键 (假定是攻击)…… 服务器做什么呢? 服务器只负责分发给其他客户端,在同步层面不需要进行其他运算。 那么B在收到了 A的操作指令后,在本地对A进行运算,以此达到与A 所看到相同结果的目的。

在帧同步中,所有客户端共享自身的操作,同时也运算其他客户端的指令。由于游戏进程的推进依赖服务器发来的操作指令,所以发送指令的频率决定了游戏的逻辑帧率,一般设定为30fps即可满足ACT类游戏。一旦服务器停止操作数据的推送,则客户端逻辑便会处于完全停止的状态。

*关于 状态同步和帧同步的衍生同步方式: P2P
这是一种没有服务器的概念的同步方式,由每个客户端相互分发 状态/操作指令 进行游戏,一般采用UDP降低延迟,帧同步对顺序有要求,那么可使用现成的UDP组件,我使用的是ENet。
先说基于P2P的状态同步,只能算半个P2P,因为需要一个主机进行随机数、攻击判定的运算,充当服务器。也有作为辅助C/S结构而使用的情况,例如DNF中的组队AI、《猎人手游》中的关卡逻辑 都有采用类似的设计。这种本质依然是C/S结构。
P2P的帧同步,是可以做到无中心化,或者说即便有主机,也只是做游戏流程控制。 每个客户端在每帧收到所有客户端的指令后才进行本帧的运算,如收不到,则等待,逻辑处于停止,也就是“锁帧”。一个客户端网络卡了,所有客户端都遭殃。不过这种同步方式一般用于局域网游戏,卡顿的情况会少很多。早期的红色警戒,星级争霸,街机厅的双台对打游戏等都采用这种帧同步方式。

ACT游戏的帧同步意义在哪?状态同步又不是不能用

ACT游戏,不是APRG,也不是MOBA,是有格斗游戏特性的动作游戏,标杆代表是DNF。特性是密度极高的逻辑判定,对精准性要求也非常高,例如:浮空,硬直,上中下段,倒地追击,霸体,投技等等。 可能在极短的时间里,就会触发以上所有特性(试想一下,一个10帧的投技面对一个还剩3帧的霸体,哪怕只有20ms的通讯延迟,也丢掉了1~2帧的判定时间)。如不能准确快速的判定并给与结果,会严重影响手感。
所以许多ACT游戏会把大量逻辑运算在本地客户端进行,配合服务器校验。DNF即是这种处理方式。

先回到同步的选型上,实际上状态同步的泛用性极强,而且非常符合思考逻辑,大部分游戏采用状态同步就可以满足。

ACT使用状态同步也没有问题的,但是帧同步可以额外提供以下几点优势:

1. 多人游戏玩法的反作弊(PVP,组队PVE)
上面说过,基于状态同步,服务端参与战斗运算的力度决定了这个游戏的反作弊效果,但无论如何,服务端都要留有一定的阈值,不然由于网络波动等因素,容易产生误判。LOL和DNF都是基于状态同步,且反外挂做得都还不错。但LOL属于MOBA,运算量与DNF这类不在一个量级,即便手感有一定延迟也感觉不太出(尤其是采用预表现后)。而DNF的反外挂主要是腾讯的外挂式反外挂组件TP (??) ,和强大的律师团队(南山必胜客了解一下… 嗯味道不错,在南山有就好几家…)。其本身的运算依然在本地,所以“微调”挂、卡输入法等骚操作一度非常流行。
而帧同步从原理上,就不存在任何的偏差。多人游戏中,只要一方的计算结果不同,就会判定为作弊。服务器要做的反作弊,就是间隔性搜集每个客户端的关键属性即可。当然缺点也有,帧同步只能应用于多人游戏,单人模式是无可奈何的。而状态同步反而可以在单人模式中依然有一定的反作弊能力。

2. 网络传输体积较小,可应对大规模同屏战斗
当同场游玩的角色达到一定规模时,同步的通讯传输量级会迅速上升,例如PVP团战,RTS游戏等,可能会出现几十、上百的战斗单位,若采用状态同步,由于描述状态需要较多字段,其通讯量相当恐怖。而帧同步仅需要传输一个4字节的int(或者更少,取决于需要传输的按键数)。

3. 可保存操作指令,进行完整的战斗回放,体积非常小。
这个很好理解,把每帧记录的输入信息保存,进行模拟传输即可回放一场完整的战斗。由于每帧仅需4字节,按30fps的逻辑帧率,10分钟的战斗录像,也仅需1kb多,存储在云端也没有负担。星际,魔兽的战斗回放文件特别小,就是使用这个原理实现的。放到现在的网游,手游,回放战斗录像可以实现许多有意思的功能,例如赛季前100名对战记录,战略游戏的攻城回放等,无疑可以增强游戏的分享动力、社交性。

4. 可与其他客户端达到完全同步效果,画面一致,0偏差。
5. 可以本地运算伤害,NPC的AI,寻路等等,甚至可以去掉服务端。


帧同步的难点在哪?

帧同步的核心点,是要求所有客户端在同一时刻运行结果完全一致,所以从广义上来说,要做到两点:
1. 服务端每帧发送给所有客户端的指令,要完全一致,且顺序完全相同。
2. 客户端接收到每个帧指令后,运算结果完全一致。

第1点毫无难度,直接略过。
第2点继续拆解,每个客户端运行结果一致,这要求排除所有可能导致运行结果不一致的因素,常见的情况和解决办法有:
– 每个客户端机器性能差异,如加载速度,帧率,解析文件速度等等
解决办法:将此类逻辑与核心游戏逻辑分离。分离有两个概念,一个是时间上,可以先做这些行为,同时阻断核心逻辑(加载、解析文件),待全部准备好,再进行核心逻辑的运行。另一个则是view与model+control的分离,也就是游戏哪怕只有纯逻辑无渲染,也可以跑起来。

– 每个客户端逻辑update/tick的先后顺序不同
解决办法:使用tick管理器统一管理,不使用原生Tick/Update
– 随机数产生的值不同
解决办法:所有客户端使用同一个随机种子
– 使用基于非逻辑帧进行的各类运算(如补间动画,timer等等)
解决办法: 将这类逻辑使用统一Tick运算
– 浮点精度不同
解决办法: 使用定点数库
………………
…………
……

如何将帧同步应用到项目?

客户端方面,首先需要一个统一的Tick管理器,驱动游戏的所有关键逻辑。
所谓关键逻辑就是帧同步要驱动的内容,核心战斗就是,而像UI、摄像机的行为一般就不属于关键内容。
整个游戏的原生Update/Tick尽量都不要使用,全部注册到Tick管理器。 此外,注册时,要提供根据优先级排序的功能。
管理器完事后,可以先使用原生Update驱动测试。
如果一切正常,那么第一步完成。

目前客户端的每帧流程为 :
原生Update >> Tick管理器 >> 若干游戏关键逻辑(排序执行)>> 根据玩家输入运行逻辑


接下来是将游戏的输入操作进行“调制”和“解调”。 以某ACT游戏来说,所有的玩家输入有:方向键(16方向)+ 攻击+ 跳跃+翻滚+技能10个 。
在改造帧同步以前,这些输入将直接被执行为具体的操作行为。

而现在,则需要将输入状态搜集起来,尽可能的压缩体积,使用最小的体积记录所有输入的状态。一个byte有8个bit,那么一个int就可以存储32个键位的状态,每帧传输量相当小(具体可以搜索bitmap算法即可,本文不在赘述)。

这个步骤是完成了“调制”。 接下来反向操作,即是“解调”,解出来的操作信息,存入一个虚拟的输入判定器,游戏所有的输入判定,都从这里获取。 完事以后,先将调制解调在本地对接起来测试,如果一切正常则此步骤完成。

目前客户端的每帧流程为 :
原生Update >> Tick管理器 >> 搜集输入信息并压缩 >> 解压输入信息且放入虚拟输入判定器>> 若干游戏关键逻辑(排序执行)>> 根据虚拟输入判定器运行逻辑

到了这个步骤,你会发现,如果能凭空捏造一些输入,游戏也可以按你的思路运行。测试这个阶段成果的最佳方式就是“回放操作”。

将每帧搜集的输入帧搜集起来,存储为录像文件。 然后再读入,逐帧“解调”,运行,如果感觉像在看录像,恭喜你,帧同步的基础工作已经完成。

如果回放不完美,有了偏差呢? 那就是遇到了上面所描述的各种因素,导致了不同步。 不要小看一点点不同步,在每秒60帧的游戏里,很快就产生蝴蝶效应,不同步会迅速放大到无法接受。

本着步步为营的态度,我建议你先停留在这一步,不断使用一个时间较长、行为复杂的录像文件进行回放测试。在录像文件里插入当前帧的物件状态是一个好主意,回放的时候,就可以逐帧对比,一旦不同步,即可停止回放,结合上下文分析导致不同步的原因。 这会是一个很棒的单元测试环境,也是本人所推崇的方法论,简化测试环境和干扰项,有助于保持清醒。

当这个步骤完美后,剩余的步骤就相当简单了。

请将客户端的流程修改为如下:

原生Update >> 搜集输入信息并压缩 >> 发送给服务端
是的,因为Tick管理器从Update那里断开了,所以目前客户端不会再执行关键逻辑

而服务器也该干活了,他的工作就是建立一个固定帧率的循环,搜集每个客户端的输入信息,也就是那个int。每帧将这些帧信息广播给所有客户端,且保证顺序一致。如果有客户端卡了,这一帧没有发送输入信息怎么办? 那就当做没有输入去广播。(也有等待这个玩家的方式,也就是锁帧)

客户端收到服务端的指令后,“解调”输入指令,并驱动一次Tick管理器,游戏便运行了一帧。假设服务器是固定30帧,那么客户端在网络流畅的情况下,就是一个30fps的游戏。

最终的同步流程为:

客户端: 原生Update >> 搜集输入信息并压缩 >> 发送给服务端
服务端: 固定Tick>> 搜集客户端输入信息 >> 发送给所有客户端
客户端: 收到服务端指令>> 解调输入指令 >> 驱动Tick管理器一次 >> 若干游戏关键逻辑(排序执行)>> 根据虚拟输入判定器运行逻辑

由于加入了网络同步,可能会发生新的一系列不同步的情况。
情况复杂了,手工排查起来也许会困难。
这里建议制作一个本地AI,驱动游戏不停的进行对战测试并录像,一旦发生不同步,就保存录像和日志进行分析。
本人就有一个不同步的bug,在运行了3天的不间断的测试后,才触发……


关于帧同步中的网络波动及预表现问题

帧同步及其依赖一个稳定的网络环境,如果延迟过大,或者丢包率较高,那么每帧的lag波动将会非常明显。在不做任何处理的情况下,画面以及操作反馈都会频繁卡顿,体验极差。

平滑网络波动是最基本的提升体验办法,原理也相当简单,就是设置一个若干帧的缓冲区。 假设缓冲区为5帧,那么在5帧内的波动都不会受到影响,玩家会得到一个固定延迟的手感,对于一些交互频率不高的游戏,是可以很快适应的(其实 假如不是专业设备 ,从键盘输入到显示器最终呈现,在本地也可能会有数帧的延迟)

那么对于ACT来说,显然只是这样处理,并不能获得良好的体验。

预表现技术则可以弥补这个问题: 原理讲起来就是字面意思, 本地先对玩家的操作进行预表现,当帧同步数据收到后,再计算并与本地预表现进行拟合。

预表现的难点集中在,如何精准的预判玩家接下来的行为,以及拟合的自然度。

ACT游戏,难点在于攻击判定、自身位移这类操作。这里,要使用状态同步的思想,本地只进行技能演出,不做出任何攻击判定,也就是不要干涉帧同步的内容(嗯,这是一个废话)。至于位移,是拟合的主要内容。由于每个游戏的设计,核心玩法不同,这里无法给出一个统一的方案,不过大体上就是使用Lerp将本地参数动态平滑接近帧同步的计算结果。

那么这里一个问题是,主角只有一个,怎么同时接受两个分裂的处理呢?

方法也很简单,Copy一份逻辑版本的主角即可,这个主角没有View层,所以消耗也不大。
对了,使用预表现策略时,帧缓冲可以关掉,或者降低缓存值。此时,通信的及时性更重要。

好了,本次分享就是以上内容了。

在文章更新的途中,有朋友提问:网上都说帧同步的反作弊难做,怎么到你这就是简单了呢?

我的回答: 首先要明确游戏类型,在一些热门游戏类型中,帧同步确实存在反作弊的问题。例如:MOBA,FPS 。 由于帧同步是本地运算所有角色的状态,即便是view层屏蔽了玩家,也可以通过一些手段拿到逻辑数据,这就等于掌握了全局信息,开了上帝之眼。 而ACT游戏,大部分情况是同屏战斗,信息不对等的影响非常小,基本可以忽略不计。

Email
57085445@qq.com

讨论群
LostGate同人游戏 184379459
魔力宝贝官方手游群 540221885