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

网络编程 2025-04-04 14:16www.168986.cn编程入门

Node.js 实时多人游戏框架的构建之旅

随着 Node.js 的蓬勃发展,其应用场景愈发广泛。在最近的极客松活动中,我们利用短短36小时,基于 Lan Party 概念,构建了一款实时多人互动游戏。这篇文章将带你走进我们如何利用 Node.js 制作实时多人游戏框架的世界。

一、起步与 Spaceroom 概览

在项目的初始阶段,我们的设计完全是由需求驱动的。我们希望 Spaceroom(我们的实时多人游戏框架)能具备以下基础功能:

能够以房间(频道)为单位区分用户群体;能够收集并处理来自组内用户的指令;在各个客户端之间实现精确的时间同步,以规定的间隔广播游戏数据;努力消除网络延迟带来的影响。随着开发的深入,我们为 Spaceroom 增加了更多功能,如游戏暂停、生成一致随机数等。这些功能可以根据实际需求在游戏逻辑框架内实现,并非 Spaceroom 框架的核心功能。

二、Spaceroom 的 API 介绍

Spaceroom 分为前后端两部分。服务器端主要负责维护房间列表,提供创建房间、加入房间的功能。我们的客户端 API 如下:

spaceroom.connect(address, callback) - 连接服务器;spaceroom.createRoom(callback) - 创建一个新房间;spaceroom.joinRoom(roomId) - 加入一个已存在的房间;spaceroom.on(event, callback) - 监听事件。客户端连接到服务器后,会收到各种事件,如新玩家加入、游戏开始等。客户端的状态可以通过 spaceroom.state 获取。

三、服务器端的实现与同步算法

服务器端的实现相对简单。如果使用默认配置文件,可以直接运行服务器端框架。我们的服务器代码可以直接在客户端运行,无需单独的服务器。对于玩过 PS 或 PSP 的玩家来说,这种体验应该非常熟悉。具体的逻辑代码实现可以参考源码。

那么,如何实现各个客户端之间的实时同步呢?关键在于同步算法。我们需要一种方式来同步用户指令。如果所有的客户端都能接收到同样的指令,那么所有的客户端就能有相同的运行结果。网络游戏的同步算法多种多样,适用的场景也各不相同。Spaceroom 采用的同步算法类似于帧锁定的概念。我们把时间轴分成一个个区间,每个区间称为一个 bucket。Bucket 用于装载指令,由服务器端维护。在每个 bucket 时间段的末尾,服务器将 bucket 广播给所有客户端,客户端验证后执行其中的指令。这样的设计旨在确保游戏的实时性和公平性。

为了应对网络延迟带来的影响,我们的服务器采用了一种精准的处理方式。每当接收到来自客户端的指令时,它都会按照预定的算法投递到对应的bucket中。这个过程遵循一定的步骤和规则。

我们设定指令携带的指令发生时间为order_start,而每个bucket的起始时间为t。如果t加上延迟时间delay_time小于或等于当前正在接收指令的bucket的起始时间,那么指令就会被投递到当前的bucket中。否则,我们会继续执行下一步。这里的delay_time是服务器与客户端之间的约定延迟,通常取客户端之间的平均延迟。在Spaceroom中,默认值为80,bucket的默认长度是48。

在每个bucket时间段的结束时,服务器会将该bucket广播给所有客户端,然后开始接收下一个bucket的指令。客户端则根据收到的bucket间隔,自动进行时间同步,将时间误差控制在一个可接受的范围内。这意味着,正常情况下,客户端每隔48ms会收到一个来自服务器的bucket。当需要处理这个bucket时,客户端会进行相应的操作。

在网络波动的情况下,如果因为延迟超出预期时间而没有收到bucket,客户端会暂停游戏逻辑并等待。在一个bucket的时间范围内,逻辑的更新可以通过线性插值(lerp)的方法来实现。

关于计时器的问题,我们的框架需要一个精确的计时器来在固定的间隔下执行bucket的广播。我们曾考虑过使用setInterval(),但很快意识到这个方法存在严重的误差。每次误差都会累积,导致越来越严重的后果。

于是我们转向setTimeout(),希望通过动态调整下次执行的时间来稳定我们的逻辑。然而测试结果并不理想。在的node.js稳定版以及Windows平台下,我们尝试了一段简单的代码来测试setTimeout()的精度,结果令人失望。在Windows环境下,setTimeout()的平均间隔达到了15.625ms,而我们期望的是1ms的间隔。

