Post

一文搞懂帧同步服务端架构

lockstep 帧同步 / 步进锁定 经典架构方案

lockstep,译步进锁定。这是云风大佬给出的翻译。当然也可以简单翻译为帧同步。为什么译为步进锁定呢?这里谈下多人游戏中画面同步算法的思想,所谓画面同步,就是让同一局游戏中的所有电脑同时执行一样的操作,得到一样的结果,也就得到同样的画面。

我们都知道,网络传输是需要时间的,也就是存在延迟的。但是,我们要理解一点,懂动画的同学应该知道,我们电脑的画面渲染是具有帧率的,也就是画面是一帧一帧的渲染的,如果可以再下一帧渲染之前,所有的客户端都知道如何渲染这一帧能够实现一致的画面,这样画面同步也就能够实现了。我们可以把帧与帧之间的时间想象成一个回合,在这个回合里,不同的客户端会下达指令,服务端将这些指令转发给所有的客户端。在回合结束的时候,对回合内收到的所有的指令进行运算,渲染画面成下一帧。所以玩家对交互界面进行游戏操作的时候,游戏界面并不会马上发生改变,而是将指令放入回合中,等待回合结束后再运算渲染画面。

有同学可能会提问,如果在这个回合中,存在客户端没有发送请求,怎么办。常见的方案是,给回合规定一个截止时间,这个时间不一定是回合结束的时间,可以比回合结束的时间要早,因为要考虑网络延迟,要保证当前回合结束时,此回合的所有玩家受到的指令都是一样的。

假设游戏的画面渲染帧率是10hz,也就是1秒内存在100个回合。服务端在一个回合里,必须要收到所有的客户端的指令,如果服务端没有收到某个玩家的指令,客户端表现上会被卡住。

那么帧同步是如何做到当前帧渲染的时候,客户端保证能够收到所有其他客户端的逻辑指令。在步进锁帧算法中,玩家在客户端的操作会被延迟确认。假设客户端所有操作都设置有延迟时长(半个回合周期以内),每当收到玩家的操作指令,会在指令队列中找到延迟时间最短的那个时间,设置超时时长。超时后,把指令队列作为下一个回合的操作指令打包发出。

指令队列中默认有半个回合周期长度的idle操作,作为无动作指令。大部分指令都是0延迟的,也就是当上个回合被确认后(结束后),会立刻被打包发出,在回合周期内(100ms)内所有客户端在网络状况良好的情况下,就能够收到所有客户端的同步信息。

看到这里,很多同学可能会觉得,诶那不是没有服务端什么是了吗,信息在客户端之间同步不就行了。其实并不是的,基于客户端之间通行实现的一致性计算,仅仅是因为游戏发展早期,没有中心服务器,且游戏环境大部分为局域网。

但是在现在,互联网环境下,大部分帧同步游戏有权威服务器作为广播服务器。

lockstep 优化

考虑下断线重连的情况。假设这样的场景,客户端在处理完帧号a的帧后,在等待帧a+1的时候掉线。然后客户端在服务端的当前帧为b时重连进服务器。这个时候客户端如何追回画面呢?

这里直接讨论优化后的方案,带快照的追帧。服务端会收到客户端的快照,这个快照包含客户端渲染整个页面所需的所有信息。快照有个帧id字段表明其属于那一帧的快照。服务端接受到快照后,把这个快照所属的id之前的所有帧缓存都删除。

客户端连回后,服务端会把快照和缓存里所有的帧都发给客户端,让客户端进行追帧操作。

帧同步算法核心:广播追帧并行算法。顾名思义就是将追帧的过程融入到广播的过程中。首先我们需要设计帧结构,代码如下:

1
2
3
4
5
6
7
8
9
type LockStep struct {
	running    bool             	// 是否正在广播
	frameRate  int32                // 帧率
	currentFrame int64              // 当前帧
	frameCache  map[int64][]Frame 	// 帧缓存
	currentSnap Snap 	// 当前快照
	userFrame map[string]int64 		// 用户当前帧
	timerId uint64 					// 定时器任务ID
}

结构体字段的修改时机,这里就不做过多的讲解,这里主要讲下LockStep如何同步和追帧的。

LockStep中一般需要有当前帧号,会随着固定帧的到来 / 定时器的触发而自增。他的作用是判断广播需要广播多少帧数据给客户端。定时器开始后,当定时器触发时,先自增当前帧,会检查所有客户端的当前帧,一般会落后当前帧1帧。广播缓存的当前帧号的帧信息。但是如果出现客户端的帧的帧号比当前帧落后N帧时,就要广播到当前帧的所有帧信息。甚至客户端的当前帧号比缓存的快照的帧号都要落后时,服务端需要先传快照,然后把所有缓存的帧信息发送给客户端。这是就是追帧。客户端收到这些帧信息后,会进行一个加速的追帧过程。

具体算法的实现就留给读者下去自己实践实践。

This post is licensed under CC BY 4.0 by the author.