网络游戏的移动同步(四)帧锁定算法

引言

我一直想了解早期游戏的网络同步,特别是早期的很多对战平台,做了很多单机游戏的对战,而这些同步又可以达到很不可思议的效果。从网络上我得知这种方案叫帧锁定,即保证每帧都是一致的。否则就锁定。比如玩dota时出现的万恶的等待框,本篇文章就希望简单的从客户端代码层面模拟这个过程(实际上到最后,我觉得纯粹用客户端模拟还是不足),另外作为整个网络同步系列的最后一个章节。

基本原理

对于单机游戏或者可联网的局域网游戏来说,大部分是没有服务器的,所有的游戏逻辑都放在客户端中处理,这导致的,只要每个用户所给的输入一致,时间一致,就能达到同样的模拟效果,所以帧锁定的基本原理便是如此。

算法流程

1.客户端定时(比如每五帧)上传控制信息。

2.服务器收到所有控制信息后广播给所有客户。

3.客户端用服务器发来的更新消息中的控制信息进行游戏。

4.如果客户端进行到下一个关键帧(5帧后)时没有收到服务器的更新消息则等待。

5.如果客户端进行到下一个关键帧时已经接收到了服务器的更新消息,则将上面的数据用于游戏,并采集当前鼠标键盘输入发送给服务器,同时继续进行下去。

6.服务端采集到所有数据后再次发送下一个关键帧更新消息。

C/S逻辑

客户端逻辑:

1. 判断当前帧F是否关键帧K1:如果不是跳转(7)。

2. 如果是关键帧,则察看有没有K1的UPDATE数据,如果没有的话重复2等待。

3. 采集当前K1的输入作为CTRL数据与K1编号一起发送给服务器

4. 从UPDATE K1中得到下一个关键帧的号码K2以及到下一个关键帧之间的输入数据I。

5. 从这个关键帧到下 一个关键帧K2之间的虚拟输入都用I。

6. 令K1 = K2。

7. 执行该帧逻辑

8. 跳转(1)

服务端逻辑:

1. 收集所有客户端本关键帧K1的CTRL数据(Ctrl-K)等待知道收集完成所有的CTRL-K。

2. 根据所有CTRL-K,计算下一个关键帧K2的Update,计算再下一个关键帧的编号K3。

3. 将Update发送给所有客户端

4. 令K1=K2

5. 跳转(1)

客户端模拟代码

// 如果当前是关键帧
if (Global.frame == keyFrame) {
    // 查看是否有服务器的更新包,当前关键帧编号,下一关键帧编号,所有玩家的控制信息
    // 获取更新包
    var rp:PlayerInputPack;
    while (client.packets.length > 0) {
        rp = client.packets.shift();
        if (rp.frame == keyFrame) {
            break;
        }
    }
    // 如果等不到当前帧的控制数据,则返回
    if (rp && rp.frame == keyFrame) {
        var nextFrame:int = keyFrame + 5;
        // 采集当前的输入作为包发送
        var p:PlayerInputPack = new PlayerInputPack();
        p.frame = nextFrame;
        p.input = InputManager.instance.keyStatus;
        client.send(p);

        // 以rp.input做输入数据
        // 模拟移动本地及网络客户端
        // 每个客户端的逻辑一致
        var players:Array = [localPlayer, netPlayer];
        for each (var player:Player in players) {
            player.velocity.x = 0;
            player.velocity.y = 0;

            if (rp.input[Keyboard.LEFT]) {
                player.velocity.x = -player.speed;
            }
            if (rp.input[Keyboard.RIGHT]) {
                player.velocity.x = player.speed;
            }
            if (rp.input[Keyboard.UP]) {
                player.velocity.y = -player.speed;
            }
            if (rp.input[Keyboard.DOWN]) {
                player.velocity.y = player.speed;
            }
        }

        // 下一个关键帧
        keyFrame = nextFrame;
        waitTime = 0;
    } else {
        waitTime += Global.elapse;

        // 等待太久了,类似魔兽争霸的那个超时面板
        if (waitTime > 1000) {
            trace('等待不到控制包信息', keyFrame);
        }
    }
} else {
    // 当前帧步进
    Global.frame++;
}

// 更新场景
localScene.update();
netScene.update();

演示

Get Adobe Flash player

总结

可以从flash演示中看到,两个场景在低延迟的情况下同步效果非常理想,甚至远远超过之前的客户端预测例子,不过这种做法的缺点就是当延迟过大时会极大的影响控制手感,另外延迟受最慢的客户端影响。

当服务器迟迟没有收到每五帧的所有控制输入时,就会发送通知给所有客户端,出现dota或者war3联网时的那个网络延迟框。在war3中,因为没有专门服务器,所以这个责任是由主机承担的。

对于游戏中的随机元素,则可以在游戏初始化时,由主机同步随机种子到所有客户端中。

参考文献

网络游戏的移动同步(四)帧锁定算法》上有2条评论

  1. yaosir

    看到您发的关于游戏同步的博文,十分感兴趣,不知道你是否可以把您上边用于演示的小程序的源代码分享与我,十分感激,我希望能在自己的电脑运行,并仔细分析!

    回复
  2. yksalun

    if (Global.frame == keyFrame)
    {…}
    只有关键帧会去执行后端发来的数据,那么这个关键帧之间的间隔时间是否会有延迟。
    比如在keyframe1执行的时候我发送了行走指令。那么要到keyframe2的时候才收到。这样之间的间隔时间就是指令响应的延迟时间了吧?

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注