目录
为什么选择帧同步
游戏开发最常用的两种同步就是帧同步和状态同步。
两种同步的技术特点各不相同各有优劣,但是通常规模不大的项目还是偏向帧同步。
主要原因还是在于开发周期短,好维护,好移植,流量消耗小。而且一般只要做好一套框架可以复用于多个游戏。
但是在对数据安全性要求很高,同步度要求很高的游戏还是应该使用状态同步。
类型 | 原理 | 安全性 | 维护性 |
状态同步 | 逻辑在服务器,服务器 运算战斗结果通知客户端,客户端做表现。 | 数据都在服务器,非常安全 | 1.实现原理容易。 2.开发的沟通成本高(逻辑和表现分散在服务器和客户端需要调试大量接口)。 3.可移植性差,每款游戏斗得从头来一遍。 4.流量消耗大。 |
帧同步 | 逻辑和表现都在客户端,客户端将玩家的操作上发给服务器,服务器接收后广播给所有客户端,客户端在执行。 | 数据在客户端,玩家可以修改数据 需要用其他办法保证数据安全性 | 1.实现比较考研架构设计。 2.开发沟通成本极低(服务器只做转发)。 3.可移植性较好(服务器基本上没什么改的,客户端也能保留一套基础框架)。 4.流量消耗极小。 |
如何设计帧同步架构
这里主要考虑使用的是lockstep模式。
关于lockstep的具体原理分析网上有很多文章,个人觉得https://gameinstitute.qq.com/community/detail/104156这篇文章解释的很好。可以参考下,这里就做一个简单的介绍。
LockStep概述
lock-step从名字来看这个设计分为两部分,lock:锁定 step:步骤。
我们设想一个场景,现在一个教练在训练两个方阵走正步。为了保持大家步伐一致,于是由教练发出口令“一”,“二”,“一”,“二”,而两个方阵在接收到命令之后同时“一”这个口令解析为迈左腿,“二”解析为迈右腿。于是两个方阵就会按照一致的步伐前进。
同理,我们游戏做战斗的时候将用户的操作看作一系列的指令。这些指令由客户端发出上报给服务器之后,由服务器在特定的时间广播给所有客户端,于是所有客户端都在同一事件会执行一样的操作。这样理论上就能实现我们的同步了。
而这里的step就是服务器收发一次指令的间隔。
什么时候触发lock呢?当我们某一个客户端因为性能问题导致自身的一个step周期拉的太长,没有及时上报给服务器。为了保证同步,这个时候所有人都必须等待最慢的这个而lock自己(就像踢正步的时候其他人腿抬起来锁定住不往前走,等待最慢的那个人也抬起腿)。等到最慢的客户端也终于走完自己的step了,所有客户端再执行下一个step(一起继续走下一步)。
LockStep中的核心问题
上述原理理论上只要所有客户端收到同样的执行做了同样的操作就实现了同步。
但是问题往往出现在同样的指令执行结果并不一致上面。
其中最核心的就是两个问题,浮点数运算随机器出现结果不一致,随机数生成不一致。
如何处理浮点数
关于浮点数的处理,个人看了很多方案。最常用的是两种。
一:同步移位将一个float转换为long,舍弃掉后面的位数。
二:通过将原有的数据放大1000倍,再截取整数部分。
两种方法的核心思路都是一致的,即通过舍弃掉可以忽略不记的小数部分(浮点的误差就在这个之下了),来保证不同的机器运算结果一致。
如何处理随机数
关于随机数一般的语言都会提供一套关于随机种子的随机数生成方法。只要随机种子一致,每次取得随机数的结果也可以保证一致。
不过保险起见,还是尽量自己定义一套随机种子算法,关于随机数的生成这里有一篇很详细的博客https://blog.csdn.net/fengying2016/article/details/80570702。
我个人是取了其中的均匀分布的随机数算法来加以封装处理,其原理如下:
其中r为随机种子,每场游戏重新定义,a,b,base为系数由自己定义。
如何处理画面卡顿
如上述的lock-step,其中有一个重要的环节就是确定step有多长。
在unity里通常游戏的帧率是60帧,也就是每帧16毫秒的样子。如果我们的step也设计为60帧,那么我们的指令就能够及时的被处理。但是问题是,往往我们的游戏每帧间隔都是不稳定的,一般可能会有几毫秒甚至更大的波动。这就会导致每次客户端的step总是会出现误差,从而一直出现lock的情况,导致画面断断续续。
另一种情况,如果我们把每帧的间隔拉长,比如设置帧率为20帧。这样每个客户端的step都差不多比较稳定了,但是新的问题来了,如果我们以每秒20帧运行游戏,当游戏对象发生位移的时候难免被玩家看出画面抖动。
因此我们需要引入一个新的概念。
逻辑和渲染分离
根据上述的问题阐述我们可以得出,如果能够使得逻辑和渲染分离即可解决画面卡顿问题。
我们的逻辑运算需要做同步必须拉长间隔,采用20帧每秒。而画面为了保证连续性,仍然采用60帧。
只要我们设计战斗的时候将逻辑和渲染分离开,20帧的帧间隔来计算逻辑(比如我的下一个位移目标点在哪里),60帧的间隔来处理渲染表现(根据上一次的逻辑位置,目标位置,和当前执行的渲染帧数以及逻辑帧数来插值得到物体当前的渲染位置)即可解决画面卡顿的问题。
具体的实现
参考框架篇 里面包括了各个核心块的实现以及一些踩坑的分享。