关于游戏帧同步问题的总结

概述

本文介绍了开发帧同步游戏中的一些经验。包括一些开发和测试过程的方法。以及包含在帧同步游戏中使用Unity的物理引擎的可行性分析及遇到的问题。

 

帧同步的原理简述

要保证各个客户端的游戏表现同步,主要是保证各个客户端的数据同步,因为表现是依赖于数据。以MVC架构为例,数据就相当于MVC架构中的M(Model)

以游戏结构来说,一般而言,架构如下

 

  1. 界面显示依赖于数据模型
  2. 界面如果需要更改数据,需要将操作同步到所有客户端,不能直接在本地修改。

输入设备如果需要更改数据,同样需要将操作同步到所有客户端。

帧同步游戏的特点

相对于状态同步,帧同步有以下几个特性。

  1. 优势

1.1       同步简单。只要同步框架做好,开发过程可以把游戏当作单机游戏来做,极大的减少同步工作量。

1.2       可以处理非常复杂的同步过程。如果一个操作导致一连串的连锁反应,一般状态同步处理过程非常困难。而帧同步只需要同步初始操作指令。例如类似王者荣耀的动作游戏。一个技能的逻辑非常的复杂,用状态同步就非常的困难了。

1.3       不需要服务端参与同步工作,免除了大量的服务端与客户端的同步协议与调试工作。

         2.劣势

2.1   帧同步同时连接的客户端数量有限制,因为同步消息需要广播给所有客户端。一般使用帧同步的游戏客户端不超过20个。

2.2     地图单位数量有限制(不适合开放世界大地图)。因为客户端需要模拟整个游戏,如果游戏世界的运算量太大,客户端会有较大的计算压力。而状态同步客户端只需要模拟部分可见区域。

2.3  即时性较低。玩家的操作需要等待数据包与服务器一个来回的时间。因此不适合做即时性要求非常高的游戏。例如FPS。玩家开火需要立即反馈,如果需要等待数据包返回,那玩家的延迟感受就很明显了。

 

开发注意事项

要保证各个客户端数据同步,开发过程需要主意几个地方:

  1. 初始化数据一致。保证初始数据一致,包括初始化顺序也需要一致。
  2. 遍历列表和字典的顺序需要一致。字典最好使用有序字典,以方便遍历。排序最好使用稳定排序,防止各个客户端排序列表不一致。
  3. 浮点数精度截断。浮点数运算在各种硬件上会有精度差异。一般截断浮点数保留4位可以防止精度问题。
  4. 本地玩家逻辑判定不能直接更改数据。一般而言游戏需要依据单位是否为本地玩家而做一些特殊的表现。例如:单位完成任务,如果是本地玩家需要一个界面提示。主意本地玩家判定内部不能修改数据,因为各个客户端的本地玩家是不同的。
  5. 随机数一致。初始化各个客户端随机数种子一致,保证各个客户端输出的随机数一致。
  6. 输入数据需要同步。本地玩家的操作需要用帧同步指令同步给所有客户端。输入设备包括硬件设备(鼠标键盘等),也包括界面操作的输入。例如虚拟摇杆。
  7. 配置表数据一致。

 

测试方法

帧同步游戏在开发过程中只需要主意几个点,不需要关心同步问题。但是难保开发过程主意到了所有问题。在交付游戏包的时候需要严格的测试。而找游戏的不同步问题也是比较麻烦的和耗时的,因为往往看到不同步的表象的时候,已经是蝴蝶轻微震动翅膀产生的飓风了。

这里列举一些测试方法:

  1. 日志记录关键位置数据。可以将这些日志输出为专门的同步日志文件。用来比对一致性。完全同步的情况,输出的日志应该完美一致。如果不一致,日志也可能提供一些线索。
  2. 检查以上列出的事项是否有遗漏。例如搜索排序算法,字典遍历等过程。
  3. 以上都不行的话,可以用一个比较耗时的方法:排除法。

检查没有任何操作(移动、使用技能等)的时候是否同步,如果同步,增加操作,直至出现不同步的操作。如果没有任何操作还是不同步,将游戏逻辑屏蔽掉一部分再测试。例如把游戏AI关闭再测试,直至出现同步的情况。

如果操作太多的情况,为了加快测试过程,可以将操作列举出来,然后用二分法,一次选择一半的操作进行测试,如果出现不同步,那造成不同步的操作可以限定为选择的这一半,再将这一半操作二分,迭代测试,找出具体是哪个操作导致的不一致。

物理同步

2017之后版本的unity官方文档中,Physics接口提供了新的API:

Physics.Simulate

Other Versions

Leave feedback

public static void Simulate(float step);

Parameters

step

The time to advance physics by.

Description

Simulate physics in the scene.

Call this to simulate physics manually when the automatic simulation is turned off. Simulation includes all the stages of collision detection, rigidbody and joints integration, and filing of the physics callbacks (contact, trigger and joints). Calling Physics.Simulate does not cause FixedUpdate to be called. MonoBehaviour.FixedUpdate will still be called at the rate defined by Time.fixedDeltaTime whether automatic simulation is on or off, and regardless of when you call Physics.Simulate.

Note that if you pass framerate-dependent step values (such as Time.deltaTime) to the physics engine, your simulation will be non-deterministic because of the unpredictable fluctuations in framerate that can arise.

To achieve deterministic physics results, you should pass a fixed step value to Physics.Simulate every time you call it. Usually, step should be a small positive number. Using step values greater than 0.03 is likely to produce inaccurate results.

 

 

这里提到,如果传入固定的step参数,Simulate将产生一个确定的输出。也就是说,物理引擎支持帧同步。

如果物理引擎支持帧同步,那将可以利用物理引擎极大的助力游戏开发。

