帧同步在小游戏实践中的那些坑(一)数据篇

背景:

帧同步技术很早就有了,从过去PC时代的war3、星际,到手游时代的王者荣耀、皇室战争。那么如何将其广泛运用于H5游戏开发呢?我们做了个JS版的帧同步SDK,有几十款游戏接入上线。开发者不用关心联网细节,就能高效地开发出表现精准、打击感强、服务器压力小、支持回放复盘的多人联机小游戏。
帧同步原理

帧同步的实现方式有很多种,这里采用乐观帧的方式。原理简单来说就是:

  • 服务器收集各客户端的操作指令,将其组装入数据帧。
  • 服务器以固定的时间间隔,不断下发数据帧给各客户端。
  • 客户端依序接收数据帧1,执行逻辑1,运行至状态1;接收数据帧2,执行逻辑2,运行至状态2…循环往复。
  • 只要各客户端接收相同的数据帧,执行相同的逻辑,就能运行至相同的状态。这就形成了同步。

下面就简单聊一聊我们使用帧同步做联机H5小游戏,实践中所遇到的那些坑。阐述内容主要基于TypeScript语言,Cocos Creator引擎。下文将从数据一致性、体验优化、性能优化这三个部分展开。

一,数据一致性

通过以上原理可以看出,各客户端要保证同步,需要保证执行相同的逻辑,能得到相同的结果。而影响结果的第一要素就是数据不一致。

1.浮点数

JS中经典的问题:0.1 + 0.2 === 0.3 ? 答案是false。0.1 + 0.2 = 0.30000000000000004。

JS中使用IEEE 754双精度64位标准来存储Number。小数计算的时候容易产生精度丢失。不同机器的架构、硬件、编译模式不同,对于精度的处理也有可能不同,于是有可能产生不一致。
那么如何处理这些浮点数?

(1)四则运算

这里有两种常用的做法:

  • 乘以一个固定的整数(比如1000),或者左移n位。目的是转换成整数运算。好处是简单、高效、直观。(但要注意:JS的移位不能超过32位,比如1<<32 === 1。)
  • 使用第三方JS定点数库(比如big.js),原理是通过字符串来模拟运算。好处是包含的API较多,但运行效率也下降的很厉害。

(2)三角函数

三角函数运算,有多项式拟合法和查表法两种。但更推荐查表法,因为效率比较高。

以sin A为例,编译阶段提前建立好[0°、1°, … , 90°] -> [sin 0°, sin 1°, … , sin 90°]的映射。运行时计算目标角度坐落的角度区间,查表获得映射结果。其他角度、cos的问题,也可以转换为sin 0-90°的问题。

(3)开方

对于开方,有两种算法比较好用:

  • 二分查找法。给定x,在[0, x]区间内不断二分查找num,判断num*num与x,找到最接近的数。算法实现简单。
  • 牛顿迭代法。基于多项式P(x) = a*(xn)+b*(x(n-1))+c*(x^(n-2))表示法,核心思想是使用 F(n+1) = (x/F(n) + F(n))/2的方式去迭代逼近。推导过程这里就不展开了。此算法收敛速度比二分法更快。

2.随机数

随机性是增强游戏趣味性的常用手段,那么如何保证各客户端调用n次随机函数,随机结果都相同呢?这就需要为JS封装一个带随机种子的伪随机函数。最常用的随机种子生成方式为线性同余法。公式为S(n+1) = a * S(n) + c%m。
其参数需要遵循Hull-Dobell定理。即:

  • c与m互质;
  • a-1可以被m的所有质因数整除;
  • 如果m是4的倍数,a - 1也必须是4的倍数。

例如可以设置:seed = 9301 * seed + 49297 % 233280。

3.碰撞检测

游戏中不可避免的要使用碰撞检测,引擎自带的碰撞检测,受到各种机型差异的影响,结果很容易产生不同。因此我们也要基于数据帧改写碰撞检测。(以二维为例,三维同理)

(1)矩形与矩形

假设矩形1的中心为(x1, y1), 宽w1, 高h1;矩形2的中心为(x2, y2),宽w2,高h2。则碰撞检测公式为: |x1-x2|<|(w1+w2)/2| && |y1-y2|<|(h1+h2)/2|

(2)圆与圆

假设圆1的中心为(x1, y1),半径r1;圆2的中心为(x2, y2),半径为r2。则碰撞公式为:(x1-x2)^2 + (y1-y2)^2 < (r1+r2)^2

(3)矩形与圆

假设存在矩形A:(x1,y1,w1,h1),圆B:(x2,y2,r2)。分以下三步:
矩形与圆的碰撞

  • 以矩形中心A(x1, y1)为原点建立坐标系;将圆B移位到该坐标系的第一象限。那么新的圆心为B’(|x2-x1|,|y2-y1|, r2)。
  • 找出矩形上距离圆心最近的点。P (min(|x2-x1|, w1/2), min(|y2-y1|, h1/2))。
  • 利用上面的圆与圆相交法,计算点P与圆B’是否相交即可。

4.执行顺序

  • 执行顺序也有可能产生不一致,需要特别警惕:
  • 逻辑数据不能通过引擎自带的update来更新,需要让逻辑与渲染分离。
  • Object不能使用不确定性的遍历方式。逻辑中不能有self的特殊处理。
  • 随机函数的使用需要区分哪些要一致,哪些不能一致。

链接:

帧同步在小游戏实践中的那些坑(一)数据篇:
https://blog.csdn.net/weixin_42109916/article/details/109307299
帧同步在小游戏实践中的那些坑(二)体验篇:
http://www.javashuo.com/article/p-cazzyzck-vh.html
帧同步在小游戏实践中的那些坑(三)性能篇:
http://www.javashuo.com/article/p-mtcdirak-vg.html