跳转到内容

OpenRCT2 — 把一款 x86 汇编游戏彻底用 C++ 重写

是什么

OpenRCT2 是 RollerCoaster Tycoon 2(过山车大亨 2)的完整开源 C++ 重实现。日常类比:就像有人把一本只能看、无法改的手写食谱,逐字翻译成可以协作编辑的电子文档,然后顺手把缺页补全了。

原版 RCT2 由 Chris Sawyer 一人用 x86 汇编独立编写,代码极度优化但完全不可读,只跑在 Windows 上。OpenRCT2 的目标是:用 C++ 完全重现原版玩法,同时新增在线多人联机、跨平台支持(Windows / macOS / Linux)、更高的游戏限制、TypeScript 插件 API,以及一批 QoL 改进。

项目起步于 2014 年,2023 年完成最后一批核心汇编代码的 C++ 转换,约 15k GitHub stars,是游戏逆向工程领域最完整的开源重实现案例之一。

为什么重要

不了解 OpenRCT2,下面这些问题都没法解释:

  • 为什么 x86 汇编游戏能在 macOS 和 Linux 上运行,却不需要 Wine 或虚拟机
  • 为什么”不改变玩法”的情况下,多人联机可以事后追加到一个单机游戏里
  • 为什么逆向工程进度可以在保证”零回归”的前提下分阶段推进十年
  • 为什么社区 mod 可以用 TypeScript 写,而不用修改任何 C++ 引擎代码

核心要点

  1. 逐步替换策略:不是从零重写,而是把 x86 汇编逐片段翻译成等价 C++,用与原始寄存器同名的全局变量保持过渡期兼容,再分批重构为类和模块。类比:修一艘正在航行的船,每次只换一块甲板,船不能停。这个方法把”全部重写”的风险分摊到数年。

  2. 确定性帧步进(Lockstep)多人模式:联机玩家的游戏状态必须完全同步。OpenRCT2 用锁步帧同步——所有玩家每一帧执行相同输入,最终状态必须比特级一致。为此所有浮点运算必须跨平台可复现,任何随机性要用固定种子。类比:乐队不靠指挥,靠所有人对着同一个节拍器演奏。

  3. TypeScript 插件沙盒:引擎暴露 JavaScript 运行时(Duktape),允许插件用 TypeScript 读写游戏状态、注册自定义 UI、自动化操作。插件运行在沙盒里,无法破坏引擎核心。类比:Excel 里的宏——你能自定义工作表,但不能改 Excel 本身的公式引擎。这让社区在不 fork 的情况下无限扩展游戏。

实践案例

案例 1:把汇编全局变量迁移为 C++ 类成员

原始 x86 汇编里,玩家金钱直接存在某个固定内存地址(比如 0x013573DC)。第一步翻译时,代码变成:

// money32 是 OpenRCT2 自定义的整型货币单位(1 单位 = 0.01 英镑),
// 不是标准 C++ 类型——整个项目里统一用它避免浮点误差
// 过渡期:用全局变量对应原始地址
static money32 gCash;
// 读取金钱的汇编等价体
money32 GetCash() {
return gCash;
}

几百次提交之后,才重构为:

// 重构完成:封装进 GameState 结构体
struct GameState {
money32 cash;
// ...其他字段
};
GameState gGameState;

关键点:两个阶段之间所有测试必须通过,游戏存档必须加载成功。每个”重构 PR”本身不改变任何可见行为——这是零回归迁移的核心原则。

案例 2:实现 TypeScript 插件来自动化建造

OpenRCT2 内置了 Duktape 解释器(可以理解为”游戏引擎里的微型浏览器 JS 引擎”),让玩家用 TypeScript 写自动化脚本而无需修改任何 C++ 代码,例如批量添加设施:

// plugin.ts — 自动在地图四角添加厕所
function placeToiletsAtCorners() {
const mapSize = map.size;
const corners = [
{ x: 2, y: 2 },
{ x: mapSize.x - 2, y: 2 },
{ x: 2, y: mapSize.y - 2 },
{ x: mapSize.x - 2, y: mapSize.y - 2 },
];
for (const pos of corners) {
// 调用引擎暴露的 API 建造设施
map.place({ type: "facility", object: "toilets", ...pos, z: 0 });
}
}
// 在游戏菜单里注册为一个可点击的按钮
ui.registerMenuItem("Place Corner Toilets", placeToiletsAtCorners);

关键点:脚本运行在沙盒里,无法访问任何不在官方 API 里的内部状态,引擎版本升级时 API 向后兼容。这让社区数百个插件可以长期维护而不随引擎改动而失效。

案例 3:调试多人帧不同步

联机时如果两个客户端的游戏状态产生偏差(desync,即”两人看到的游乐园已经不一样了”),游戏会立刻断线以防止进一步撕裂。排查方法:在两台联机的电脑上分别打开终端,运行以下命令过滤状态哈希日志:

Terminal window
# 在终端(命令行)里运行 OpenRCT2 并过滤出含 "hash" 的行
# "grep" 是过滤命令,只显示匹配关键词的日志
openrct2 --network-log-level=verbose 2>&1 | grep "hash"
# 对比两台机器在第 N 帧输出的哈希值
# 如果哈希不同,说明那一帧出现了分歧
# 常见根因:
# - 随机数没用固定种子(改用 gScenarioRand,不能用 std::rand())
# - 平台浮点精度差异(改用整数或定点数替代浮点)
# - 计时器精度在 Windows/Linux 上不一致

