Lua 嵌入游戏脚本

FreeGuideOnline 最新 2026-07-09

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)

游戏引擎需要向脚本暴露接口,比如 spawnEnemymoveCamera。这些 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);