如何写一款实时的,多设备的对战游戏初级入门指北

什么叫多设备实时游戏

为时间, 具备以下属性的游戏称为实时游戏(伪,我定义的概念):

设有 台设备,用 , , … 标记,有:

如果在 ,在 ,则

简而言之就是在同一时间,在不同的设备运行关于时间的函数得到的结果是一样的。

多设备实时游戏很难做到吗?

难!但为什么呢?

举个简单的例子,以 javascript 为例,若在不同设备同时下页面按钮(设我的两只手是神 之手,按下的时间完全相同),让页面运行 var a = Date.now(),然后输出 alert(a) 结果是一样的吗?结果非常可能是不一样的(如果一样那么恭喜你,你已经用掉中彩票头奖的运气了)。其实这个也正是实时游戏的困难点之一:设备时钟不同

有人会说,时钟不同步,就同步时钟呗,那么尝试考虑一下场景:甲乙两人在山间的不同地方,双方之间的距离超过一公里但不知道相互间的距离,却能够互相听清楚对方的声音。那么这时候乙想要问一下甲现在究竟是几分几秒,当乙听到现在是 16 分 40 秒的时候,那一刻真的是 16 分 40 秒吗?显然不是,因为声音的传播速度是 ,那么等甲的声音传播到乙的时候,已经过了好几秒了,况且甲说话还要时间呢。设备之间的通讯也存在同样的问题:消息传播存在延时,这是多设备实时游戏的困难点之二:消息传播存在延时

如何解决实时性的问题

首先来看一个我做的 DEMO:

http://static.aliyun.jiangdl.com:10080/double-pong/index.html?side=left

打开了之后可以看见如下图的界面:

pic1

然后点 Share Game 会出现二维码,然后在设备2扫描这个二维码就会出现类似的界面,然后在点击一下,就会推出二维码界面,这时候双方按下 Start 就会开始游戏,效果如下:

pic2

这个游戏将两个设备连接起来当做一个完整的屏幕来玩 Pong 这个游戏。由于球在两个之间实时地传来传去,而且碰到屏幕的时候会随机地增加减少球的速度,也就是说游戏必须满足上述所说的 的这个特性。

细心的你会发现,及时游戏球的连贯性还可以,但是还是有一点瑕疵。实际上,在实现上我们只能使得游戏尽可能地趋向实时,并不可能真正地实现实时游戏。若 ,我们能做的只是尽可能地将这个 尽可能小,以至于趋近实时。

下面将会说一下,如何将这个 做到尽可能小。

利用估算 RTT 同步设备间时钟

越小则 就越小,那么我们就可以通过估算设备之间通信的耗时来算出时间 从设备 传送到设备 时, 收到信息时的时间来减少误差,设 为从设备 传送到 的时间,那么 就可以用 来估算,其中 为消息从 再从 的时间。

知道了 之后,那么就可以同步时钟了不是?事实上大部分时候我们不需要真正同步时钟,我们只需要调整通过 来减少 就行,下面以游戏中按下 Start / Restart 按钮开始游戏的时机要同步发生的例子为例,如果不知道 ,假设 B 收到 A 开始游戏信号的时候:都运行如下代码:

setTimout(function() {
    startGame();
}, 3000);

正如上述所说,因为有延迟,那么 A 和 B 的游戏开始将在不同的时间发生。但是有了 之后,我们就可以在 B 处运行以下代码来保证 A 和 B 同时开始游戏:

setTimout(function() {
    startGame();
}, 3000 - delay);

如何获取 RTT

回到甲乙山间同步时间的例子,乙怎么才能从甲那里知道时间呢?可以这么做:先估算出甲到乙消息传播的时间,可以这么约定:当乙喊一声“yo yo”的时候(),甲听到的时候“yo yo”这个信号的时候立即说“check it out”,当乙听到“check it out”的时候(),那么 就是甲到乙的

假设 RTT 符合正态分布,那么可以通过计算 RTT 的均值来拟合真实的 RTT,事实上 RTT 是一个动态变化的值,如何获取当前的 RTT 是决定实时游戏的关键。

减少设备通信次数

由于有延时的存在,即使 很小,但是如果通信次数 很大的时候,那么 也会到不能忽略不计。一个简单的想法就是通过补间来处理。例如在上述的游戏中,我们只需要知道球在 A 点的时候()的速度,在反弹之前()(反弹时候速度的绝对值不一定一样,是利用随机函数来产生的),就能知道球在 这段时间内球的位置。

反弹的时候再同步一次会解决这个问题,由于有延时的存在,同步期间球的位置就不能显示了,但是 DEMO 中并没有出现这种情况,在有随机数参与过程的情况下也可以保证状态的一致性,方法就是利用随机数种子来解决这个问题,事实上这种方案在很多游戏中都会采用,例如 Minecraft 的地图生成,PSP 的怪物猎人里面物品的刷新和怪物行为的发生等等,由于随机数种子一般是通过时间来产生的,所以你会看到很多游戏的作弊方法都是通过锁时间来完成。

在这个 DEMO 里面也可以通过类似的方法来保证动作的连贯性:通过同步随机数种子来保证球的轨迹的一致性。Javascript 中原声并没有提供通过种子来产生随机数的方法,可以参考下面这个项目来产生随机数:

https://github.com/davidbau/seedrandom

通过这个手段,到此为止,我们只需要同步游戏的种子、游戏开始的时间、游戏结束的时间就行了,通信的任务就这么多,极大地简化了游戏对战对网络的依赖(不考虑防止作弊)。

做游戏重要的事情

做游戏之前,一定要先想清楚游戏的灵魂是什么,在代码层面体现就是游戏的逻辑,游戏的逻辑是和展示、通讯毫无相关的一些东西。例如象棋这个游戏,在手机上能玩,在桌子上利用实体象棋也能玩,甚至如果双方记性特别好的话,通过对话就能玩。这是因为游戏的进行不依赖于展现和通信手段。

所以__逻辑__是做游戏最最重要的事情之一(几乎没有之一)。在做任何游戏之前,用最简洁的状态来表示你的游戏,是成功的关键。

更简单的对战游戏开发体验(网页端)

在这个游戏中,并没有提到通信是怎么实现的,是因为我用了 RoomJS 这个编程框架(本人拙作),这是一个服务器对使用端完全透明的,使用方法十分简单(本文不是软文啊,别误会)

在网页调用类似于 broadcast(messageName, message) 的方法就可以广播消息。在未来还会加入自动获取 RTT 的功能。

总结

事实对战游戏,如果动作的操作频率小于 的情况下是完全可以做到的,如果操作频率大于 的情况,可以通过改变游戏的规则和考虑用户体验的情况下来在工程上实现。

Back