尽管我们在Mac上测试得到了较好的结果,但在Windows平台上遇到的问题使我们开始寻找解决方案。我们开始怀疑是否是Windows下的最大计时器间隔导致的这个问题。我们的疑惑促使我们继续深入研究这个问题,寻找更好的解决方案。我们希望找到一种方法,能够在各种平台上都能保证计时器的精度,以确保我们的游戏逻辑能够流畅运行。好奇心驱使之下,我立即下载了一款名为ClockRes的应用程序进行测试。当我在控制台运行后,结果即刻呈现在眼前,令人不禁深入。

测试代码呈现出的结果如下:

最大计时器间隔:15.625毫秒

最小计时器间隔:0.5毫秒

当前计时器间隔:1.001毫秒

显然,这与我们预期的节点程序性能有所出入。当我们翻开node.js的手册,会找到关于setTimeout的一段描述。其中指出实际延迟取决于外部因素,如操作系统的计时器精细度和系统负载。这样的解释,或许可以为我们带来一些启示。

测试的结果显示,实际延迟受到最大计时器间隔的影响,而当前系统的计时器间隔仅为1.001毫秒。这一发现让我无法轻易接受现状,于是出于强烈的好奇心,我决定深入node.js的源代码。

在这段代码中,我们可以看到setTimeout函数的实现细节。它是如何与系统时钟、事件循环和其他核心模块交互的?这些疑问在我心中萦绕,促使我深入背后的原理。这不仅仅是对技术的,更是对极限的追求和对知识的渴望。在这个过程中,我发现自己的好奇心得到了极大的满足,同时也对node.js有了更深入的了解。这次之旅让我深感技术世界的无限魅力,也让我对未来充满了期待。深入了解Node.js中的Event Loop机制,特别是其timer实现源码,能让我们更清晰地理解Node.js的工作原理。现在,让我们聚焦于event loop的主循环,其中的奥秘。

主循环的每一次迭代都在不断运行,直到满足特定的退出条件。在这个过程中,它会进行一系列的操作,如更新全局时间、检查并执行到期计时器的回调等。这个过程涉及到的代码逻辑复杂且精细。

我们看到`uv_update_time`函数,这是用来更新当前系统时间的。它的内部实现利用了Windows系统的GetTickCount()函数,该函数能够获取到当前系统的时钟时间,以毫秒为单位。然后,这个时间值被用来更新LARGE_INTEGER类型的time变量,这个变量用来存储当前的时间。如果计时器的值已经超过了原有的时间值,那么就需要对计时器的高位字节进行加1操作。这样的设计确保了计时器的准确性。

接下来,我们继续看主循环的其他部分。当没有正在运行的请求或者事件时,会调用idle回调函数以防止event loop退出。然后,它会处理所有的请求和结束游戏相关的操作。接着,它会调用prepare invoke来执行一些预先准备好的操作。然后,它会收集所有的IO事件,并执行相应的处理函数。如果有setImmediate()等立即执行的任务,也会进行相应的处理。在这个过程中,如果满足特定的退出条件(如单次运行或者非等待运行),那么就会跳出主循环。否则,就会一直运行直到满足退出条件。

更新全局时间并舞动event loop的魔法

当我们的程序在舞蹈般的event loop中翩翩起舞时,首先会更新全局的时间标记,就像调整舞台的灯光,为接下来的表演做好准备。

接着,我们会检查是否存在过期的定时器,如果有,就会执行相应的回调,如同舞台上的演员在关键时刻完成他们的表演。然后,我们会检查等待的请求队列reqs,处理正在排队的请求,这就像是在舞台侧边处理即将上台的演员们。

进入核心环节——poll函数。这个函数如同一个繁忙的指挥家,负责收集所有的IO事件。如果有IO事件到来,它会将处理这些事件的函数添加到reqs队列中,等待下一次event loop的执行。在这个环节,系统会调用一个方法来收集IO事件,这个方法会让进程阻塞,直到有IO事件到来或者达到设定的超时时间。

现在,假设我们设置了一个1ms的计时器。poll函数最多会阻塞1ms后恢复。当event loop继续时,uv_update_time会更新当前时间。如果我们发现GetTickCount函数的精度问题,那么即使poll函数只阻塞了1ms,uv_update_time可能并没有精确地更新当前时间。我们的定时器不会被判定为过期,poll函数再次等待1ms。这就像是一个舞蹈节奏的微调,但由于时间的精度问题,舞蹈可能变得不太协调。

