Node.js 制作实时多人游戏框架(2)

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

Node.js 制作实时多人游戏框架

在 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 平台下,运行这样一段代码:

复制代码 代码如下:


var sum = 0, count = 0; 
function test() { 
  var now = Date.now(); 
  setTimeout(function () { 
    var diff = Date.now() - now; 
    sum += diff; 
    count++; 
    test(); 
  }); 

test(); 

一段时间之后在控制台里输入 sum/count,可以看到一个结果,类似于:

复制代码 代码如下:


> sum / count 
15.624555160142348 

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

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

复制代码 代码如下:


Maximum timer interval: 15.625 ms 
Minimum timer interval: 0.500 ms 
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 的源码来一窥究竟。

Node.js 中的 BUG

相信大部分你我都对 Node.js 的 even loop 机制有一定的了解,查看 timer 实现的源码我们可以大致了解到 timer 的实现原理,让我们从 event loop 的主循环讲起:
 

复制代码 代码如下:


while (r != 0 && loop->stop_flag == 0) { 
    /* 更新全局时间 */ 
    uv_update_time(loop); 
    /* 检查计时器是否到期,并执行对应计时器回调 */ 
    uv_process_timers(loop); 
 
    /* Call idle callbacks if nothing to do. */ 
    if (loop->pending_reqs_tail == NULL && 
        loop->endgame_handles == NULL) { 
      /* 防止event loop退出 */ 
      uv_idle_invoke(loop); 
    } 
 
    uv_process_reqs(loop); 
    uv_process_endgames(loop); 
 
    uv_prepare_invoke(loop); 
 
    /* 收集 IO 事件 */ 
    (*poll)(loop, loop->idle_handles == NULL && 
                  loop->pending_reqs_tail == NULL && 
                  loop->endgame_handles == NULL && 
                  !loop->stop_flag && 
                  (loop->active_handles > 0 || 
                   !ngx_queue_empty(&loop->active_reqs)) && 
                  !(mode & UV_RUN_NOWAIT)); 
    /* setImmediate() 等 */ 
    uv_check_invoke(loop); 
    r = uv__loop_alive(loop); 
    if (mode & (UV_RUN_ONCE | UV_RUN_NOWAIT)) 
      break; 
  } 

其中 uv_update_time 函数的源码如下:(https://github.com/joyent/libuv/blob/v0.10/src/win/timer.c))

复制代码 代码如下:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wgxgzp.html