唰唰冷呵映 发表于 2021-1-21 11:52

Unity帧同步解决方案(二)

上篇讲了帧同步的基本原理和优化思路,比较偏理论一点。本篇更侧重实践,到底怎样在Unity上做一款帧同步的Moba手机游戏。
逻辑和显示分离
我们上大学的时候,天天说MVC结构,基本上所有通过代码衍生的产品几乎都有这样的思路。所谓MVC,是指数据,逻辑,显示的分离。而一般对于游戏来讲,特别是帧同步的战斗模块,逻辑和显示是最重要的,也是必须的。因为逻辑是需要在服务器上运行的,而服务器是没有显示功能的,并且战斗服务器的代码和客户端是一样的,这就决定服务器在脱离显示模块,也能正常运行。
那么对于Unity来讲,怎样做到逻辑显示分离呢。这里以闹闹项目为例,用Lua作为脚本语言。lua和Unity的交互这里就不细讲了,具体参考XLua。首先,Unity作为游戏引擎,承担了输入和输出的责任,对于闹闹来讲,战斗中游戏的输入主要是JoyStick,既玩家的移动和释放技能指令。输出则是动画,模型,特效等显示相关以及声音等。针对JoyStick,模型动画,特效,声音,我们分别定义好Lua和C#的接口。剩下的就全都是基础战斗逻辑,对于客户端和服务器来讲,就没有任何区别。闹闹项目考虑到Lua的效率问题,将消耗比较大的移动,碰撞,寻路放入C++中。Lua和C++都是跨平台的,在客户端和服务器都能使用。
特别要注意一下C#和Lua层的箭头,除了JoyStick外,不允许有任何C#层的东西去影响Lua层的战斗逻辑,而lua和C++层则可以自由调用(考虑效率情况下)。
为了方便理解,这里举个例子。
玩家通过摇杆JoyStick(C#层)产生MoveMsg,发送给Lua层InputControl,InputControl在输入Tick的时候,处理消息(去重,加密,压缩)发送给服务器,当客户端收到服务器发送回来的MoveMsg时,FightObject的Tick会通过lua层的MoveControl,调用C++层的MoveControl,设置移动方向。在C++的Tick时,MoveControl再执行移动,当产生位移时,通知Lua层的MoveControl,Lua设置逻辑上的真实位置,Lua在发生位置改变时,通知C#层的AvatarControl。按照这样一个流程,角色就能够真实移动起来,并且在服务器上,只需要去掉通知C#层的AvatarControl(lua的FakeClass),完全不影响整体战斗逻辑。服务器某些类似乎需要重写,必须设置没有任何功能的AvatarControl,创建FightObject的流程等,可以通过Lua的DoFile重新写一份,不过这里一定要特别小心,别影响战斗逻辑,这个地方也是出现不同步的高发地段。
浮点的处理
帧同步的另一核心,必须保证在所有端跑出来的结果是一样的。但是我们都知道,浮点的运算在不同操作系统甚至不同机器上算出来的结果都是有精度差异的。浮点处理一般有两种办法,一种分数,一种定点数。据说王者荣耀是用分数(不确定)。我们觉得用分数处理起来很不方便,就选用定点数方案。
定点数,说简单简单,说不简单也不简单。因为定点数的原理很简单,就是用整数存,计算的时候用整数计算,再进行转换,这里不做过多阐述定点数是怎么实现的了,网上也有很多定点数的现成库。但是,从上面我贴出的架构来看,首先最大的难点是Lua需要支持定点数,lua中的小数是double,需要把Lua源码中的基础小数全部替换成定点数。项目分为C++,Lua,C#三层,这三层的小数接口全都要改成定点数,这一步实际上是要比上一步难,因为需要处理xlua的一些东西。再一个难点便是Box2D,物理系统一般也没有定点数,需要自己修改,Box2D我认为还算比较好修改,如果使用physic3D的话,我估计这个工作量就很大了。如果项目用到其他跟战斗相关的库,必须全部改成定点数。
浮点的处理整体工作都不是特别大的技术活,但一定需要很严谨的人花很多的时间,这里边都是细致功夫。这也是bug多发地段,必须早早稳定下来(非常重要),不然出现不同步或其他bug了,都没法确定到底是浮点本身出问题了,还是其他战斗逻辑代码所致。
定点数,如果用8位(256)存小数部分,那么小数的精度是1/256,整数最大24位(800w左右)。这个在战斗逻辑一定要考虑进去,可能会出现很多问题,比如:0.1*0.1*10 ≠0.1。遇到这样的问题,最好绕过去或者接受。
随机函数
伪随机是必须的,伪随机的算法网上也到处都是。基本功能只需要,设定一个种子,取随机只跟取出的次数有关,然后就是保证随机性。即:我在AB两个机器上,设置同一个种子,取出第一个数都是10,第二个数都是23,以此类推。伪随机也必须是定点数哦,所以也需要稍微修改一下网上找下来的库。每局战斗开始,由服务器产生一个随机种子同步给所有客户端,之后遇到随机取出来的数就一样了。
随机数也是不同步的高发地段,并且这个出现不同步会带来非常严重的不同步。出现的情况有多种,但是总结就是调用次数不一致,一般出现场景有以下几种。lua有些显示相关的代码只在客户端上运行,但是需要做随机,这里必须使用unity自己的Random函数。为了节省效率,我们的AI只在服务器上运行,生产指令后同步给各个客户端,这里也是禁止使用随机的,但是AI有时候免不了用随机咋办,我一般用当时的位置或者当前帧数(当对来说是随机的)。其实AI只运行在服务器上就说明,所有影响战斗逻辑的代码都不应该存在,比如:在行为树上设置属性等。
为了验证随机函数没有出问题,可以在每帧信息中生成一个随机数,各自客户端进行对比,查看是否相同。这样方便查看出现不同步时,定位是否是随机数的问题。
帧同步工具
在整体框架搭建好后,一定要赶紧完成这几个比较重要的工具。不同步信息查看工具,录像播放工具,自动战斗工具。
不同步一定是帧同步项目在制作过程中出现最多的一个词,如何定位问题是解决问题的重中之重。我们需要制作一个工具,能过查看具体是哪个属性,在第几帧出现不同步的,并通过前后的一些信息,诸如使用哪些技能,做了哪些操作导致出现不同步。这就需要先统计信息,客户端和服务器都要统计各自的属性,操作,buff,子弹等尽可能多的跟战斗相关的信息。客户端统计完,发送给服务器,然后服务器保存在本地。这里涉及到统计和发送两个问题,统计需要消耗效率,在lua上做这个功能是很耗时的,再一个发送会增大流量。这两个点都是在线上所不能容忍的,不过我们可以设置一个选项,在正式上线时关闭,平常测试时在内网或者PC上时,可以打开。尽管如此,我们也可以做一些优化,比如:每个单位的属性有很多,我们可以只记录改变了的属性,然后推演出来。服务器在收到调试信息后,不要每帧都进行IO操作,可以等战斗结束,或怕战斗没结束就崩溃,可以每过一段时间保存一次。服务器处理好数据后,就需要做一个可视化的工具来展示这些数据了。
仅仅制作不同步信息展示工具,只能查看到大致的方向,一些比较疑难的不同步问题还是很难查找,我们就需要制作录像播放工具。录像播放对于帧同步的意义非常大,本身帧同步的一点非常大的优势在于能很容易做到完全重现战斗录像。能播放录像,就能解决好多问题,一旦发现bug,只需要重播一次就好了,之前做游戏最头疼的就是无法复现的bug。重播录像也能提供给策划查看游戏的玩法生态,或者重现一些策划数据达到预料之外的情况。最后,几乎所有的帧同步游戏都有观战系统,这个系统能很好的提升玩家生态。录像有这么多好处,其他类型游戏为什么没做呢,主要问题还是太困难。而对于帧同步来说,要实现这个非常简单,帧同步的架构本身就支持这项工作,我们只需要保存每帧各个客户端的输入消息。事实上,服务器本身就已经有这写信息,我们只需要把这些信息保存起来,然后再在客户端读取这些信息,这些信息量也比较少,一场战斗就几兆大小,自然而然,录像系统就完成了。
自动战斗工具是有天我看到测试,在不断开房间挂机,查看是否有崩溃或报错。那么,弄一个自动战斗的系统,只需几行代码,就能让无数机器24小时为我们测试,这样极大地减少了策划的工作量。既然产生了这么多战斗场次,一些数据对我们来讲相当宝贵,一方面能筛选出出问题的场次(不同步,崩溃,报错),利用上面两个工具解决问题。另一方面,策划也能通过这些场次的TLog(战斗信息)粗略地验证战斗数值的合理性。
不同步问题总结
浮点和随机上面说了,这里不做过多叙述。
代码不要出现“我”这样的逻辑,对于不同客户端和服务器“我”是不同的,但是据我观察,这个是避免不了的,特别对于界面显示来讲,所在在使用“我”的时候一定要注意,不要影响战斗逻辑。
服务器有一些自己的Lua代码,一部分是战斗服的逻辑,还有一部分则是重载了客户端的一些方法,因为有些逻辑,服务器确实是不一样的,这些地方一定要特别谨慎。
原则上除了Input是不让C#调用lua代码的,但是也是不可避免有些显示相关的需要回调到lua,这里也千万不要改变战斗逻辑,建议从C#回调lua的战斗代码统一写到一个地方,方便查阅和检查。
不确定顺序的容器的遍历要严令禁止,lua表的遍历,for pairs是必须要禁止的,可以使用for ipairs。C++的Map等,总之在使用容器的遍历时,一定要查阅是否是确定顺序的。
不同平台的long型是不一样的,long这个类型最好是不要使用,要么用int,要么用longlong。




先写到这把,可能还有些补充,想着需要写很多,但是写起来就老忘,以后想到啥再作补充吧。
页: [1]
查看完整版本: Unity帧同步解决方案(二)