这次借助开发Demo的机会,也探索了一下这方面的可行性以及遇到的坑。

一开始搭建项目框架的时候,一切似乎都没有问题。物理引擎确实能够产生确定的输出。随着地图单位的增加,出现性能瓶颈。这时候开始着手优化性能。观察unity的profiler发现,CharacterController.Move方法,非常的消耗性能。当时将Move方法改为SimpleMove,测试之后,出现了角色坐标不同步现象。SimpleMove方法,传递的参数是Vector3 speed而移动一个单位,速度参数是不够的,还需要时间参数( s = v*t )。而这个方法的时间参数取自哪里不得而知。这可能是造成不同步的根源。

接下来我将CharacterController组件替换为了Rigidbody组件,使用刚体来驱动角色位移。性能也得到了极大的改进(300个单位,CharacterController.Move耗时9毫秒,Rigidbody 2毫秒)。这个时候,更为诡异的事情出现了,测试结果偶尔同步,偶尔不同步,并且如果让单位之间可以穿透,不同步的现象极大的降低(本机测试基本上是不出现了)。但是多端一起测试的时候,不同步现象还是容易出现。后来通过日志发现,只要玩家使用弹道技能,弹道碰撞到单位之后,这个单位的坐标极大概率会不同步(通过使用上面提到的二分法找到的问题)。因此确定了问题:刚体间的碰撞会造成不同步。单位之间互相穿透,降低了单位碰撞的频率,因此不同步的几率降低了,但是单位依然会与建筑等阻挡碰撞,依然会有概率出现不同步现象。

目前的解决方案是将玩家单位的刚体组件还原为CharacterController,将AI角色的移动组件设为NavMeshAgent。场景中不使用任何刚体。仍然可以使用物理引擎的触发器,碰撞检测方法。虽然CharacterController有性能问题,但是玩家单位比较少可以忽略不计。

总的来说,帧同步项目使用Unity的物理引擎需要慎重。虽然某些物理引擎的功能会带来便利,但是不能使用刚体。

### 游戏帧同步的实现方式及原理 帧同步是一种在多人游戏中确保所有客户端状态一致的技术。其核心原理是通过同步玩家的操作输入,而不是直接同步游戏中的对象状态,来保证每个客户端的游戏逻辑能够产生相同的结果[^1]。 #### 原理详解 帧同步的核心在于操作输入的同步与确定性逻辑的执行。每个客户端会按照固定的时间步长(通常称为“帧”)收集玩家的操作输入,并将这些输入发送到服务器或其他客户端。服务器或对等客户端接收到这些输入后,按照时间顺序重新应用这些输入以更新本地的游戏状态。只要所有客户端的代码版本一致,并且随机数生成器的状态也同步,那么所有客户端运行出来的表现结果就会是一致的[^2]。 #### 实现方法 以下是帧同步的具体实现方法: #### 1. 操作输入的收集与传输 每个客户端需要定期收集玩家的操作输入,并将其打包成数据帧发送到服务器或其他客户端。这些数据帧通常包含当前的帧索引和具体的操作信息。例如,在一个实时策略游戏中,操作输入可能包括单位的选择、移动命令、攻击目标等[^1]。 ```csharp public class InputCollector : MonoBehaviour { private List<FrameData> inputBuffer = new List<FrameData>(); public void AddInput(int frameIndex, string inputData) { inputBuffer.Add(new FrameData { FrameNumber = frameIndex, InputData = inputData }); } public List<FrameData> GetInputs() { return inputBuffer; } } [System.Serializable] public class FrameData { public int FrameNumber; public string InputData; // 可以扩展为更复杂的数据结构 } ``` #### 2. 同步帧的处理 服务器或对等客户端接收到操作输入后,会按照时间顺序将这些输入应用到本地的游戏逻辑中。为了保证一致性,所有客户端必须使用相同的时间步长进行更新,并且游戏逻辑必须是确定性的,即相同的输入总是产生相同的结果。 ```csharp public class FrameProcessor : MonoBehaviour { private List<FrameData> frameBuffer = new List<FrameData>(); private int currentFrameIndex = 0; public void ProcessFrames() { while (currentFrameIndex < frameBuffer.Count) { var frame = frameBuffer[currentFrameIndex]; ApplyInput(frame); currentFrameIndex++; } } private void ApplyInput(FrameData frame) { // 根据输入数据更新游戏状态 Debug.Log($"Processing frame {frame.FrameNumber}: {frame.InputData}"); } } ``` #### 3. 确定性逻辑的实现 为了确保所有客户端的游戏逻辑一致,必须避免任何非确定性的因素。这包括但不限于: - 使用固定的随机数种子,以确保随机数生成器在不同客户端上产生相同的结果。 - 避免依赖系统时间或硬件特性,因为这些因素在不同设备上可能会有所不同。 #### 4. 延迟补偿与回滚机制 在网络延迟较高的情况下,某些操作输入可能未能及时到达目标客户端。为了解决这一问题帧同步通常结合延迟补偿和回滚机制。当检测到输入延迟时,客户端可以暂时回滚到之前的状态,重新应用正确的输入以修正当前状态。 #### 定点数的应用 在帧同步中,为了避免浮点数运算带来的精度损失,通常会采用定点数来表示和计算逻辑数据。定点数是指计算机中采用的一种数的表示方法,其中参与运算的数的小数点位置固定不变。这种方法常用于对精度要求比较高的逻辑/数据计算中[^3]。 ### 总结 帧同步适用于对同步要求较高、玩家数量适中的场景,如格斗竞技类游戏和MOBA类游戏。其优点在于数据量较小、同步效率高,但缺点是对网络延迟较为敏感,需要额外的延迟补偿和回滚机制来保证一致性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值