Unity制作手游的帧同步时遇到的问题

发表于2018-06-08
评论1 3.6k浏览
网络游戏的同步可以做的很简单,也可以做的很复杂。简单来说就是通过Http或者Socket来跟服务器同步数据。而如果往复杂了说,可以有p2p、帧同步、航位预测等等高级课题可以研究。

因为我们的项目需求----几百个独立单位的实时同步(带pvp的rts),所以研究了下帧同步。不过后来发现它其实有很多问题,所以具体如何还要看其他同事的研究成果。以我个人来说,修改游戏方案反而是最合理的解决方案。

首先从需求上来说,几百个独立单位的作战,本身结果和士兵强弱从策划的角度来看就是不可控制的。玩家更加不可控制。比如士兵的攻击力增加10%或者弓箭手的射程增加50%,带来的影响可能是几倍于预想值。这样一个不可控制的游戏对玩家而言,即便再独特,其吸引力也不如卡牌游戏。 从这点上来说,项目的需求本身就是值得商榷的。

帧同步的原理和实现可以参考这些文章:
http://clintonbrennan.com/2013/12/lockstep-implementation-in-unity3d/
http://blog.sina.com.cn/s/blog_674f1bd20101omv7.html

大意是,游戏运行时以10fps(100毫秒间隔,具体数值可根据实际情况调整)运行一个逻辑帧,逻辑帧负责物理、ai、攻击判定等等。而动画和实际位移由渲染帧负责。这样动画表现是流畅的。而客户端每逻辑帧都会与服务器进行通信同步客户端的操作,当操作同步完成客户端的逻辑帧可以继续向后模拟。比如运行第3帧要确保第一帧的数据是完整的。双方客户端接收的操作内容是一致的,初始状态是一致的,所以运行的结果也应该是一致的。随机数可以使用确定随机种子的伪随机数来解决。

所以关键问题就是要确保客户端以同样的输入可以获得同样的运行结果。 这个是理论上可行的,很多rts游戏也都是基于此原理。比如魔兽争霸3  最高指挥官2  全面战争  星际争霸 帝国时代 等等。  以最高指挥官(Super Commander)为例,其全部单位有上千个,如果要实时同步他们的状态以现在的网络条件都是很困难的,所以这些rts同步的仅仅是操作,即点击了什么位置,点击了什么按钮,选择了什么目标,按了什么按键。客户端接收到对应的操作,进行同样的模拟,达到同样的输出。从而完成网络同步。

魔兽争霸的录像机制也是基于此原理,记录操作,而不记录状态。所以几十分钟的游戏,其战报只有几百k。同样因为这个原理,所以在出bug的时候(概率不高,但是玩家多了,还是很容易碰到的)会造成录像与实际玩的时候不一致。比如玩的时候剑圣暴击杀死了对手,从而翻盘取得了经典的胜利,但是战报播放的时候却没有发生暴击,直接被对方给推了。  全面战争也是如此,所以也会出现战报跟实际游戏不一致的情况,不过概率确实不大,只有发生极限情况才可能出现这样的问题。

但是,如果拿Unity实现一个RTS手游,要实现确定性的模拟就非常困难了。因为Unity中的Start Awake Update等函数的调用是不受控制的,协程、SendMessage、Invoke也是不受控制的,动画事件更加不受控制,物理使用Physic也是不保证确定性模拟的,即OnTriggerEnter这些函数的调用或者RigidBody的运动都可能存在误差。一开始只是非常小可以忽略的误差,而误差可能逐渐积累最终的蝴蝶效应就是战斗结果完全不一致。比如一开始只是一个单位的坐标偏差了0.1米,但是可能影响到一个箭有没有射中它,进而影响到它究竟死没死,如果单位的死亡产生不一致了,那么后续的战斗就完全错乱掉了。

