概述

本文介绍了开发帧同步游戏中的一些经验。包括一些开发和测试过程的方法。以及包含在帧同步游戏中使用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

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

The time to advance physics by.

 

这里提到,如果传入固定的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的物理引擎需要慎重。虽然某些物理引擎的功能会带来便利,但是不能使用刚体。