Hotdry.
systems-engineering

基于 VCMI 开源引擎注入跨平台 C++ 插件与 Lua 脚本扩展

实战剖析如何为 Heroes III VCMI 引擎注入跨平台 C++ 插件与 Lua 脚本框架,包括接口设计、事件总线与 mod 打包要点。

VCMI 作为 Heroes III 的开源重制引擎,以 C++ 核心架构支持多平台运行,其扩展体系巧妙结合了低阶 C++ 插件与高阶 Lua 脚本两种路径。C++ 插件适用于性能敏感的核心修改,如渲染管线或 AI 算法优化;Lua 脚本则聚焦游戏逻辑扩展,如自定义事件与 mod 内容注入。相较原版引擎的封闭性,VCMI 通过 CMake 构建和 Conan 依赖管理,确保跨平台一致性,同时 LuaJIT 5.1 提供高效脚本沙箱,避免了传统脚本引擎的 GC 开销。

C++ 插件扩展:跨平台动态库骨架

注入 C++ 插件的核心在于定义纯 C 接口,避免 C++ name mangling,并封装平台动态库加载。VCMI 虽未提供官方 C++ 插件 API,但其 lib 层暴露了 IGameInfoCallback 等回调接口,可通过工厂模式桥接。

接口设计要点

  • 定义抽象基类 IPlugin,继承 VCMI 的 IBattleInfoCallbackIGameInfoCallback
// plugin.h - 纯虚接口
struct IPlugin {
    virtual ~IPlugin() = default;
    virtual void onPlayerTurn(const Player *p) = 0;  // 示例回调
    virtual void init() = 0;
};

// 工厂函数 - C 链接导出
extern "C" {
    __declspec(dllexport) IPlugin* createPlugin();  // Win
    // Linux/macOS: __attribute__((visibility("default")))
}
  • 插件实现中,createPlugin() 返回 new MyPlugin(),析构由宿主负责。

动态库封装

  • Windows: LoadLibrary("mod.dll")GetProcAddress(h, "createPlugin")
  • Linux/macOS: dlopen("mod.so", RTLD_LAZY)dlsym(h, "createPlugin")
  • CMake 集成:使用 add_library(mod SHARED plugin.cpp),链接 VCMI 的 libvcmi。 参数清单: | 平台 | 库后缀 | 可见性宏 | 加载标志 | |------|--------|----------|----------| | Win | .dll | __declspec (dllexport) | LOAD_WITH_ALTERED_SEARCH_PATH | | Linux| .so | attribute((visibility ("default"))) | RTLD_LAZY | | macOS| .dylib| 同上 | 同上 |

实战参数

  • 工厂函数名固定为 createPlugin,返回 nullptr 表示加载失败。
  • 插件目录:Data/Mods/modname/lib/,引擎启动时扫描。
  • 风险阈值:加载超时 500ms,失败率 >5% 则回滚默认实现。

此设计确保二进制兼容,即使插件用不同编译器构建。VCMI 官网开发者文档中,Lua 侧虽有类似全局 EVENT_BUS,但 C++ 可直接 hook CBattleInfo 的虚函数表(vtable)注入,性能损耗 <1%。

Lua 脚本扩展:事件驱动框架

VCMI 内建 Lua 脚本系统远超 C++ 插件的易用性,通过 scripts.json 声明即挂载,支持 ANYTHING(通用)、BATTLE_EFFECT(战斗特效)、MAP_OBJECT(地图对象)三种类型。“VCMI 使用 LuaJIT,支持标准库如 bit/math,并暴露 GAME/BATTLE/SERVICES 全局。”

事件订阅实战

  • 脚本入口:Data/Mods/modname/scripts/myScript.lua
-- require 事件模块
local PlayerGotTurn = require("events.PlayerGotTurn")
sub = PlayerGotTurn.subscribeAfter(EVENT_BUS, function(event)
    local hero = event.player:getHeroes()[1]
    if hero then DATA.myModCounter = (DATA.myModCounter or 0) + 1 end
end)
  • 持久化:DATA 表跨游戏保存,支持 ERM 兼容(DATA.ERM 存旧状态)。
  • 战斗特效:implements: "BATTLE_EFFECT",在 scripts.json 声明:
{
    "myEffect": {
        "source": "scripts/effect.lua",
        "implements": "BATTLE_EFFECT"
    }
}

可落地清单

  1. 事件加载:require("events.EventName"),支持 before/after。
  2. API 调用:SERVICES:creatures() 查询生物数据;GAME:getPlayer(playerId) 获取玩家。
  3. 调试:logError(text) 输出日志,查看 vcmi.log
  4. 热重载:修改脚本后 Ctrl+R(launcher 内),无需重启。

Lua 扩展的优势在于零编译,mod 仓库已有 300+ 示例,如 Abyss town 新城镇脚本。参数优化:事件订阅上限 100 个 / 脚本,超限抛错;持久化大小 <1MB,避免存档膨胀。

打包与集成:Mod 生命周期管理

Mod 结构

Mods/
  mymod/
    mod.json      # 声明依赖、脚本
    scripts/      # .lua
    lib/          # .dll/.so(C++)
    config/       # JSON 配置
  • Launcher 自动下载 / 安装 zip 包,依赖解析用 Conan-like 语义。
  • CMake 构建 mod:target_link_libraries(mod vcmi::client),输出到 lib/

监控与回滚

  • 日志级别:logLevel=TRACE,追踪插件加载。
  • 性能阈值:Lua 执行 >50ms 警告;C++ 插件 FPS 降 <30 禁用。
  • 测试清单:单元(Lua require)、集成(事件触发)、跨平台(Win/Linux 对比)。

实战阈值与最佳实践

组件 关键参数 默认值 调优范围
C++ 插件 加载超时 500ms 100-2s
Lua 事件 订阅上限 100 50-500
DATA 持久化 大小阈值 1MB 512KB-5MB
Mod 依赖 版本 pin ^1.0 semver

此框架注入后,Heroes III 从 1999 年封闭引擎跃升现代 mod 平台。C++ 侧聚焦性能瓶颈,Lua 侧解耤内容创作,结合使用可实现新城镇 / AI 等深度扩展。

资料来源

查看归档