event loop的工作流程就像一场精心编排的舞蹈,各个部分协同工作,确保程序的流畅运行。如果我们遇到像GetTickCount这样的精度问题,可能会打乱这个节奏,需要我们深入并修复这些问题。当我们终于看到GetTickCount成功更新,即每15.625毫秒更新一次时,循环的当前时间得到了更新。这时,我们的计时器在uv_process_timers中被判定为已过期。

对于Node.js的源代码,有时让人感到很无助。它使用了一个精度不高的时间函数,而且并未做任何特殊处理。因为我们使用的是Node-WebKit,除了Node.js的setTimeout,我们还有Chromium的setTimeout可以选择。我们编写了一段测试代码,在浏览器或Node-WebKit上运行(括号中的数字表示需要测定的间隔),结果如下。

按照HTML5的规范,理论上,前5次的结果应该是1毫秒,之后的结果则是4毫秒。然而在我们的测试案例中,结果从第三次开始,这意味着在理论上,前三次的结果都应该是1毫秒,之后的每次结果则会有变化。尽管存在误差,并且最小理论结果也是4毫秒,但显然这个结果比Node.js的要好得多,令人满意。

强烈的好奇心驱使我们去查看Chromium的源代码,了解它是如何实现的。在确定循环的当前时间方面,Chromium使用了timeGetTime()函数。查阅MSDN我们可以发现,这个函数的精度受到系统当前的timer interval的影响。在我们的测试机上,这个精度是大约的1.001毫秒。在Windows系统中,timer interval默认是其最大值(在我们的测试机上为15.625ms),除非应用程序修改了全局timer interval。

如果你关注IT新闻,可能看到过这样一条新闻:Chromium似乎设定了非常小的计时器间隔。难道我们不再需要担心系统的计时器间隔了吗?别高兴得太早。事实上,这个问题在Chrome 38中已经被修复了。难道我们要使用修复前的Node-WebKit吗?这显然不够优雅,也阻止了我们使用性能更高的Chromium版本。

深入EnableHighResolutionTimer函数

在技术的世界里,计时器的精度往往关乎着程序的性能和用户体验。今天我们要的EnableHighResolutionTimer函数,便是与时间精度息息相关的一个关键部分。

该函数的主要目的是根据需求启用或禁用高分辨率计时器。让我们深入理解其工作原理。

我们看到在函数的开头部分,使用了base::AutoLock lock(g_high_res_lock.Get())来保证线程安全。然后,通过检查全局变量g_high_res_timer_enabled的值来决定是否执行后续操作。如果已启用或禁用高分辨率计时器,函数会直接返回,避免重复操作。

接下来,当我们要启用或禁用高分辨率计时器时,会调用timeBeginPeriod和timeEndPeriod这两个由Windows提供的函数。这两个函数能够修改系统的timer interval,也就是计时器的最小时间间隔。在接入电源时,我们能够获得的最小timer interval是1ms,而在使用电池时,这个间隔会扩大到4ms。

那么为什么我们需要关注这个时间间隔呢?因为在某些场景下,程序的运行时间可能会受到这个间隔的影响。比如在游戏逻辑的实现中,我们经常使用requestAnimationFrame函数来不断更新画布。这个函数需要至少一个16ms的计时器,以触发高精度计时器的要求。如果系统的计时器间隔较大,可能会影响游戏的流畅性和响应速度。

回到我们的测试环节,测试结果发现setTimeout的间隔并不稳定在4ms,而是在不断波动。在Chromium和Node.js的event loop中,这种波动是由系统计时器的影响造成的。当测试机器使用电源时,系统的timer interval为1ms,因此测试结果会有±1ms的误差。如果你的电脑没有被更改系统计时器间隔,运行特定的测试,最大延迟可能会达到64ms。

通过使用Chromium的setTimeout实现,我们可以将误差控制在相对较小的范围内。具体来说,对于setTimeout(fn, 1),误差可以控制在大约4ms;而对于setTimeout(fn, 48),误差可以控制在大约1ms。这对于提高程序的性能和用户体验至关重要。

EnableHighResolutionTimer函数为我们提供了一种灵活的方式来控制计时器的精度,使我们能够根据实际需求来优化程序的性能。在未来的技术发展中,我们期待更精确的计时器技术能够带来更多的惊喜和可能性。经过深入研究,我们的团队设计了一种新的蓝图,使得代码更加高效且易于理解。以下是重构后的代码片段:

