帧同步学习记录

帧同步

参考链接

网易雷火



两篇学会帧同步

一. 简介

帧同步和状态同步是目前最常用的游戏同步设计。它们并不互斥,可以一起相辅相成的出现于同步逻辑中。

下图来自 Unity游戏开发 帧同步战斗框架 理论篇
在这里插入图片描述

二. 客户端逻辑

帧同步的逻辑都在客户端,所以首要保证的是不同客户端同一帧内的计算结果一定要相同。

  • 可控的客户端的逻辑
  • 逻辑显示分离
  • 可控的随机

1).客户端帧同步逻辑

unity游戏开发 帧同步战斗框架 框架篇

帧同步的逻辑全都在客户端计算,必须保证每个客户端相同帧的计算结果是一样的。

要完全控制客户端的计算流程,比如,移动,碰撞,动画事件,延迟处理(等待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.浮点数

浮点数带来的误差,比如计算概率属性(暴击伤害,百分比治疗等等),碰撞(小数带来的碰撞误差),以及循环增减时的低位浮点数累计问题等。

基本做法:

  1. 实现一套安全的浮点数计算方法
  2. 使用整数,百分比计算都乘以一个倍数(10,100、1000等),然后舍去小数部分

混合使用即可。目前只用了第二种,计算结果(属性变化,位置移动等)向下取整( math.floor() )。

3.map和array

同样,常用的字典类型也基本不能使用,应该都用队列这种固定顺序的数据结构来存储。

但是字典类型方便查找,都用队列肯定会降低查找效率以及代码的工整性。

解决的方法就是实现一个数据结构,内部同时使用 字典和队列 来维护当前数据,实现这组数据的增删改查等逻辑即可。

4).预测/回滚

预测、回滚、快照是一起出现的,因为帧同步的逻辑延迟和网络固定的延迟,以及数据的拆解和gc等等各种因素。

实际同步肯定会有些许的卡顿。常用的方法就是客户段对当前的逻辑进行预测(1帧),先进行当前帧的模拟。并且存储当前游戏的快照。

如果服务器发送下来的操作导致预测结果出错,则快速回滚到前一帧,并执行正确的逻辑,回到当前帧再进行下一帧的预测。

暂时还没做到这块,对快照存储还有些疑问。具体概念参考如下文章:

2 天做了个多人实时对战,200ms 延迟竟然也能丝滑流畅?

三.网络处理

1).TCP/UDP

TCP是一般使用的法案。但想要快肯定用UDP,目前安全的UDP库也很多,可以直接使用。

四.调试工具