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