设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

Node.js打造实时多人游戏框架

2014-10-29 11:02| 发布者: joejoe0332| 查看: 2664| 评论: 0|原作者: CSDN|来自: CSDN

摘要: Node.js的大红大紫也造就了一大批新应用、新工具的诞生。比如基于Node.js的开发框架、开源软件等等。本文转自阿里巴巴用户体验部有一点博客,作者详细描述了使用Node.js、Node-Webkitk开发的实时多人游戏框架Spacero ...

  【编者按】Node.js的大红大紫也造就了一大批新应用、新工具的诞生。比如基于Node.js的开发框架、开源软件等等。本文转自阿里巴巴用户体验部有一点博客,作者详细描述了使用Node.js、Node-Webkitk开发的实时多人游戏框架Spaceroom过程。


  在 Node.js 如火如荼发展的今天,我们已经可以用它来做各种各样的事情。前段时间UP主参加了极客松活动,在这次活动中我们意在做出一款让“低头族”能够更多交流的游戏,核心功能便是 Lan Party 概念的实时多人互动。极客松比赛只有短得可怜的36个小时,要求一切都敏捷迅速。在这样的前提下初期的准备显得有些“水到渠成”。跨平台应用的 solution 我们选择了node-webkit,它足够简单且符合我们的要求。

  按照需求,我们的开发可以按照模块分开进行。本文具体讲述了开发 Spaceroom(我们的实时多人游戏框架)的过程,包括一系列的探索与尝试,以及对 Node.js、WebKit 平台本身的一些限制的解决,和解决方案的提出。


Getting started

Spaceroom 一瞥

在最开始,Spaceroom 的设计肯定是需求驱动的。我们希望这个框架可以提供以下的基础功能:

  • 能够以 房间(或者说频道) 为单位,区分一组用户
  • 能够接收收集组内用户发来的指令
  • 在各个客户端之间对时,能够按照规定的 interval 精确广播游戏数据
  • 能够尽量消除由网络延迟带来的影响

当然,在 coding 的后期,我们为 Spaceroom 提供了更多的功能,包括暂停游戏、在各个客户端之间生成一致的随机数等(当然根据需求这些都可以在游戏逻辑框架里自己实现,并非一定需要用到 Spaceroom 这个更多在通信层面上工作的框架)。


APIs

Spaceroom 分为前后端两个部分。服务器端所需要做的工作包括维护房间列表,提供创建房间、加入房间的功能。我们的客户端 APIs 看起来像这样:

  • spaceroom.connect(address, callback) – 连接服务器
  • spaceroom.createRoom(callback) – 创建一个房间
  • spaceroom.joinRoom(roomId) – 加入一个房间
  • spaceroom.on(event, callback) – 监听事件
  • ……

客户端连接到服务器后,会收到各种各样的事件。例如一个在一间房间中的用户,可能收到新玩家加入的事件,或者游戏开始的事件。我们给客户端赋予了“生命周期”,他在任何时候都会处于以下状态的一种:

你可以通过 spaceroom.state 获取客户端的当前状态。

使用服务器端的框架相对来说简单很多,如果使用默认的配置文件,那么直接运行服务器端框架就可以了。我们有一个基本的需求:服务器代码 可以直接运行在客户端中,而不需要一个单独的服务器。玩过 PS 或者 PSP 的玩家应该清楚我在说什么。当然,可以跑在专门的服务器里,自然也是极好的。

逻辑代码的实现这里简略了。初代的 Spaceroom 完成了一个 Socket 服务器的功能,它维护房间列表,包括房间的状态,以及每一个房间对应的游戏时通信(指令收集,bucket 广播等)。具体实现可以参看源码。


同步算法

那么,要怎么才能使得各个客户端之间显示的东西都是实时一致的呢?

这个东西听起来很有意思。仔细想想,我们需要服务器帮我们传递什么东西?自然就会想到是什么可能造成各个客户端之间逻辑的不一致:用户指令。既然处理游戏逻辑的代码都是相同的,那么给定同样的条件,代码的运行结果也是相同的。唯一不同的就是在游戏过程当中,接收到的各种玩家指令。理所当然的,我们需要一种方式来同步这些指令。如果所有的客户端都能拿到同样的指令,那么所有的客户端从理论上讲就能有一样的运行结果了。

网络游戏的同步算法千奇百怪,适用的场景也各不相同。Spaceroom 采用的同步算法类似于帧锁定的概念。我们把时间轴分成了一个一个的区间,每一个区间称为一个 bucket。Bucket 是用来装载指令的,由服务器端维护。在每一个 bucket 时间段的末尾,服务器把 bucket 广播给所有客户端,客户端拿到 bucket 之后从中取出指令,验证之后执行。

