Lua 嵌入游戏脚本
c #include "lua.h" #include "lualib.h" #include "lauxlib.h"
int main(void) { // 创建新的 Lua 虚拟机 lua_State* L = luaL_newstate(); // 打开标准库(数学、字符串等) luaL_openlibs(L);
// 执行一行 Lua 代码
luaL_dostring(L, "print('Hello from Lua!')");
// 关闭虚拟机
lua_close(L);
return 0;
}
编译时链接 `-llua`(或直接包含源码),运行后应在控制台看到 Hello from Lua!。这就完成了最基础的嵌入。
---
## Lua 与 C 之间的数据高速公路
### 核心概念:虚拟栈
Lua 和 C 之间所有数据传递都通过一个抽象的 **栈** 完成。栈索引遵循 Lua 约定:栈顶为 -1,栈底为 1。常见的压入与取出函数:
```c
lua_pushnumber(L, 3.14); // 压入数字
lua_pushstring(L, "NPC"); // 压入字符串
lua_pushboolean(L, 1); // 压入布尔值
lua_pushnil(L); // 压入 nil
从栈中取出值:
double hp = lua_tonumber(L, -1);
const char* name = lua_tostring(L, 1);
int is_ally = lua_toboolean(L, 2);
类型检查务必先行,否则可能崩溃:
if (lua_isnumber(L, 1)) {
// 安全地使用 lua_tonumber
}
从 C 调用 Lua 函数
调用全局 Lua 函数
假设你在 Lua 脚本中定义了:
function onDamage(target, damage)
print(target..' 受到 '..damage..' 点伤害')
return target .. '_hurt'
end
C 侧调用步骤:
// 1. 将函数名压栈
lua_getglobal(L, "onDamage");
// 2. 压入参数
lua_pushstring(L, "Player");
lua_pushnumber(L, 30);
// 3. 调用函数(2个参数,1个返回值)
if (lua_pcall(L, 2, 1, 0) == LUA_OK) {
// 4. 获取返回值
const char* result = lua_tostring(L, -1);
// 处理 result...
lua_pop(L, 1); // 清除返回值
} else {
// 处理调用错误
const char* err = lua_tostring(L, -1);
printf("Lua error: %s\n", err);
lua_pop(L, 1);
}
要点:lua_pcall 会在成功时将栈清理干净并压入返回值;失败时压入错误信息。务必检查返回值。
从 Lua 调用 C 函数(注册宿主 API)
游戏引擎需要向脚本暴露接口,比如 spawnEnemy、moveCamera。这些 C 函数必须遵守签名:
int MyCFunction(lua_State* L);
函数从栈读取参数,并将返回值压栈,最后返回返回值个数。
示例:实现一个 C 函数 giveItem
int lua_giveItem(lua_State* L) {
const char* itemName = luaL_checkstring(L, 1); // 参数1:物品名
int count = luaL_checkinteger(L, 2); // 参数2:数量
// 实际游戏逻辑......
// bool success = Inventory::Add(itemName, count);
// 返回两个值:是否成功,以及提示信息
lua_pushboolean(L, 1);
lua_pushfstring(L, "获得了 %d 个%s", count, itemName);
return 2; // 表示压入2个返回值
}
注册该函数到 Lua 全局环境:
lua_register(L, "giveItem", lua_giveItem);
现在 Lua 里就可以直接调用 giveItem("sword", 5),就像内置函数一样。
更优雅的注册方式:库表
使用 luaL_Reg 数组一次性注册多个函数:
static const luaL_Reg gameLib[] = {
{"giveItem", lua_giveItem},
{"spawnEnemy", lua_spawnEnemy},
{"getPlayerX", lua_getPlayerX},
{NULL, NULL}
};
int luaopen_game(lua_State* L) {
luaL_newlib(L, gameLib);
return 1;
}
然后在 Lua 中加载:local game = require("game"),使用 game.spawnEnemy(...)。这样命名空间更干净。
管理脚本生命周期与热重载
加载并执行 Lua 文件
不要频繁使用 luaL_dostring,应将逻辑放在 .lua 文件中:
if (luaL_loadfile(L, "scripts/main.lua") || lua_pcall(L, 0, 0, 0)) {
fprintf(stderr, "无法加载脚本: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
luaL_loadfile 将编译后的函数压栈,lua_pcall 执行它。你可以缓存编译后的函数实现热重载:
// 首次加载
luaL_loadfile(L, "scripts/player.lua");
int funcRef = luaL_ref(L, LUA_REGISTRYINDEX); // 保存引用
// 重载:释放旧引用,重新编译并更新引用
luaL_unref(L, LUA_REGISTRYINDEX, funcRef);
luaL_loadfile(L, "scripts/player.lua"); // 发生文件变动时重新加载
funcRef = luaL_ref(L, LUA_REGISTRYINDEX);
// 执行该脚本
lua_rawgeti(L, LUA_REGISTRYINDEX, funcRef);
lua_pcall(L, 0, 0, 0);
通过注册表索引,你可以在游戏运行时替换脚本逻辑,而不必重启进程。
游戏脚本实战模式
模式一:事件驱动脚本
常用于 UI 响应、战斗流程。宿主导入 EventDispatcher.lua,C 侧通过 lua_getglobal 调用 dispatch(eventName, ...)。
Lua 侧:
Event.listen("on_boss_killed", function(bossId)
giveItem("legendary_sword", 1)
print("世界Boss被击败!")
end)
模式二:实体行为脚本
将怪物 AI 编写为 Lua 表,内含 update(dt) 和 on_hit(damage) 方法。
C 侧每帧遍历怪物实体,按需调用其 Lua 行为表:
// 假设每个怪物实体持有一个 Lua 表引用
lua_rawgeti(L, LUA_REGISTRYINDEX, monster->scriptRef);
lua_getfield(L, -1, "update");
lua_pushvalue(L, -2); // 表作为 self
lua_pushnumber(L, deltaTime);
lua_pcall(L, 2, 0, 0);
这样策划可以轻松调整小怪的巡逻路径或技能释放条件。
内存管理与性能优化
垃圾收集控制
Lua 使用自动内存管理,但游戏需要可预测的帧率。建议:
- 每帧手动执行一步增量 GC:
lua_gc(L, LUA_GCSTEP, 10); - 在加载关卡、切换场景时主动调用完整 GC:
lua_gc(L, LUA_GCCOLLECT, 0); - 避免在 Update 循环中创建大量临时表/闭包,尽量复用。
避免栈泄漏
每次与 C API 交互后,栈平衡至关重要。使用辅助宏或 RAII 包装类(C++ 项目)来管理栈清理。
int top = lua_gettop(L);
// ... 进行操作 ...
lua_settop(L, top); // 恢复栈顶
数值类型注意
默认 Lua 使用 double 存储所有数字。如果游戏需要大量整数运算,可以考虑编译 Lua 时启用 LUA_32BITS 或将关键循环逻辑留在 C/C++ 侧。
调试与错误处理
捕获脚本错误
所有可能出错的调用都应包裹在 lua_pcall 中,并记录错误的栈回溯:
lua_getglobal(L, "debug");
lua_getfield(L, -1, "traceback");
lua_remove(L, -2); // 移走 debug 表
int errfunc = lua_gettop(L);
// 调用实际函数时,errfunc 作为错误处理函数索引
lua_pcall(L, nargs, nresults, errfunc);