最近做了一个网页版 NES 游戏站。打开页面,选一款小时候玩过的游戏,创建双人房间,再把链接发给朋友。对方进入以后,可以一边语音或视频聊天,一边在同一局游戏里分别控制 P1 和 P2。
不需要远程控制对方的电脑,也不是把游戏画面当视频直播。两个浏览器各自运行同一个 Rust/WebAssembly 模拟器,通过 WebRTC 交换手柄输入和同步状态。摄像头与麦克风也是可选的,关掉以后仍然可以正常联机。
小时候两个人要坐在同一台电视前,共用一对手柄。现在大家可能已经不在同一个城市,但发一条链接,还是可以把那一局接起来。这是这个项目最想实现的体验。
当前可用的游戏功能
进入游戏站后,可以浏览中文游戏目录,也可以搜索游戏。选中一款后,浏览器下载对应 ROM,校验内容,再交给 WebAssembly 模拟器载入。
单机时可以直接使用:
- 键盘;
- 标准 Gamepad API 手柄;
- 用户主动授权的 WebHID 兼容手柄;
- 移动设备上的触控按键;
- 三个即时存档槽;
- SRAM 自动保存;
- 本地选择的 .nes 文件。
本地双人也可以共用一台电脑。P1 使用 WASD、J/K、Shift 和 Enter,P2 使用方向键和小键盘。
不过这个项目里更有意思的部分,是把原来的本地双人搬到两台浏览器上。
一条链接建立双人房间
房主先载入游戏,再创建联机房间。页面会生成房间码和邀请链接,链接里带有游戏 ID 与房间码。
朋友打开链接后,页面自动找到同一款游戏并加入房间。双方会再次检查 ROM SHA-256 和模拟器核心版本,只有完全一致才继续建立游戏通道。
连接完成后,角色是固定的:
- 房主控制 P1;
- 客机控制 P2。
游戏运行在两边本地。网络传输的是按键和必要的状态数据,不是每一帧画面。这样在连接正常时,操作不需要先绕到服务器再返回。
可以边聊天边玩
进入房间后,麦克风和摄像头默认不会擅自打开。用户可以分别启用语音和视频,浏览器会在操作时请求设备权限。
音视频与游戏数据使用同一个 RTCPeerConnection,但彼此不是同一个业务通道:
- 音频轨道传麦克风;
- 视频轨道传摄像头;
- input DataChannel 传逐帧手柄输入;
- control DataChannel 传握手、状态和同步控制。
摄像头打不开,不会让游戏跟着断开;麦克风授权失败,也不影响两条 DataChannel。对这类怀旧游戏来说,视频不是必需品,但看到对方打到熟悉场景时的反应,会比纯粹联机多一点意思。
网络抖动时的回滚与同步
如果每一帧都等待远端按键,网络延迟会直接变成操作延迟。当前实现会让本地先运行,暂时预测对方输入;真实输入到达后,如果预测不一致,就恢复最近的 checkpoint 并重新执行几帧。
双方还会定期比较模拟器状态 hash。发现状态不一致、回滚窗口不够,或者 RTC 连接刚刚重建时,游戏会暂停,由房主发送完整状态,把客机重新拉到同一帧。
使用者看到的应该只是短暂的“正在同步”,而不是自己处理存档和进度。具体实现分别记录在:
联机要求 ROM 与核心版本完全一致
两边显示相同游戏名,不代表 ROM 内容一定相同。翻译版、修改版和不同归档中的同名文件,都可能有不同字节内容。
项目不按文件名判断联机兼容,而是核对 ROM SHA-256。模拟器核心源码发生变化后,coreBuildId 也会改变。只要 ROM 或核心版本不同,双方就不能继续同步。
这项限制看起来严格,但它避免了更麻烦的情况:两边勉强开始后,在某个关卡因为代码或 Mapper 行为不同而悄悄跑偏。
项目目前支持 16 个 Mapper 编号。卡带如何切换 PRG、CHR bank,以及 MMC1、MMC3 和其他 IRQ 型 Mapper 怎么适配,单独记录在 Mapper 适配记录:从 Bank Switching 到 IRQ。
目前还不是一个公共游戏平台
当前站点是受控访问的自托管项目,不是面向公众分发 ROM 的开放平台。
现有目录有 476 个内容不同的 ROM,它们通过了当前的启动、存档恢复、确定性重放和帧哈希回归。这只能说明测试过的路径可以运行,不能证明每款游戏都已经人工通关,也不能把模拟器代码的 MIT License 延伸成游戏内容许可。
联机体验也还有明确边界:
- 只支持两个人;
- 房主固定为 P1;
- 还没有覆盖所有浏览器与网络组合;
- 弱网下可能出现回滚、暂停或完整状态重同步;
- 浏览器进入后台后会暂停联机;
- WebHID 在部分浏览器中不可用。
这些限制暂时都没有藏起来。项目现在更像一个已经能实际使用的私人游戏空间,而不是完成度很高的游戏平台。
最后
这个项目最开始的目标并不复杂:让一款老游戏重新变成两个人共同经历的事情。
打开网页,挑一款熟悉的游戏,把链接发给很久没见的朋友。语音里聊着近况,偶尔开一下摄像头,再为谁抢走道具、谁先掉下去笑一会儿。
技术上,底下有模拟器、Mapper、WebAssembly、WebRTC、回滚和状态同步。真正使用时,希望它只剩下一件很轻松的事:今晚要不要再打一局。