```javascript

// 获取最大间隔偏差

var deviation = getMaxIntervalDeviation(bucketSize); // bucketSize设定为48,deviation设定为2

function gameLoop() {

var now = Date.now();

// 检查是否已达到时间桶边界

if (previousBucket + bucketSize <= now) {

previousBucket = now;

doLogic();

}

// 计算延迟时间,如果超过了设定的偏差值,则执行相应的延迟操作

var delay = bucketSize - deviation - (now - previousBucket);

if (delay > 0) {

// 等待小于或等于bucket_size的时间,实际间隔小于48ms

setTimeout(gameLoop, delay);

} else {

// 忙等待,使用setImmediate以避免阻塞IO事件

setImmediate(gameLoop);

}

}

```

在这段代码中,我们调整了时间间隔的逻辑,允许有一定的偏差值。当实际时间超过设定的时间桶边界时,执行相应的逻辑操作。而在等待下一个时间桶的过程中,我们根据偏差值来设定延迟时间,实现了更精确的间隔控制。这种方式相较于直接等待固定的时间间隔更加灵活,能有效避免资源浪费。我们也面临着一些挑战。

虽然我们在Chromium环境中取得了一定程度的成功,但解决方案显然不够优雅。我们注意到在Node.js环境中存在一个潜在的BUG,这个BUG可能导致CPU使用率飙升。幸运的是,在v.0.11.3版本中,这个问题已经得到了修复。通过查看libuv代码的master分支,我们可以看到修改后的代码在poll函数等待完成后,会加上一个timeout,从而确保计时器能够如期到期。这意味着我们的努力虽然部分得到了回报,但仍需关注系统定时器的影响。为此,我们可以考虑编写Node.js插件来调整系统定时器的间隔。在我们的游戏中,由于客户端建立房间后成为服务器,因此在Node-WebKit环境中的计时器问题并非首要考虑的问题。我们已经按照上述解决方案取得了满意的结果。我们还提供了WebSocket支持以及自定义通信协议来实现更高性能的Socket支持。随着需求的增加和时间的推移,我们不断完善这个框架的功能和性能。例如,为了满足游戏中生成一致随机数的需求,我们为Spaceroom增加了生成随机数种子的功能。客户端可以利用md5的随机性结合随机数种子生成随机数。看到这些成果令人倍感欣慰。在编写这样一个框架的过程中,我们不仅解决了技术难题,也收获了宝贵的经验。如果你对Spaceroom感兴趣,欢迎加入我们共同开发!在无限的宇宙空间中,Spaceroom 正在逐渐展现出它的无限潜力,并有望在更多领域大放异彩。这不仅仅是一个概念或是一个设想,而是承载着人类对未知领域的渴望和精神的象征。

想象一下,Spaceroom 如同一位矫健的拳击手,正蓄势待发,准备在更广阔的舞台上施展拳脚。它的存在不仅仅是为了在某个特定领域里发挥出色的功能,更是为了给人们的生活带来更多的可能性和想象空间。无论你是在喧嚣的都市,还是在宁静的乡村,Spaceroom 都能够为你带来前所未有的体验。

随着技术的不断发展和人们对生活品质追求的日益提升,Spaceroom 正在成为更多人心中的理想之选。它的出色表现不仅仅是停留在表面,而是深入的卓越性能。无论是从设计、功能还是用户体验上,Spaceroom 都能够给人带来惊喜和满足。

在这个充满竞争和机遇的时代里,Spaceroom 正在以其独特的优势和特点吸引着更多人的关注。它的智能化、便捷性和高效性正在改变人们的生活方式和工作模式,让人们的生活变得更加美好和高效。

Spaceroom 的影响力正在逐渐扩大,它不仅仅是一个简单的产品,更是一种精神的象征。它代表着人类对未知领域的和追求,对未来的信心和憧憬。Spaceroom 正在成为一个让人们信任和依赖的品牌,成为更多人选择的理由和信念。

Spaceroom 正在以其独特的魅力和实力征服着更多的人心。它的未来充满了无限的可能性和潜力,我们有理由相信,它会在更多的地方施展它的拳脚,为人们的生活带来更多的惊喜和改变。让我们一起期待 Spaceroom 在未来的表现吧!

上一篇:javascript实现按回车键切换焦点 下一篇:没有了

Copyright © 2016-2025 www.168986.cn 狼蚁网络 版权所有 Power by