关键点:状态哈希是多人模式的”体检报告”,每帧广播一次;客户端对比发现偏差立刻报告,比”等到视觉上出问题”早几十帧。这种方法也适用于任何需要确定性模拟的联机游戏。

踩过的坑

  1. 必须提供原版 RCT2 安装目录:OpenRCT2 不附带游戏资源(图形、声音、地图),版权归 Atari。拿不到原版文件就无法启动,连 demo 都跑不了。

  2. 汇编遗留大量隐式全局状态:原作者用汇编直接操作内存,没有”对象”的概念。翻译出来的 C++ 充满互相依赖的全局变量,改一处容易触发距离很远的副作用。每次重构都要大量回归测试。

  3. 浮点不一致导致联机断线:不同编译器、不同 CPU 处理浮点的行为有细微差异(比如 x87 vs SSE2 精度)。任何用于游戏逻辑的浮点运算都需要用整数或定点数重写,否则联机必然断线。

  4. 旧存档格式无公开文档.sv6(存档)和 .sc6(场景)格式由 Chris Sawyer 自己设计,从未公开。所有字段语义靠反汇编原版执行文件逐比特推断,加上社区多年对齐测试。新增字段时要维护向前兼容性。

适用 vs 不适用场景

适用

  • 逆向工程老游戏、老软件并发布为开源版本(参考路径:OpenTTD / OpenRA / openage)
  • 给没有 mod 支持的老游戏追加插件系统,且不想 fork 或改核心
  • 需要在原始单机游戏基础上实现联机,但引擎架构完全不考虑网络的情况
  • 学习”如何在不中断服务的前提下大规模重构遗留系统”

不适用

  • 从头构建新游戏(用 bevy / panda3d 等游戏引擎更高效)
  • 需要完全原版体验但不想提供 RCT2 授权文件(OpenRCT2 不能替代购买原版)
  • 期望插件能访问底层渲染管线(插件 API 只暴露游戏逻辑层,不涉及 OpenGL/Vulkan)
  • 汇编基础薄弱而又必须深度参与核心逆向工作的团队

历史小故事(可跳过)

  • 2002 年:RollerCoaster Tycoon 2 发售,Chris Sawyer 用汇编独立完成整个引擎,代码极度紧凑。
  • 2011 年:社区开始研究 RCT2 可执行文件,最初只是用内存补丁解锁游戏内的限制(骑乘高度、游客数量等)。
  • 2014 年:开发者 IntelOrca 发起 OpenRCT2 项目,目标是用 C++ 完整重写,让游戏跨平台并开放源码。
  • 2018 年:在线多人模式正式稳定,lockstep 帧同步实现,全球玩家可以联机建园。
  • 2020 年:TypeScript 插件 API 发布,社区数百个插件涌现,覆盖自动化、数据分析、自定义 UI。
  • 2023 年:核心游戏逻辑中最后一批 x86 汇编等价代码完成 C++ 重构,标志着”翻译阶段”正式结束,进入纯 C++ 开发模式。

学到什么

  1. 逆向工程 + 迁移可以分阶段做:不需要一次性重写,逐片翻译 + 过渡期全局变量 + 大量测试,能把风险降到每次只承担一小步。
  2. 确定性模拟是联机的基础:多人模式不靠服务器权威,而是靠”所有客户端执行完全相同的计算”。任何非确定性(随机、浮点、时序)都是 bug。
  3. 好的插件 API 比 fork 更有生命力:TypeScript 沙盒让社区在不破坏核心的前提下无限扩展,比让人 fork 修改 C++ 吸引更广泛的贡献者。
  4. 文档缺失是遗留系统最大的技术债:原版 RCT2 存档格式靠反汇编推断耗费了社区数年时间,提前写文档的边际成本极低,但事后补的代价极高。

延伸阅读

  • OpenRCT2 官方文档与下载(含各平台安装指南和 changelog)
  • 插件 API 开发文档(TypeScript 插件接口完整参考)
  • OpenTTD —— Transport Tycoon 的同类重实现,走了相似的逆向工程路径
  • bevy —— Rust 游戏引擎,代表”从头写新游戏”的现代路线,与 OpenRCT2 的”重实现老游戏”路线形成对比
  • minetest —— 另一个开源游戏项目,展示社区驱动开发如何维护长期活跃度

关联

  • bevy —— 现代 Rust 游戏引擎,适合从头构建;OpenRCT2 则是逆向重实现的典型路线,两者目标相反
  • minetest —— 同为开源游戏,展示了如何用插件生态替代 mod 文件,插件理念与 OpenRCT2 TypeScript API 相近
  • panda3d —— Python 3D 游戏引擎,与 OpenRCT2 同属”游戏引擎”分类但面向全新创作
  • cocos2d-x —— 跨平台游戏引擎,同样解决”一份代码跑多平台”的问题,但走的是引擎抽象层路线而非逆向迁移
  • nvm —— 版本管理工具,体现了”不改变原始行为、只增加管控层”的相似工程哲学

反向链接

  • bevy —— Bevy — Rust 数据驱动 ECS 游戏引擎
  • minetest —— Luanti / Minetest — 给自己造一个开源体素游戏引擎
  • nvm —— nvm — 在同一台机器上轻松切换 Node 版本
  • panda3d —— Panda3D — Disney/CMU 出品的开源 3D 游戏引擎