脚本引擎

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. 若加载失败 → 保留旧版本, 打印错误, 不崩溃