【游戏】网络游戏开发中的帧同步

两种同步模式

  • 帧同步:顾名思义,多个客户端的数据以帧的形式安严格的顺序进行同步。
  • 状态同步:每个运动物体在各个端维护当前的状态和运动的向量数据,以事件驱动的机制,同步当前物体最新的状态数据和运动向量。(状态数据指类似位置,血量等非行为数据,目的是用于状态纠错)
帧同步 状态同步
游戏类型 MOBA/RTS… RPG/FPS…
C/S 复杂度 客户端实现复杂 服务端实现复杂
防作弊难度 客户端存在全图的数据,难以防止全图挂,视野挂等 状态和事件由服务端控制下发,容易校验玩家的状态
风险 容易出现一致性问题,随机数。浮点数的实现都得考虑跨平台。 服务端需要跑1:1的游戏逻辑,服务器的资源要求高, 同时在线多的游戏还得考虑状态同步量大导致的网络瓶颈

帧同步

帧同步的是英文LockStepSync的翻译,严格的讲应该叫步调一致(协调)的同步,其中主要有两种实现针对不同的场景,局域网模式,也就是P2P网络的游戏采用的是严格的锁定帧同步,比如DOTA。还有一种是有中心服务器的,C(client)S(Server)模式的游戏,比如LOL,王者荣誉等,采用的则是非锁定的帧同步。

  • 锁定帧同步:客户端的每一帧的推进,都需要得到同一局的所有玩家确认,所以一个玩家掉线游戏就会暂停。但是因为是局域网所以这种情况比较少,是可以接受的。
  • 非锁定帧同步:每个客户端和服务端以一定的帧率进行帧数据的同步,服务端只管当帧时间抵达时,将数据整理成一个帧数据包广播给所有的客户端。客户端收到帧数据包后对帧号进行比对,判断是否需要进行追帧,和执行逻辑。这种模式下,客户端的网络只会影响自己的游戏体验,其它网络正常的玩家是可以正常游戏的。

P2P网络做非锁定帧同步是不太合适的,因为如果作为主机的客户端,离线或者出现数据丢失,其它客户端是没办法恢复一致的状态的。而有中心服务器的模式,游戏的帧数据会先保存起来再广播出去,即便客户端重启,也是可以获取到帧数据队列进行追帧重放。

帧同步的原理

帧同步的实现,可以将整个游戏理解为一个状态机。 游戏开始时下发的玩家数据,游戏中每个角色,玩家信息等状态是一致的。我们将这个起始状态下所有的游戏状态集合当做S0,序列帧为F0,当游戏进行到序列帧F100,状态集合为S100,期间,客户端安严格帧顺序的控制指令集合(状态转移指令),每个客户端从F0到F100期间,应用这些状态转移指令,同样的执行指令,同样的执行逻辑,最终的状态也肯定是一致。

核心流程

lockstep

网络部分的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
message Command1{
int64 uid;
...
}

message Command2{
int64 uid;
...
}

message Command3{
int64 uid;
...
}

message Frame{
int64 frameId;
repeated Command1 command1;
repeated Command2 command2;
repeated Command3 command3;
....
}

class GameRoom{


Queue<Object> commandQueue = new ConcurrentLinkedList();
/**300ms 一帧关键帧**/
public finnal static int TICK_INTERVAL = 300;
/**任务调度器**/
TaskScheduler taskScheduler = ...;
/**不会并发执行,只需要保证可见性**/
public volatile int frameId = 0;

public void onGameStart(){
long now = System.currentTimeMillis()
// 更新 游戏状态等.....
pushGameStart();
// 主要的帧推送代码
taskScheduler.scheduler(()->nextTick(), Instant.now().plusMillis(TICK_INTERVAL))
}

public void nextTick(){
long start = System.currentTimeMillis()
frameId ++;
while(!commandQueue.isEmpty()){
Frame frame = ....
...
push2RoomUser(frame);
}
// 构造结构体和推送消息的耗时
long timeUse = System.currentTimeMillis() - start;

// 计算下一帧的剩余时间
long nextTickTimeLeft = TICK_INTERVAL - timeUse;
// 加入调度队列
taskScheduler.scheduler(()->nextTick(), Instant.now().plusMillis(nextTickTimeLeft < 0 ? 0 : nextTickTimeLeft))
}
}

public void onReciveUserCommand1(Command1 command1){
//预处理逻辑
commandQueue.offer(command1)

}
...

}

// 其它处理如断线重连,帧数据落地等......
...

防作弊

帧同步的实现方式,大部分的逻辑可以在客户端实现,轻量级的游戏,完全可以采用通用的帧同步服务,微信小游戏开发平台就提供了这样的一个通用的帧同步服务。由于大部分的逻辑在客户端实现,这就给防作弊带来了一定的难度。对于帧同步常规的防作弊主要侧重两方面,一是让作弊玩家只能自嗨,二是关键的状态特别是结算相关的玩家状态进行必须校验或者做云端处理。

  1. 状态数据抽样:服务端隔一段时间收集每个客户端指定帧的帧数据,进行对比。少数服从多数,出现异常的客户端要求重连。
  2. 逻辑服:将客户端的逻辑实现抽象出来,服务端跑一个逻辑服务器,跟游戏进行同步运行或者结算的时候运行。关键状态数据以逻辑服上报的结果为主,如果逻辑出现异常,再以1上报的结果作为兜底的策略。