详解帧同步和状态同步
在网络游戏开发中,同步是一个很重要的概念。什么是同步?同步就是多个客户端表现一致。举例来说,例如王者荣耀的一局游戏里,十个玩家在游戏里看到的表现应该是相同的,玩家A在某一时刻朝某个方向释放了一个技能,产生了一定效果,剩下的九个人也应该在同一时刻看到玩家A释放技能,产生效果。又或者一个大型的 MMORPG 游戏游戏中,某个玩家朝 boss 释放技能,造成 boss 血量减少,场景内剩余的玩家也应该能看到技能的释放和 boss 血量的减少。这就是网络同步,同步的目标就是时刻保证多台设备的表现相同。甚至在某些时候,不止需要不同客户端的表现一致,客户端和服务端的数据也应该是一致的。由此可见,同步是一个网络游戏中的概念。网络同步通常可以分为两大类:状态同步和帧同步,这两种方法在多人在线游戏中用于确保不同玩家之间的数据和状态保持一致性。
帧同步和状态同步
帧同步
帧同步是一种基于时间的同步方法,其核心思想是确保游戏中的逻辑在不同客户端上按照相同的时间步骤进行。游戏世界被分为一系列固定的时间帧,所有玩家在每一帧中执行相同的游戏逻辑。每一帧都具有确定性,玩家在相同的输入情况下将产生相同的结果,这有助于确保玩家之间的游戏状态一致。
帧同步的具体实现方式是:客户端按照一定的帧速率去上传当前的操作指令,服务端将操作指令广播给所有客户端,当客户端收到来自服务器的广播指令之后,本地执行逻辑运算。如果输入的指令是一致的,计算过程也是一致的,那么所有客户端某一时刻得到的结果一定是一致的,这就是帧同步。
Tips:帧同步是游戏逻辑和时间线的同步,而不是画面的同步,帧是指逻辑帧,不是渲染帧。为了让运行结果不与硬件的运行速度关联,不能用unity中的Time.deltaTime,而是使用固定的时间片段差值,无论两帧之间的真实时间间隔是多少,游戏逻辑的执行次数都是恒定的)。渲染帧是实际在屏幕上显示的帧,由于图形渲染需要一定的时间,因此在两个逻辑帧之间可能会有多个渲染帧。为了实现平滑的显示,通常使用插值技术,将两个逻辑帧之间的状态进行平滑过渡,从而在屏幕上呈现出流畅的动画效果,所以渲染帧是逼近逻辑帧的。如果硬件的运行速率跟不上逻辑帧,可能会出现多次逻辑帧后才执行一次渲染的情况,此时画面就会有卡顿和丢帧。
举个例子来说,游戏内有 ABC 三个玩家,某一帧内角色 A 执行了攻击,角色 B 执行了移动,都会向服务器发送对应指令,服务器把角色 A 的攻击和角色 B 的移动指令广播给 ABC 三个客户端,ABC 都执行 A 的攻击,B 的移动,ABC 三个客户端的表现就是一致的。
总而言之,帧同步将逻辑放在客户端去做,服务器只负责转发操作,不做任何逻辑处理。相同的输入+相同的时机 = 相同的显示,发送的是操作,接收的也是操作。
状态同步
状态同步是一种基于事件和状态的同步方法,确保所有玩家在共享的虚拟游戏世界中看到相同的游戏状态和事件。与帧同步不同,状态同步不要求将游戏逻辑绑定到特定的时间帧上,而是侧重于在发生事件或状态变化时广播这些变化,以确保所有参与者在同一时间线上。
状态同步的具体实现方式是:服务器掌握场景内所有的实体情况,客户端已知的所有属性都是由服务器传给客户端的,例如自己的血量,队友的血量,boss的血量等。当事件发生时,客户端向服务器发送指令,服务器经过计算后,把改变后的状态和属性广播给所有客户端。例如 Boss 战时,玩家 A 释放了一个技能攻击 boss,服务器收到 A 释放技能的指令后,需要验证攻击是否生效,计算玩家 A 的剩余蓝量,Boss的血量等,然后把计算后的这些状态广播给 ABC 客户端,客户端再进行相关的显示。又比如在匹配房间中,房主交换两个玩家的位置,会向服务器发送一个指令,服务器验证更换是否生效,生效则把更新后的位置信息发送给每一个客户端,客户端收到位置信息后更新客户端显示,保证每个客户端看到的效果是一致的。
总而言之,状态同步就是将逻辑放在服务器去做,并且在服务器进行校验,然后把计算结果发送给客户端,客户端再进行显示。发送的是操作,接收的是状态。
帧同步和状态同步比较
帧同步和状态同步的主要区别就是核心逻辑写在哪里。帧同步的核心逻辑在客户端,状态同步的核心逻辑在服务器。
- 客户端代码复杂程度
- 帧同步:帧同步的客户端代码编写复杂程度比状态同步复杂的多,因为核心逻辑在客户端进行计算。由于需要保证每个客户端的计算结果完全一致,一些会产生随机结果的数据结构和算法就不能直接使用。而且在一些情况下,需要避免使用无序的数据结构,例如字典,因为在不同的客户端上,数据结构的遍历顺序可能不同。在这种情况下,可以考虑使用有序的数据结构,或者在同步时明确定义数据的顺序。
- 状态同步:状态同步的客户端代码实现起来很简单,因为计算逻辑发生在服务器,客户端主要只进行展示,只要保证服务器发给每个客户端的结果都是一致的就行。
- 服务器代码复杂程度
- 帧同步:帧同步的服务器代码编写相对来说比较简单,因为服务器只负责转发数据。
- 状态同步:状态同步的服务器代码比较复杂,因为状态同步的大量逻辑运算都是在服务器上进行的,服务器需要负责计算、校验、广播等工作。
- 开发效率
一般来说帧同步的开发效率会比较高,因为战斗逻辑都写在客户端,服务端只负责转发操作消息,双端开发的时候不需要做额外的对接工作。 - 流量消耗
状态同步的流量消耗高于帧同步,因为帧同步只转发操作,而状态同步需要转发大量的状态信息,流量消耗更大,例如一个英雄可能有100多条属性,每次改变都要同步一次属性,流量消耗很大。而且因为同步数据量比较大,所以状态同步对服务器带宽要求比较高。 - 客户端体验
帧同步的逻辑运算是在本地进行的,服务器只转发操作,速度更快,时间更短,所以它的反应更加灵敏,打击感更强,细节反馈更好,而状态同步的延迟要相对高一些。 - 网络影响
在传统帧同步方案中,网络条件较差的客户端会影响其他玩家的游戏体验。(优化方案:乐观帧锁定、渲染与逻辑帧分离、客户端预执行、指令流水线化、操作回滚等)帧同步要求较低的延迟,而状态同步对网络延迟的适应性较高。 - 断线重连
- 帧同步:帧同步的断线重连比较复杂,在断线重连的时候,需要执行追帧操作,服务器一次性发送缺失的帧数据,客户端加速播放服务器同步的帧数据来快速跟上游戏。例如客户端在战斗开始的第10秒断线了,第15秒连回来了,服务器需要把第10秒到第15秒之间5秒内的所有消息一次性发给客户端,客户端加速整个游戏的核心逻辑运行速度,直到追上现有进度。想象一下王者荣耀掉线之后,再进入游戏,需要加速播放一段游戏进程来追赶上现在的游戏进度。
- 状态同步:状态同步的断线重连非常简单,因为服务器持有所有对象的状态数据,断线重连之后服务器直接下发数据,客户端创建对象,同步信息即可。
- 回放
- 帧同步:帧同步的离线回放比较简单,因为只需要保存一局游戏内每一帧的操作即可,然后把每帧的操作信息播放一遍即可。但帧同步的实时回放比较困难,因为需要本地对全场数据进行序列化,才能回到目标时间,回放完后还要追上实时游戏状态。
- 状态同步:状态同步支持离线回放,但是需要不断记录状态信息,回放时读取合适时间的信息,回放文件一般很大。实现实时播放比较容易,因为可以方便的记录快照信息,并按照录制内容随时播放。
实时回放:实时回放允许玩家在游戏进行的同时观看录像。这意味着玩家可以随时切换到回放模式,查看游戏的实时进展。
离线回放:离线回放允许在游戏结束后或在不同时间点观看录像。玩家可以选择特定的游戏回合、关卡或时间点进行回放。
- 安全性/反作弊
- 帧同步:安全性较差,因为帧同步战斗逻辑都发生在客户端,服务器没有验证,容易产生外挂(加速、透视、自动瞄准、数据修改等)。引入服务器校验(对比各个客户端的结果)可以解决一些常规外挂,但是解决不了透视、全图视野之类的外挂。
- 状态同步:状态同步安全性较高,因为核心逻辑计算发生在服务器,服务器对数据具有权威性,而攻击服务器远比修改客户端复杂,所以状态同步比较容易实现反作弊。但是仍然解决不了透视,全图视野之类的问题。
- 跨平台
- 帧同步:不适合跨平台,因为对结果一致性的要求严格,需要考虑浮点数修正等问题。
- 状态同步:适合跨平台,因为服务器服务器实时传输权威数据,微小误差影响不大。
- 性能开销
- 帧同步:帧同步客户端性能开销大,服务器性能开销较低。
- 状态同步:状态同步客户端性能开销较低,服务器性能开销较大。
- 物理系统
Unity的物理系统一般是基于浮点数计算的,即使是相同的输入,计算结果也可能具有不确定性。即使每一帧计算的结果相差很小,但成千上万帧之后,计算结果的误差也可能变得很大,导致不同客户端出现不同的表现。所以使用帧同步一般需要实现一套确定的物理计算,使用定点数来代替浮点数,确保计算结果的一致性。而状态同步则没有这个问题,因为服务器持有权威数据,误差会在下一次被覆盖掉,确保每一个客户端的表现一致。
帧同步和状态同步的应用场景
状态同步基本上适合绝大部分的游戏,包括帧同步擅长的领域,用状态同步也能有较好的实现。而帧同步的局限性就更大一些,但在合适的情况下能有比状态同步更好的表现。
那么什么情况下更适合用帧同步,什么情况下更适合用状态同步呢?
- 一般情况下,少量玩家,游戏时间较短,每一局游戏中不允许其他玩家中途加入,这种游戏更适合采用帧同步,能获得更及时的反馈和更好的打击感和操作手感,如王者荣耀5v5, 守望先锋等。
- 而 MMORPG 等基本都是采用状态同步,因为场景数据计算量巨大,例如要持有并计算场景上1000个对象的状态,客户端性能达不到,而且安全性要求更高,需要服务器进行更多的数据校验。
- 在状态同步和帧同步都适用的一些场景,往往需要考虑其他因素,例如网络波动比较厉害的环境,比如地铁上手机信号不好,传输大量数据往往会造成数据的延迟,影响游戏体验,这时可以考虑帧同步。
帧同步的优化方案
乐观帧锁定
传统严格帧锁定算法中,要求客户端定时发送指令,服务器必须等待收集到所有客户端的包之后再下发,每个时间切片服务器只有收到所有客户端传过来的消息后,才会统一广播给所有客户,网速慢的客户端会卡住网速快的客户端,出现“一人卡机全员等待”的问题。实践中可以采用乐观帧锁定,即定时不等待的方式,无论服务器是否收到客户端的信息,都将目前为止收集到的指令封装并下发,客户端发现自己的指令丢失之后应该将指令重传。这样所有客户端不会因为某个客户端的卡顿而卡顿。具体实现是:服务端每一秒,固定下发一定帧数的逻辑帧到客户端(视具体项目而定),不需要等待全部客户端发送指令,客户端对于每一个逻辑帧,补充几个渲染帧。如果客户端没有update数据了,就等待新的数据到来。如果一下收到很多连续的update,那就快进播放。
逻辑和显示分离
在帧同步中,游戏逻辑不依赖于具体的显示层,而是基于独立的逻辑帧进行处理。这样的设计允许在没有加载角色模型的情况下运行游戏逻辑,支持服务器端验证,提高跨平台和版本兼容性。
帧预测
帧预测是一种优化技术,通过客户端预测下一个时间切片的游戏状态,减少与服务器的通信量,提高游戏响应速度。实现帧预测的步骤包括①客户端记录当前状态和指令;②客户端根据当前的游戏状态和输入指令,预测出下一个时间切片的游戏状态,并在本地进行渲染;③发送输入指令至服务器;④服务器计算下一时间切片的状态并返回给所有客户端;⑤客户端比较并进行帧回滚。帧预测能有效减少通信量,但也带来一些问题,如预测误差和帧回滚。
帧回滚
帧回滚是一种帧同步中的纠错机制,用于修正客户端预测的游戏状态与服务器实际状态不一致的情况。实现帧回滚的过程涉及记录、预测、通信、服务器计算、比较和修正等步骤。帧回滚的实现需要考虑预测误差、网络延迟和帧率波动等因素。尽管帧回滚可以确保各客户端游戏状态一致,但也会引入一定的延迟和卡顿,因此在选择帧同步方案时需要综合考虑游戏性质和网络条件。