帧同步学习记录
帧同步
参考链接
网易雷火
- cocos2dx lua socket 使用
- cocos creator 项目总结二(战斗帧同步解析)
- 2 天做了个多人实时对战,200ms 延迟竟然也能丝滑流畅?
不是标题党,讲的非常完善清晰,而且还有状态同步的例子
两篇学会帧同步
-
Unity 游戏开发总结
大佬的专栏,还有很多Unity的知识
一. 简介
帧同步和状态同步是目前最常用的游戏同步设计。它们并不互斥,可以一起相辅相成的出现于同步逻辑中。
二. 客户端逻辑
帧同步的逻辑都在客户端,所以首要保证的是不同客户端同一帧内的计算结果一定要相同。
- 可控的客户端的逻辑
- 逻辑显示分离
- 可控的随机
1).客户端帧同步逻辑
帧同步的逻辑全都在客户端计算,必须保证每个客户端相同帧的计算结果是一样的。
要完全控制客户端的计算流程,比如,移动,碰撞,动画事件,延迟处理(等待s秒)等。
渲染因为跟不同硬件设备以及引擎有着相对较强的关联,所以客户端会设计成逻辑与显示分离。
渲染部分可以交给引擎提供的更新,而逻辑更新必须由客户端实现的Update控制。
1.主要逻辑
Update(delta){
事件帧
逻辑帧
}
UpdateRender(delta){
渲染帧
}
- 事件帧:帧同步的帧,包含某段时间内所有玩家的操作
- 逻辑帧:游戏的所有逻辑
- 渲染帧:显示部分的更新,比如坐标
1个事件帧 = n个逻辑帧 = n*m 个渲染帧
2.统一时间间隔
事件帧和逻辑帧是一起更新的,逻辑帧间隔小于等于事件帧。使用更小的间隔来判断 deltaTime。
要注意的是,Update 传入的间隔时间不是固定的,是变化的。比如 前台 -> 后台,后台 -> 前台。这时候传入的时间就会很大。
#define 时间帧更新间隔
#define 逻辑帧更新间隔
Update(delta){
delta 计算
if(delta < 逻辑帧时间间隔)return;
间隔x次逻辑帧 = math.min(delta/逻辑帧更新间隔, 剩余事件帧数量)
for(ini i = 0;i < 间隔x次逻辑帧;i ++){
//事件帧
Event(逻辑帧更新间隔)
//逻辑帧
Logic(逻辑帧更新间隔)
}
}
Event(deltaTime){
更新次数 + 1
if(更新次数 < n) return
if(是否有事件帧) {
逻辑帧更新次数 = 0
return;
}
//分发
更新次数 = 0
逻辑帧更新次数 = n
}
Logic(deltaTime){
if(逻辑帧更新次数 <= 0) return;
逻辑帧更新次数 - 1
//进行逻辑帧更新
,,,
}
传入的间隔时间经过处理,每次更新间隔就是逻辑帧间隔时间。这样确保了即使是不同客户端,不同间隔时间,每帧的更新也一定是同样的间隔时间。
3.断线重连和回放
回放:帧同步天然支持回放,把整局游戏按帧回放即可。
断线重连:想想回放的逻辑,追帧加速即可。
2).逻辑显示分离
1.保证不同客户端结果相同
比如,动画系统。
游戏中的行为,交互都是跟动作有关的。
比如攻击动画在 x 秒的动作打开碰撞,x秒的动作关闭碰撞。在x秒的时候生成一道剑气等等。如果你的动画系统不在你的控制之中,那么有可能在不同的设备上,不正确的时间点进行对应的行为。
目前的做法是用一组技能点队列来控制动画,而不是动画的某一帧来触发事件。每次更新的时候进行技能点的检测,以及生成对应行为。
2.平滑卡顿
帧同步只会同步操作,而且一般来说手机端都是用 20-30 网络帧同步来制作的。
30帧基本就是人眼卡顿的极限了。显示和逻辑分离则仍可以使用30帧以上的渲染更新,以及使用插值的来平滑卡顿。
3.作弊检测
每隔x帧,各个客户端向服务器端发送检验数据,如果都一样则通过,数据异常则可能是bug或作弊。
3).可控的随机
帧同步的逻辑都是客户端在计算的,所以得保证每个客户端计算的结果要一致。那么对于一些不确定的逻辑就要给予确定性。
1.随机数
可以参考如下链接的做法:
目前项目是很简单粗暴的做法。随机生成了一个 x 长度的随机队列,每次随机数按顺序从里面取,取到队尾再回头取。
如果随机数队列有变化,那之前的回放就不可查看了。
2.浮点数
浮点数带来的误差,比如计算概率属性(暴击伤害,百分比治疗等等),碰撞(小数带来的碰撞误差),以及循环增减时的低位浮点数累计问题等。
基本做法:
- 实现一套安全的浮点数计算方法
- 使用整数,百分比计算都乘以一个倍数(10,100、1000等),然后舍去小数部分
混合使用即可。目前只用了第二种,计算结果(属性变化,位置移动等)向下取整( math.floor() )。
3.map和array
同样,常用的字典类型也基本不能使用,应该都用队列这种固定顺序的数据结构来存储。
但是字典类型方便查找,都用队列肯定会降低查找效率以及代码的工整性。
解决的方法就是实现一个数据结构,内部同时使用 字典和队列 来维护当前数据,实现这组数据的增删改查等逻辑即可。
4).预测/回滚
预测、回滚、快照是一起出现的,因为帧同步的逻辑延迟和网络固定的延迟,以及数据的拆解和gc等等各种因素。
实际同步肯定会有些许的卡顿。常用的方法就是客户段对当前的逻辑进行预测(1帧),先进行当前帧的模拟。并且存储当前游戏的快照。
如果服务器发送下来的操作导致预测结果出错,则快速回滚到前一帧,并执行正确的逻辑,回到当前帧再进行下一帧的预测。
暂时还没做到这块,对快照存储还有些疑问。具体概念参考如下文章:
2 天做了个多人实时对战,200ms 延迟竟然也能丝滑流畅?
三.网络处理
1).TCP/UDP
TCP是一般使用的法案。但想要快肯定用UDP,目前安全的UDP库也很多,可以直接使用。