脚本引擎

C++ 与脚本语言的桥梁——Lua C API, 反射/序列化, 热重载, embedding vs extending。

概念

脚本引擎是游戏开发中”核心 (C++) 与逻辑 (脚本)“的粘合剂。为什么需要脚本?1) 游戏逻辑迭代速度远快于引擎编译速度;2) 设计师和策划不写 C++;3) 热重载——修改脚本无需重启游戏。最常见的游戏脚本语言是 Lua (轻量, 嵌入式), 也有 C#, Python 等。

核心概念

概念含义关键技术
Embedding将脚本语言嵌入 C++ 宿主程序Lua C API, sol2
Extending用 C/C++ 扩展脚本语言的功能注册 C 函数到 Lua
绑定层自动生成 C++ ↔ 脚本的互操作代码SWIG, tolua++, sol2, pybind11
反射运行时获取/修改程序结构的能力类型擦除, 属性注册
序列化C++ 对象 ↔ 脚本序列化格式存档/读档
热重载运行时重新加载脚本文件监控, 状态迁移

Lua C API 基础

// Lua 虚拟栈: C ↔ Lua 数据交换的唯一通道
// 压入参数 → 调用 Lua 函数 → 获取返回值
 
lua_State *L = luaL_newstate();       // 创建 Lua 虚拟机
luaL_openlibs(L);                      // 加载标准库
 
// 在 Lua 中执行代码
luaL_dostring(L, "function add(x, y) return x + y end");
 
// 调用 Lua 函数
lua_getglobal(L, "add");              // 获取全局函数 "add"
lua_pushnumber(L, 3);                 // 压入参数 x
lua_pushnumber(L, 4);                 // 压入参数 y
lua_pcall(L, 2, 1, 0);               // 调用: 2 个参数, 1 个返回值
double result = lua_tonumber(L, -1);  // 获取返回值
lua_pop(L, 1);                        // 清理栈
 
lua_close(L);

C++ 绑定层

// 使用 sol2 (现代 C++ Lua 绑定库)
#include <sol/sol.hpp>
 
struct Player {
    std::string name;
    int health;
    float x, y;
 
    void take_damage(int amount) {
        health -= amount;
    }
};
 
sol::state lua;
lua.open_libraries();
 
// 将 C++ 类暴露给 Lua
lua.new_usertype<Player>("Player",
    sol::constructors<Player()>(),
    "name",     &Player::name,
    "health",   &Player::health,
    "x",        &Player::x,
    "y",        &Player::y,
    "damage",   &Player::take_damage
);
 
// 在 Lua 中:
//    player = Player:new()
//    player.health = 100
//    player:damage(20)
//    print(player.health)  → 80

反射与序列化

// 简化版属性反射系统
struct Property {
    std::string name;
    std::function<void*(void*)> getter;
    std::function<void(void*, void*)> setter;
};
 
template<typename T>
void register_class(Registry &reg) {
    T dummy;
    for (auto &prop : T::properties()) {
        reg.add(prop.name, prop.getter, prop.setter);
    }
}
 
// 使用
struct Enemy {
    float speed;
    int hp;
 
    static std::vector<Property> properties() {
        return {
            {"speed",  [](void *obj) -> void* { return &static_cast<Enemy*>(obj)->speed; }},
            {"hp",     [](void *obj) -> void* { return &static_cast<Enemy*>(obj)->hp; }}
        };
    }
};

热重载

热重载流程:
    1. 文件监控线程: inotify / FileSystemWatcher 检测脚本文件变化
    2. 检测到变化 → 向主线程 post 事件 "script_changed: path/to/script.lua"
    3. 主线程在安全时机 (帧末尾):
       a. 暂停执行该脚本关联的 System
       b. 保存脚本管理对象的状态 (序列化关键数据)
       c. 卸载旧脚本模块 (lua package.loaded[mod] = nil)
       d. 重新加载脚本文件
       e. 用保存的数据恢复状态 (反序列化)
       f. 恢复 System 执行
    4. 若加载失败 → 保留旧版本, 打印错误, 不崩溃