这个就是帧同步的过程中的“不同步”现象。帝国时代和最高指挥官会在每逻辑帧计算当前所有单位的属性的crc值,几个客户端之间比较这个crc值,如果发现不一致,那么就是不同步现象。此时可以把不同步的客户端踢出游戏,也可以强制同步某个单位或者是所有单位的状态。产生不同步现象的原因可能很多,比如机器卡了一下,网络卡了一下。无论什么原因,但是确实是出Bug了,悲剧的是,这个Bug是非常难查的。可能是使用某个特殊技能造成一个特殊效果的时候,恰好杀死了对方,这种情况下会出Bug。也可能是错误的使用了被内存池回收的数据,如果此时此数据已被重新分配出去,那么就会出现Bug,如果没有被重新分配出去,那么可以“正常”的运行。 当游戏越复杂,这种Bug可能越多,并且几乎不可查,因为只有当大量玩家玩的时候才会暴露出这样的问题,QA根本无法重现这个Bug。 即便不是帧同步,“确定性”模拟,这类Bug都是最让人头疼的,可能会“无法重现”几年,更何况对精确度要求非常高的时候。

实现一个“确定性”模拟的客户端,其难度很高,并且维护成本非常大。每个添加的技能都要深思熟虑,并且要花大量的时间去测试。每个新添加的代码都可能造成不同步现象,除非战斗部分写好后一百年不动,否则无论是重构还是添加新功能都会战战兢兢,如履薄冰。

不光是Unity本身的问题,底层和上层都需要做相应的处理。

底层要保证浮点数的“确定性”,这个本身就是一个高级课题,保证后还要考虑修改后的效率问题,毕竟手游上单位多了,简单ai也会成为大问题,如果浮点数计算的效率低了,很可能会大大的降低运行效率。数据结构如List也可能会有不稳定排序的问题。

上层的ai逻辑也要精心设计,以尽可能“准确”的描述来执行行为,比如a单位移动到b点,攻击c,这样的ai执行起来相对可靠。而如果是a追踪b单位,直到x单位进入攻击范围则开始攻击,这个就埋下了很大的隐患,除非所有的计算和调用都是确定性的。

以手游来说,以帧同步来实现战斗同步还有一些副作用。比如要保证客户端完全匹配,无论是配置还是版本,差一点儿都没有办法一同游戏。所以无论是安装包还是配置,只要有修改就必须要让玩家更新,否则不能进入战斗。  而不同设备、不同cpu,是否会造成不同的结果,这个不能百分百的肯定,也就是说,游戏的兼容性有很大的隐患。

总结一下就是,现在手游上使用帧同步来处理rts并不现实,迄今为止我并没有看到成功的例子。 而早期的rts游戏,可能因为游戏相对简单,自研引擎相对可控,再加上大量人力物力的投入,这些跟我们手游使用Unity快速开发游戏的模式并不相同。 而除开rts,我并不认为其他的游戏形式如ARPG、横版格斗等等需要使用帧同步,正常的行为和状态同步足够了。注意,帧同步的确定性模拟这个需求是个大难题,并不是帧同步本身有多难。也并不是说不使用帧同步,客户端就不能自己运行逻辑,进行动作先行或者是预测。

参考资料(这些资料都相当经典,强烈推荐阅读):
1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond
http://www.gamasutra.com/view/feature/3094/1500_archers_on_a_288_network_.PHP
Syncing System of TA spring
https://springrts.com/wiki/Syncing_System
Opinion: Synchronous RTS Engines And A Tale of Desyncs
http://www.gamasutra.com/view/news/126022/Opinion_Synchronous_RTS_Engines_And_A_Tale_of_Desyncs.php
SYNCHRONOUS RTS ENGINES 2: SYNC HARDER
http://forrestthewoods.com/synchronous-rts-engines-2-sync-harder/
Cross platform RTS synchronization and floating point indeterminism
http://gamasutra.com/blogs/MaksymHryniv/20150107/233596/Cross_platform_RTS_synchronization_and_floating_point_ind

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引