一. 简述
我们用最精简的模型来描述一下帧同步。
客户端检测服务器的逻辑帧 -> 拿到逻辑帧 -> 进行处理 -> 处理出结果 -> 完成本次逻辑帧处理 -> 表现层将处理结果渲染到屏幕上 -> 结束
客户端检测用户操作 -> 打包成action -> 上报到服务器 -> 结束
在此基础上,客户端可以通过缓存帧,平滑帧,预测,插值,等方案优化表现层的效果及手感,但是底层模型是一样的。
比如缓存帧就是客户端检测到新的逻辑帧之后不立即处理,而是先缓存到一个队列中,等到满足一定数量之后,才对队列popFrame进行处理。
而平滑帧,则是在缓存帧的基础上,将popFrame的操作做成定时操作,从而抵抗网络波动导致逻辑帧到达的时间间隔不一致问题。
帧同步游戏一定要分为逻辑层和表现层。
表现层怎么折腾都可以,但是逻辑层必须保证确定性。
那么哪一部分属于逻辑层呢?
拿到逻辑帧 -> 进行处理 -> 处理出结果 -> 完成本次逻辑帧处理
打包成action -> 上报到服务器
所以,平缓帧的实现方案中,才能用普通的setTimeout来直接定时popFrame而没有问题。
再举个例子,比如用户操作摇杆,摇杆获取到的方向是浮点型,将浮点型直接打包成action是否ok呢?答案是不行的,因为一旦这么处理,服务器的逻辑帧就包含了浮点数。
那么如果将浮点型先转换为string再打包到action中是否OK呢,这样就是可以的。但是有个条件,就是客户端取到逻辑帧中的这个字段需要做数学运算时,需要从string直接转为定点数,一定不能出现浮点数。
二. 容易导致不一致的坑
注意:以下说的都是逻辑层。
初始化/释放物理实体的顺序
比如之前在start里面将entity加入到物理世界,而每个客户端的start函数执行顺序不确定,导致物理世界的entities的顺序不一致。从而在进行碰撞检测的时候,检测出来的结果顺序会不一致。
我们具体来验证一下start/onDestroy函数的执行顺序问题。
cocos creator对一个对象生命周期主要包含几个方面:
ctor
onLoad
start
...
onDestroy
而我们可以通过测试代码来确定他们的执行顺序:
cc.log('ins before');
// 使用给定的模板在场景中生成一个新节点
let objStar = cc.instantiate(this.starPrefab);
cc.log('addChild before');
// 将新增的节点添加到 Canvas 节点下面
this.node.addChild(objStar);
cc.log('addChild after');
打印出来的结果为:
ins before
ctor
addChild before
onLoad
addChild after
并没有直接打印start。可见,start不会在当前立刻执行,而是要等之后才执行。
也即start顺序不可控。
那么onDestroy函数呢?测试代码如下:
cc.log('destroy before');
other.custom.node.destroy();
cc.log('destroy