Hotdry.
systems-engineering

VCMI 如何把 90 年代 Heroes III 引擎重构成可热插拔、Lua 可扩展的现代游戏运行时

拆解 VCMI 的 C++ 重构思路:JSON 描述 + 动态脚本加载 + Lua 事件总线,实现零冲突模组与热插拔。

1999 年发布的《魔法门之英雄无敌 III》是回合策略巅峰,但其 32 位二进制与硬编码数据把 “可扩展性” 锁死在 90 年代。VCMI 项目用现代 C++ 把整颗引擎重写一遍,目标只有一个:让 25 年后的玩家仍能像搭积木一样 “热插拔” 新城镇、新生物、新英雄,甚至把战斗逻辑换成 Lua 脚本。以下拆解它如何做到这一点。

一、从二进制 blob 到 JSON 描述:先解耦,再谈扩展

原版游戏把城镇、生物、法术全部写死在 EXE 与 LOD 压缩包里,任何新增内容都要 patch 二进制。VCMI 的第一步是把 “数据” 与 “代码” 彻底分离:

  • 所有实体(faction、creature、artifact、spell、hero class)统一用 JSON 描述,字段与内存结构一一对应;
  • 资源文件(图片、声音、动画)保持原格式兼容,但路径与命名规则开放给模组;
  • 引擎启动时通过 Filesystem::loadModFilesystem() 把每个 mod 的目录树挂到虚拟文件系统,后加载者覆盖前者,实现 “同名即覆盖” 的零冲突依赖。

这一步把 “新增一个城镇” 从反汇编变成写配置文件,为后续脚本化铺平道路。

二、热插拔架构:三行 JSON 就能把脚本装进运行时

VCMI 把 “脚本” 也当成一种实体,在 mod 的 content/config/scripts.json 里声明即可:

{
    "mySpellEffect": {
        "source": "scripts/lua/fireAura.lua",
        "implements": "BATTLE_EFFECT"
    }
}

引擎在 ScriptHandler::initialize() 阶段扫描所有 mod 的声明,按类型分发给客户端 / 服务器:

  • implementsBATTLE_EFFECT,脚本会在战斗结算时按需加载到 LuaJIT 虚拟机;
  • implementsMAP_OBJECT,脚本成为地图事件的回调处理器;
  • 所有脚本文件以 std::filesystem::last_write_time 做版本戳,开发模式下改动后自动重载,无需重启游戏。

通过 “描述 - 注册 - 延迟加载” 三级解耦,VCMI 实现了真正意义上的热插拔:玩家在大厅里勾选 / 取消 mod,下一把游戏立即生效,无需重新编译本体。

三、Lua 运行时:事件总线 + 全局 API,把战斗逻辑写成脚本

VCMI 把 LuaJIT 嵌在服务端与客户端两侧,暴露一组全局对象:

  • GAME——IGameInfoCallback 接口,可查玩家、城镇、地图对象;
  • BATTLE——IBattleInfoCallback 接口,可迭代战场格子、部队堆;
  • EVENT_BUS—— 事件总线句柄,可订阅 PlayerGotTurnBattleAttackSpellCast 等 30+ 事件;
  • SERVICES—— 只读访问所有静态定义(creature stats、spell config),保证脚本只能 “读规则” 而不能 “改规则”;
  • DATA—— 持久化表,回合间自动序列化到存档,解决脚本状态落地问题。

以自定义战斗法术为例,脚本只需订阅事件并返回特效参数:

local SpellCast = require("events.SpellCast")
SpellCast.subscribeAfter(EVENT_BUS, function(e)
    if e.spell.id == "myFireAura" then
        e.targets[1]:addBonus({type="FIRE_SHIELD", value=50})
    end
end)

所有事件回调在服务器主循环中顺序执行,脚本异常被捕获并写进 script_errors.log,不会导致整局游戏崩溃。

四、实战:最小可运行 mod 的目录结构

MyFireAuraMod/
├─ mod.json              // 版本、作者、依赖声明
├─ content/
│  ├─ config/
│  │  ├─ spells.json     // 新增法术定义
│  │  └─ scripts.json    // 脚本注册表
│  └─ scripts/
│     └─ lua/
│        └─ fireAura.lua // 上文示例脚本
└─ Data/
   └─ spells/
      └─ fireAura.def    // 动画资源

关键字段只有三处:

  1. mod.json 里声明 `
查看归档