为了降低网络延迟造成的影响,服务器接到的来自客户端的指令每一个都会按照一定的算法投递到对应的 bucket 中,具体按照以下步骤:

  1. 设 order_start 为指令携带的指令发生时间, t 为 order_start 所在 bucket 的起始时间
  2. 如果 t + delay_time <= 当前正在收集指令的 bucket 的起始时间,将指令投递到 当前正在收集指令的 bucket 中,否则继续 step 3
  3. 将指令投递到 t + delay_time 对应的 bucket 中

其中 delay_time 为约定的服务器延迟时间,可以取为客户端之间的平均延迟,Spaceroom 里默认取值80,以及 bucket 长度默认取值48. 在每个 bucket 时间段的末尾,服务器将此 bucket 广播给所有客户端,并开始接收下一个 bucket 的指令。客户端根据收到的 bucket 间隔,在逻辑中自动进行对时,将时间误差控制在一个可以接受的范围内。

这个意思是,正常情况下,客户端每隔 48ms 会收到从服务器端发来的一个 bucket,当到达需要处理这个 bucket 的时间时,客户端会进行相应处理。假设客户端 FPS=60,每隔 3帧 左右的时间,会收到一次 bucket,根据这个 bucket 来更新逻辑。如果因为网络波动,超出时间后还没有收到 bucket,客户端暂停游戏逻辑并等待。在一个 bucket 之内的时间,逻辑的更新可以使用 lerp 的方法。


在 delay_time = 80, bucket_size = 48 的情况下,任一指令最少会被延迟 96ms 执行。更改这两个参数,例如在 delay_time = 60, bucket_size = 32 的情况下,任一指令最少会被延迟 64ms 执行。


计时器引发的血案

整个看下来,我们的框架在运行的时候需要有一个精确的计时器。在固定的 interval 下执行 bucket 的广播。理所当然地,我们首先想到了使用setInterval(),然而下一秒我们就意识到这个想法有多么的不靠谱:调皮的setInterval() 似乎有非常严重的误差。而且要命的是,每一次的误差都会累计起来,造成越来越严重的后果。

于是我们马上又想到了使用 setTimeout(),通过动态地修正下一次到时的时间来让我们的逻辑大致稳定在规定的 interval 左右。例如此次setTimeout()比预期少了5ms, 那么我们下一次就让他提前5ms. 不过测试结果不尽人意,而且这怎么看都不够优雅。

所以我们又要换一个思路。是否可以让 setTimeout() 尽可能快地到期,然后我们检查当前的时间是否到达目标时间。例如在我们的循环中,使用setTimeout(callback, 1) 来不停地检查时间,这看起来像是一个不错的主意。


令人失望的计时器

我们立即写了一段代码来测试我们的想法,结果令人失望。在目前最新的node.js 稳定版(v0.10.32)以及 Windows 平台下,运行这样一段代码:
  1. var sum = 0, count = 0;  
  2. function test() {  
  3.   var now = Date.now();  
  4.   setTimeout(function () {  
  5.     var diff = Date.now() - now;  
  6.     sum += diff;  
  7.     count++;  
  8.     test();  
  9.   });  
  10. }  
  11.   
  12. test();  
一段时间之后在控制台里输入 sum/count,可以看到一个结果,类似于:
  1. > sum / count  
  2. 15.624555160142348  

什么?!!我要 1ms 的间隔时间,你却告诉我实际的平均间隔为 15.625ms!这个画面简直是太美。我们在 mac 上做同样的测试,得到的结果是 1.4ms。于是我们心生疑惑:这到底是什么鬼?如果我是一个果粉,我可能就要得出 Windows 太垃圾然后放弃 Windows 的结论了,不过好在我是一名严谨的前端工程师,于是我开始继续思索起这个数字来。

等等,这个数字为什么那么眼熟?15.625ms 这个数字会不会太像 Windows 下的最大计时器间隔了?立即下载了一个 ClockRes 进行测试,控制台一跑果然得到了如下结果:

  1. Maximum timer interval: 15.625 ms  
  2. Minimum timer interval: 0.500 ms  
  3. Current timer interval: 1.001 ms  
果不其然!查阅 node.js 的手册我们能看到这样一段对 setTimeout 的描述:
The actual delay depends on external factors like OS timer granularity and system load.

然而测试结果显示,这个实际延迟是最大计时器间隔(注意此时系统的当前计时器间隔只有 1.001ms),无论如何让人无法接受,强大的好奇心驱使我们翻翻看 node.js 的源码来一窥究竟。



酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部