Featured image of post Sol2:在C++中调用Lua脚本

Sol2:在C++中调用Lua脚本

Lua脚本学习

Sol2:在C++中调用Lua脚本

本文档记录了在 C++ 代码中使用 sol2 嵌入 Lua 脚本时的一些个人理解。


向Lua中注册自定义用户类型

先看一段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
sol::state lua;
lua.new_usertype<Type>("TypeName", 
    // 类型构造函数
    sol::constructors<ConstructFunc1, ConstructFunc2, ...>
    // 或使用:sol::no_constructor 来禁用 TypeName.new()

    // 成员变量
    "member1", Type.valueptr1,
    "member2", Type.valueptr2,

    // 元方法
    sol::meta_function::addition, AddFun1,
    sol::meta_function::substraction, SubFun1,
    sol::meta_function::multiplication, MultiplicationFun1,

    // 成员方法
    "memberFunc1", MemberFunc1(),
    "memberFunc2", [](Type& self, /* args... */) {  // 对应成员方法的第一个参数
        // func...
    },
);

这段代码用来向lua脚本中注册一个c++类型,支持一些基本的元方法重载(比如四则运算、比大小、迭代(pairipair)等)。

[!Tip] 更多类型可以查看源码:types.hpp: enum class meta_function


Q1: UserType 和 Table 有什么区别?

在 sol2 中,new_usertype<T>create_named_table 都可以在 Lua 中创建一个“Table”,但它们的本质完全不同:

UserType(用户类型)

比如我现在要将glm::vec2注册到lua中:

1
2
3
4
5
lua.new_usertype<glm::vec2>("vec2",
    sol::constructors<glm::vec2(), glm::vec2(float, float)>(),
    "x", &glm::vec2::x,
    "y", &glm::vec2::y
);

Lua 中使用:

1
2
3
local pos = vec2.new(10, 20)  	  -- 创建 C++ glm::vec2 实例
print(pos.x)                   	  -- 访问成员
local sum = pos + vec2.new(5, 5)  -- 运算符重载

UserType可以看作:将一个真正的 C++ 类/结构体暴露给 Lua,在 Lua 操作时可以把他看作一个真实的 C++ 对象进行操作,自由度很高。

Table(表)

我现在要注册一个“Input”表在 Lua 中:

1
2
3
4
5
6
sol::table Input = lua.create_named_table("Input");
Input["KeyCode"] = lua.create_table_with(
    "W", KeyCode::W,
    "A", KeyCode::A
);
Input.set_function("IsKeyPressed", &Input::IsKeyPressed);

他就只是一个 Lua 脚本中的Table,与原生C++没有任何关系。

Lua 中使用:

1
2
3
if Input.IsKeyPressed(Input.KeyCode.W) then
    -- 移动逻辑
end

Q2: 探索 sol2 new_usertype 实现方案

注册glm::vec2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
lua.new_usertype<glm::vec2>("vec2",
    sol::constructors<glm::vec2(), glm::vec2(float, float)>(),
    "x", &glm::vec2::x,
    "y", &glm::vec2::y,
    sol::meta_function::addition, [](const glm::vec2& a, const glm::vec2& b) { return a + b; },
    sol::meta_function::subtraction, [](const glm::vec2& a, const glm::vec2& b) { return a - b; },
    sol::meta_function::multiplication, sol::overload(  // 函数重载
        [](const glm::vec2& a, float b) { return a * b; },
        [](float a, const glm::vec2& b) { return a * b; },
        [](const glm::vec2& a, const glm::vec2& b) { return a * b; }
    )
);

对于这段代码,我们可以看到,他的函数入参似乎自由度非常高,既可以设置构造函数,又可以设置成员变量,还可以做元操作的重载,一个函数的入参能够这么复杂,肯定不是一个简单的**“函数重载”**能够搞定的。


new_usertype 解析1:state_view::new_usertype

源码位置:sol/state_view.hpp

1
2
3
4
template <typename Class, typename... Args>
usertype<Class> new_usertype(Args&&... args) {
    return global.new_usertype<Class>(std::forward<Args>(args)...);
}

这里的 Args&&... args 是 C++11 的可变参数模板(Variadic Template)

  • typename... Args模板参数包,可以匹配任意数量的类型
  • Args&&... args函数参数包
  • std::forward<Args>(args)...参数包展开

...常用来做变长模板参数展开,std::forward<Args>(args)... 可以被理解为:

1
(std::forward<Arg1>(arg1), std::forward<Arg2>(arg2), std::forward<Arg3>(arg3))

放在原代码中就是:

1
return global.new_usertype<Class>((std::forward<Arg1>(arg1), std::forward<Arg2>(arg2), std::forward<Arg3>(arg3)));  // 对每个参数进行完美转发

new_usertype 解析2:basic_table_core::new_usertype

源码位置:sol/table.hpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
template <bool is_global, typename base_type>
template <typename Class, typename Key, typename Arg, typename... Args, typename>
usertype<Class> basic_table_core<is_global, base_type>::new_usertype(Key&& key, Arg&& arg, Args&&... args) {
    constexpr automagic_flags enrollment_flags = meta::any_same_v<no_construction, meta::unqualified_t<Arg>, meta::unqualified_t<Args>...>
         ? clear_flags(automagic_flags::all, automagic_flags::default_constructor)
         : automagic_flags::all;
    constant_automagic_enrollments<enrollment_flags> enrollments;
    enrollments.default_constructor = !detail::any_is_constructor_v<Arg, Args...>;
    enrollments.destructor = !detail::any_is_destructor_v<Arg, Args...>;
    usertype<Class> ut = this->new_usertype<Class>(std::forward<Key>(key), std::move(enrollments));
    static_assert(sizeof...(Args) % 2 == static_cast<std::size_t>(!detail::any_is_constructor_v<Arg>),
         "you must pass an even number of arguments to new_usertype after first passing a constructor");
    if constexpr (detail::any_is_constructor_v<Arg>) {
        ut.set(meta_function::construct, std::forward<Arg>(arg));
        ut.tuple_set(std::make_index_sequence<(sizeof...(Args)) / 2>(), std::forward_as_tuple(std::forward<Args>(args)...));
    }
    else {
        ut.tuple_set(std::make_index_sequence<(sizeof...(Args) + 1) / 2>(), std::forward_as_tuple(std::forward<Arg>(arg), std::forward<Args>(args)...));
    }
    return ut;
}

他这个源码看起来吓人,其实一点也不简单,但我们可以剔除一些不必要的信息,只留下我们要关注的内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
template <typename Class, typename Key, typename Arg, typename... Args, typename>
usertype<Class> basic_table_core<is_global, base_type>::new_usertype(
    Key&& key, Arg&& arg, Args&&... args) 
{
    // 0. 注册常用功能
    constexpr automagic_flags enrollment_flags = meta::any_same_v<no_construction, meta::unqualified_t<Arg>, meta::unqualified_t<Args>...>
     ? clear_flags(automagic_flags::all, automagic_flags::default_constructor)
     : automagic_flags::all;
constant_automagic_enrollments<enrollment_flags> enrollments;
enrollments.default_constructor = !detail::any_is_constructor_v<Arg, Args...>;
enrollments.destructor = !detail::any_is_destructor_v<Arg, Args...>;
    
    // 1. 创建 usertype 对象
    usertype<Class> ut = this->new_usertype<Class>(std::forward<Key>(key), std::move(enrollments));
    
    // 2. 编译期断言:参数必须是偶数(key-value 成对)
    static_assert(sizeof...(Args) % 2 == static_cast<std::size_t>(!detail::any_is_constructor_v<Arg>),
         "you must pass an even number of arguments to new_usertype after first passing a constructor");
    
    // 3. 根据第一个参数类型分发处理
    if constexpr (detail::any_is_constructor_v<Arg>) {
        // 如果 Arg 是构造器,单独处理
        ut.set(meta_function::construct, std::forward<Arg>(arg));
        // 剩余参数两两配对
        ut.tuple_set(std::make_index_sequence<(sizeof...(Args)) / 2>(), 
                     std::forward_as_tuple(std::forward<Args>(args)...));
    }
    else {
        // 否则,所有参数都两两配对
        ut.tuple_set(std::make_index_sequence<(sizeof...(Args) + 1) / 2>(), 
                     std::forward_as_tuple(std::forward<Arg>(arg), std::forward<Args>(args)...));
    }
    return ut;
}
automagic_enrollments : “自动魔法”

第一段代码:

1
2
3
4
5
6
constexpr automagic_flags enrollment_flags = meta::any_same_v<no_construction, meta::unqualified_t<Arg>, meta::unqualified_t<Args>...>
     ? clear_flags(automagic_flags::all, automagic_flags::default_constructor)
     : automagic_flags::all;
constant_automagic_enrollments<enrollment_flags> enrollments;
enrollments.default_constructor = !detail::any_is_constructor_v<Arg, Args...>;
enrollments.destructor = !detail::any_is_destructor_v<Arg, Args...>;

这段代码是 sol2 的**“自动魔法(Automagic)”系统,它会根据你的 C++ 类型自动推断并注册**一些常用功能,省去手动编写的麻烦。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// sol2 中的 automagic_flags 注册类型枚举
// sol/types.hpp
enum class automagic_flags : unsigned {
    none = 0x000u,
    default_constructor = 0x001,
    destructor = 0x002u,
    pairs_operator = 0x004u,
    to_string_operator = 0x008u,
    call_operator = 0x010u,
    less_than_operator = 0x020u,
    less_than_or_equal_to_operator = 0x040u,
    length_operator = 0x080u,
    equal_to_operator = 0x100u,
    all = default_constructor | destructor | pairs_operator | to_string_operator | call_operator | less_than_operator | less_than_or_equal_to_operator
         | length_operator | equal_to_operator
};

struct automagic_enrollments {
    bool default_constructor = true;    // 自动注册默认构造函数
    bool destructor = true;             // 自动注册析构函数
    bool pairs_operator = true;         // 自动注册 pairs() 迭代
    bool to_string_operator = true;     // 自动注册 __tostring
    bool call_operator = true;          // 自动注册 operator()
    bool less_than_operator = true;     // 自动注册 operator<
    bool less_than_or_equal_to_operator = true;  // 自动注册 operator<=
    bool length_operator = true;        // 自动注册 #(取长度)
    bool equal_to_operator = true;      // 自动注册 operator==
};

// 编译期默认值的版本
template <automagic_flags compile_time_defaults = automagic_flags::all>
struct constant_automagic_enrollments : public automagic_enrollments { };

首先,我们需要确认当前传入的参数中是否包含用户自定义的构造函数:

1
2
3
4
constexpr automagic_flags enrollment_flags = 
    meta::any_same_v<no_construction, meta::unqualified_t<Arg>, meta::unqualified_t<Args>...>
        ? clear_flags(automagic_flags::all, automagic_flags::default_constructor)
        : automagic_flags::all;

如果参数中包含 sol::no_constructor,就从所有自动功能中移除默认构造函数的自动注册,否则仍然是全部注册。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// any_same 实现方式:递归进行类型萃取
// std::integral_constant 编译期常量值推导
template <typename T, typename...>
struct any_same : std::false_type { };

template <typename T, typename U, typename... Args>
struct any_same<T, U, Args...> : std::integral_constant<bool, std::is_same<T, U>::value || any_same<T, Args...>::value> { };

template <typename T, typename... Args>
constexpr inline bool any_same_v = any_same<T, Args...>::value;

运行时调整:

1
2
enrollments.default_constructor = !detail::any_is_constructor_v<Arg, Args...>;
enrollments.destructor = !detail::any_is_destructor_v<Arg, Args...>;
  • 如果用户已经提供了构造函数(如 sol::constructors<...>),就不再自动注册默认构造函数
  • 如果用户已经提供了析构函数,就不再自动注册析构函数

这里又用到了一个模板元方法:any_is_constructor_vany_is_destructor_v,用来判断一个参数是不是sol2提供的构造函数和析构函数,比如any_is_constructor_v

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 基础模板
template <typename T>
struct is_constructor : std::false_type { };

// 特化:sol::constructors<...>
template <typename... Args>
struct is_constructor<constructors<Args...>> : std::true_type { };

// 特化:sol::no_construction
template <>
struct is_constructor<no_construction> : std::true_type { };

template <typename... Args>
using any_is_constructor = meta::any<is_constructor<meta::unqualified_t<Args>>...>;

template <typename... Args>
inline constexpr bool any_is_constructor_v = any_is_constructor<Args...>::value;

这是一个经典的类型萃取写法,用来检查参数类型是否是sol::constructorssol::no_construction;其中``meta::unqualified_tstd::remove_cv<std::remove_reference_t>`的别名,用来获取纯粹的类型。

当然这段代码在c++20中也可以更直观:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 使用变量模板 concept
template <typename T>
concept IsAnyConstructor = 
    std::same_as<std::remove_cvref_t<T>, no_construction> ||
    requires(std::remove_cvref_t<T>* p) {
        requires (
            requires { []<typename... A>(constructors<A...>*){}(p); }
        );
    };

// 使用
if constexpr (IsAnyConstructor<Arg>) {
    // ...
}

[!TIP] C++20 Concepts 的核心优势是语义化:让代码比原本的“模板元编程”更通俗易懂"如果 Arg 是一个构造器类型",而不是"如果 is_constructor<Arg>::value 为真"。


static_assert 和 成员指针
1
2
static_assert(sizeof...(Args) % 2 == static_cast<std::size_t>(!detail::any_is_constructor_v<Arg>),
     "you must pass an even number of arguments to new_usertype after first passing a constructor");

这个断言确保参数数量正确:

场景Arg 类型any_is_constructor_v!(…)Args 数量要求
有构造器constructors<...>truefalse (0)必须是偶数 (% 2 == 0)
无构造器普通 key(如 "x"falsetrue (1)必须是奇数 (% 2 == 1)

为什么无构造器时要求奇数? 因为 Arg 本身是一个 key,需要和它后面的第一个 Args 配对,所以剩余参数要是奇数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 有构造器:
new_usertype("vec2",
    constructors<...>(),   // Arg(单独处理)
    "x", &vec2::x,         // Args(2个,偶数 ✅)
    "y", &vec2::y          // Args(4个,偶数 ✅)
);

// 无构造器:
new_usertype("vec2",
    "x",        // Arg(key)
    &vec2::x,   // Args[0](value,配对 Arg)
    "y",        // Args[1](key)
    &vec2::y    // Args[2](value,配对 Args[1])
    // 共 3 个 Args,奇数 ✅
);

这里顺带提一嘴,有人可能会好奇:&vec2::x这是个什么语法,vec2又不是静态变量,为什么能对其成员取地址呢?取出来的又有什么用呢?

对于一个结构体:

1
2
3
4
5
6
7
8
struct MyStruct 
{
    int a;
    float b;
    char c;
};

auto p = &MyStruct::a;  // p: int MyStruct::*

这里的p就是一个指向该结构体成员的特定指针,你可以这样使用:

1
2
3
4
5
6
7
8
9
MyStruct obj{ 33, 42.3f, 'a' };

int MyStruct::* pm = &MyStruct::a;

int v1 = obj.*pm;      // v1 == 33
obj.*pm = 100;         // obj.a 变成 100

MyStruct* p = &obj;
int v2 = p->*pm;       // v2 == 100

new_usertype 解析3:tuple_set

源码位置:sol/usertype.hpp

1
2
3
4
5
template <std::size_t... I, typename... Args>
void tuple_set(std::index_sequence<I...>, std::tuple<Args...>&& args) {
    (void)args;
    (void)detail::swallow { 0, (this->set(std::get<I * 2>(std::move(args)), std::get<I * 2 + 1>(std::move(args))), 0)... };
}
1. 函数签名解析
1
2
template <std::size_t... I, typename... Args>
void tuple_set(std::index_sequence<I...>, std::tuple<Args...>&& args)
  • std::size_t... I - 这是一个非类型模板参数包,存储的是整数序列
  • std::index_sequence<I...> - 一个空的标签类型,用于传递索引序列
  • std::tuple<Args...> - 把所有参数打包成 tuple

调用示例:

1
2
3
4
5
// 假设经过层层处理后,调用结果如下:
ut.tuple_set(std::make_index_sequence<3>(),              // I = 0, 1, 2
             std::forward_as_tuple("x", &T::x, 
                                   "y", &T::y, 
                                   "z", &T::z));
2. Swallow

Swallow,通常指一种 “用 std::initializer_list + 逗号表达式 来展开参数包,并把返回值丢掉,只保留副作用的方案。

1
(void)detail::swallow { 0, (expression, 0)... };

在 C++17 之前,没有折叠表达式,sol2 使用了 swallow 技巧

1
2
3
4
5
6
7
8
9
// 定义 swallow 为一个 std::initializer_list<int> 类型
using swallow = std::initializer_list<int>;

// 展开过程:
swallow { 0,                                          // 保证初始化列表非空
    (this->set(std::get<0>(args), std::get<1>(args)), 0),  // I=0: 取索引0和1
    (this->set(std::get<2>(args), std::get<3>(args)), 0),  // I=1: 取索引2和3
    (this->set(std::get<4>(args), std::get<5>(args)), 0)   // I=2: 取索引4和5
};

逗号表达式 (expr, 0):

  • 执行 expr(即 this->set(...)
  • 返回 0(用于初始化列表)
  • 利用初始化列表的顺序求值保证确保按序执行(由于c++17以前,...展开只能用在特定的上下文中,比如初始化列表)

如果使用c++17的折叠表达式来实现的话:

1
2
3
4
5
template <std::size_t... I, typename... Args>
void tuple_set(std::index_sequence<I...>, std::tuple<Args...>&& args) {
    (this->set(std::get<I * 2>(std::move(args)), 
               std::get<I * 2 + 1>(std::move(args))), ...);
}

new_usertype 解析4: usertype_storage::set

源码位置:sol/usertype_storage.hpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template <typename T, typename Key, typename Value>
void usertype_storage_base::set(lua_State* L, Key&& key, Value&& value) {
    using ValueU = meta::unwrap_unqualified_t<Value>;
    using KeyU = meta::unwrap_unqualified_t<Key>;
    
    if constexpr (std::is_same_v<KeyU, call_construction>) {
        // 处理 call 构造(通过 Type() 语法调用)
        // ...
    }
    else if constexpr (std::is_same_v<KeyU, base_classes_tag>) {
        // 处理基类继承
        this->update_bases<T>(L, std::forward<Value>(value));
    }
    else if constexpr (meta::is_string_like_or_constructible<KeyU>::value 
                       || std::is_same_v<KeyU, meta_function>) {
        // 处理字符串 key 或元方法枚举
        std::string s = u_detail::make_string(std::forward<Key>(key));
        
        // 创建绑定对象
        using Binding = binding<KeyU, ValueU, T>;
        std::unique_ptr<Binding> p_binding = std::make_unique<Binding>(std::forward<Value>(value));
        
        // 存储并注册到 Lua
        this->storage.push_back(std::move(p_binding));
        // ... 注册到各个 metatable
    }
    else {
        // 处理非字符串 key(如 Lua reference)
        // ...
    }
}

到这里函数的入参基本上就都拆开了,剩下的就太多了,有空慢慢整理吧…


向Lua中注册Table

UserType 不同,Table 不绑定任何 C++ 类型,它就是一个纯粹的 Lua 表。通常用来:

  • 创建命名空间(如 InputMath
  • 注册枚举值(如 KeyCode.W
  • 提供工具函数集合
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
sol::state lua;

// 创建一个命名表
sol::table Input = lua.create_named_table("Input");

// 添加子表(枚举)
Input["KeyCode"] = lua.create_table_with(
    "W", 87,
    "A", 65,
    "S", 83,
    "D", 68
);

// 添加函数
Input.set_function("IsKeyPressed", [](int keyCode) {
    return /* 检查按键状态 */;
});

Lua 中使用:

1
2
3
if Input.IsKeyPressed(Input.KeyCode.W) then
    print("W 键被按下")
end

Q3: Table 中的函数,self 应该怎么填?

你可能看到过这样的代码:

1
2
3
table.set_function("funcName", [](sol::this_state ts, sol::optional<float> speed) {
    // ...
});

这里的问题是:Table 的函数需不需要 self 参数?

答案:取决于你在 Lua 中如何调用

情况1:用 . 调用(普通函数调用)

1
Input.IsKeyPressed(87)  -- 用点号调用

这种情况下,不需要 self,函数参数就是你传入的参数:

1
2
3
Input.set_function("IsKeyPressed", [](int keyCode) {
    return /* ... */;
});

情况2:用 : 调用(方法调用)

1
2
someTable:doSomething()  -- 用冒号调用
-- 等价于:someTable.doSomething(someTable)

这种情况下,Lua 会自动把调用者作为第一个参数传入,你需要 self

1
2
3
4
table.set_function("doSomething", [](sol::table self) {
    // self 就是 someTable
    auto name = self["name"].get<std::string>();
});

实际例子对比

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
sol::table Player = lua.create_named_table("Player");

// 静态函数风格(用 . 调用)
Player.set_function("Create", [](const std::string& name) {
    return /* 创建玩家 */;
});

// 方法风格(用 : 调用)
Player.set_function("GetName", [](sol::table self) {
    return self["name"].get<std::string>();
});
1
2
local p = Player.Create("Alice")
local name = p:GetName()

特殊参数:sol::this_state

如果你需要在函数内部访问 Lua 状态(比如创建新表),使用 sol::this_state

1
2
3
4
table.set_function("CreateChild", [](sol::this_state ts) {
    sol::state_view lua(ts);
    return lua.create_table();  // 返回一个新表
});

sol::this_state 是透明参数,Lua 调用时不需要传,sol2 会自动注入。


Q4: C++ 如何持有 Lua 返回的 Table?

比如这段代码:

1
2
3
4
5
6
7
sol::protected_function_result result = loadResult();  // 执行脚本
sol::object obj = result;

if (!obj.is<sol::table>())
    return sol::nil;

sol::table classTable = obj.as<sol::table>();  // 持有脚本返回的表

执行流程如下:

  1. 执行 Lua 脚本loadResult() 执行加载的脚本
  2. 获取返回值:Lua 脚本可以 return 一个值,这个值被包装成 sol::object
  3. 类型检查obj.is<sol::table>() 检查返回值是否是表
  4. 类型转换obj.as<sol::table>() 将其转为 sol::table

假设你的 Lua 脚本是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- Player.lua
local Player = {
    name = "Unknown",
    health = 100,
    
    TakeDamage = function(self, amount)
        self.health = self.health - amount
    end
}

return Player

C++ 加载并持有这个表后,就可以操作它:

1
2
3
4
5
6
7
8
sol::table Player = lua.script_file("Player.lua");  // 加载并获取返回值

// 读取字段
std::string name = Player["name"];
int health = Player["health"];

// 调用方法
Player["TakeDamage"](Player, 50);  // 传入 self

sol::object 的作用

sol::object 是 sol2 的通用容器,可以持有任何 Lua 值:

1
2
3
4
5
6
7
8
sol::object value = lua["someValue"];

if (value.is<int>())
    int n = value.as<int>();
else if (value.is<std::string>())
    std::string s = value.as<std::string>();
else if (value.is<sol::table>())
    sol::table t = value.as<sol::table>();

Q5: 什么是"向 Lua 注入数据"?

假设有如下代码:

1
lsc.ScriptInstance["entity"] = Entity{ e, this };

假设 lsc.ScriptInstance 是一个 Lua 表(比如上面加载的 Player 表),这行代码就是:

在这个表里添加一个新字段 entity,值是一个 C++ 对象

Lua 脚本需要访问 C++ 的数据(比如当前实体),但 Lua 不能直接调用 C++ 代码。通过"注入",我们把数据塞进 Lua 表里,脚本就能访问了:

1
2
3
4
5
6
function Player:Update(dt)
    -- self.entity 就是 C++ 注入的 Entity 对象
    local pos = self.entity:GetPosition()
    pos.x = pos.x + self.speed * dt
    self.entity:SetPosition(pos)
end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
flowchart TD
    subgraph CPP["C++ 端"]
        E["Entity obj"]
        T["Transform"]
        P["Physics"]
    end

    subgraph LUA["Lua Table"]
        entity["entity = obj"]
        health["health = 100"]
        update["Update = func"]
    end

    subgraph CALL["Lua 脚本调用"]
        script["self.entity:..."]
    end

    CPP -->|"1. 注入"| entity
    LUA -->|"2. Lua 访问"| CALL

Lua 嵌入完整流程

结合我的实际代码,梳理完整的 Lua 脚本嵌入流程。

整体架构

1
2
3
4
5
6
7
8
flowchart TD
    subgraph Engine["Yuicy Engine"]
        LSE["LuaScriptEngine<br/>(管理sol2)"] --> LB["LuaBindings<br/>(类型注册)"]
        LSE --> LSC["LuaScriptComponent<br/>(脚本组件)"]
        Scene["Scene.cpp<br/>(生命周期管理)"] --> LSC
    end

    Engine --> Script["player_controller.lua"]

嵌入流程:

1. 初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// LuaScriptEngine.cpp
sol::state* LuaScriptEngine::s_luaState = nullptr;  // 一个 sol2 的Lua状态机

void LuaScriptEngine::Init()
{
    s_luaState = new sol::state();
    // 启用 Lua 标准库
    s_luaState->open_libraries(
        sol::lib::base,
        sol::lib::math,
        sol::lib::string,
        sol::lib::table,
        sol::lib::os,
        sol::lib::io,
        sol::lib::package
    );
    // 注册 C++ 绑定
    RegisterBindings();
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// LuaBindings.cpp
// 类型注册
void RegisterAll(sol::state& lua)
{
    RegisterLog(lua);         // 日志函数
    RegisterMath(lua);        // Vec2、Vec3、Vec4
    RegisterInput(lua);       // 输入相关
    RegisterComponents(lua);  // 组件
    RegisterEntity(lua);      // Entity
    RegisterScene(lua);       // Scene
}
注册数学类型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
lua.new_usertype<glm::vec2>("Vec2",
    sol::constructors<glm::vec2(), glm::vec2(float), glm::vec2(float, float)>(),
    "x", &glm::vec2::x,
    "y", &glm::vec2::y,
    sol::meta_function::addition, [](const glm::vec2& a, const glm::vec2& b) { return a + b; },
    sol::meta_function::subtraction, [](const glm::vec2& a, const glm::vec2& b) { return a - b; },
    sol::meta_function::multiplication, sol::overload(
        [](const glm::vec2& a, float b) { return a * b; },
        [](float a, const glm::vec2& b) { return a * b; }
    )
);
// lua.new_usertype<glm::vec2>("Vec3")...
// lua.new_usertype<glm::vec2>("Vec4")...
注册输入函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Input
lua.new_usertype<Input>("Input",
    sol::no_constructor,
    "IsKeyPressed", [](KeyCode key) { return Input::IsKeyPressed(key); },
    "GetMousePosition", []() {
        auto [x, y] = Input::GetMousePosition();
        return std::make_tuple(x, y);
    }
);

// Key Table
sol::table keyTable = lua.create_named_table("Key");
keyTable["W"] = Key::W;
keyTable["A"] = Key::A;
keyTable["S"] = Key::S;
keyTable["D"] = Key::D;
// ...
注册常用 Entity 方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
lua.new_usertype<Entity>("Entity",
    sol::no_constructor,
    "GetTransform", [](Entity& e) -> TransformComponent& {
        return e.GetComponent<TransformComponent>();
    },
    "HasTransform", [](Entity& e) -> bool {
        return e.HasComponent<TransformComponent>();
    },
    "GetSprite", [](Entity& e) -> SpriteRendererComponent& {
        return e.GetComponent<SpriteRendererComponent>();
    },
    "GetRigidbody", [](Entity& e) -> Rigidbody2DComponent& {
        return e.GetComponent<Rigidbody2DComponent>();
    },
    "IsValid", [](Entity& e) -> bool {
        return (bool)e;
    }
    // ...
);
注册 Scene 相关函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
sol::table sceneTable = lua.create_named_table("Scene");

// 查找实体(需要传入一个 self.entity 实体 来获取 Scene 引用)
sceneTable.set_function("FindEntityByName", [](Entity& self, const std::string& name) -> Entity {
    Scene* scene = self.GetScene();
    if (scene)
        return scene->FindEntityByName(name);
    return Entity{};
});

// 创建投掷物
// sol::optional 可选函数参数
sceneTable.set_function("CreateProjectile", [](Entity& self, float x, float y, float dirX, float dirY, 
    sol::optional<float> speed, sol::optional<float> lifetime, ...) -> Entity {
    Scene* scene = self.GetScene();
    if (scene)
    {
        ProjectileConfig config;
        config.speed = speed.value_or(15.0f);
        // ...
        return scene->CreateProjectile({ x, y }, { dirX, dirY }, config);  // 在场景中创建一个投掷物
    }
    return Entity{};
});
// ...

2. 定义脚本组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct LuaScriptComponent
{
    std::string ScriptPath;           // 脚本路径
    
    sol::table ScriptInstance;        // Lua 表实例
    // 通过实例查找回调函数
    sol::function OnCreateFunc;       
    sol::function OnUpdateFunc;
    sol::function OnDestroyFunc;
    sol::function OnCollisionEnterFunc;
    sol::function OnCollisionExitFunc;
    sol::function OnTriggerEnterFunc;
    sol::function OnTriggerExitFunc;

    bool IsLoaded = false;
};

3. 加载脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bool LuaScriptEngine::LoadScript(const std::string& filepath)
{
    // 检查缓存,是否未加载过
    if (s_scriptCache.find(filepath) != s_scriptCache.end())
        return true;

    // 读取文件
    std::ifstream file(filepath);
    std::stringstream buffer;
    buffer << file.rdbuf();
    std::string scriptContent = buffer.str();

    // 编译脚本
    sol::load_result loadResult = s_luaState->load(scriptContent, filepath);
    if (!loadResult.valid())
    {
        sol::error err = loadResult;
        YUICY_CORE_ERROR("Failed to load script: {}", err.what());
        return false;
    }

    // 缓存编译结果
    s_scriptCache[filepath] = std::move(loadResult);
    return true;
}

4. 创建脚本实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
sol::table LuaScriptEngine::CreateScriptInstance(const std::string& filepath)
{
    if (!LoadScript(filepath))
        return sol::nil;

    auto it = s_scriptCache.find(filepath);
    
    // 执行脚本,获取返回值
    sol::protected_function_result result = it->second();
    
    sol::object obj = result;
    if (!obj.is<sol::table>())
    {
        YUICY_CORE_ERROR("Script did not return a table");
        return sol::nil;
    }

    // 复制脚本表(创建独立实例,让每个实体独享一份数据)
    sol::table classTable = obj.as<sol::table>();
    sol::table instance = s_luaState->create_table();
    
    for (auto& pair : classTable)
    {
        instance[pair.first] = pair.second;
    }

    return instance;
}

5. 初始化脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 场景启动时
void Scene::InitializeLuaScripts()
{
    auto view = m_Registry.view<LuaScriptComponent>();
    for (auto e : view)
    {
        auto& lsc = view.get<LuaScriptComponent>(e);
        
        if (!lsc.ScriptPath.empty() && !lsc.IsLoaded)
        {
            // 创建脚本实例
            lsc.ScriptInstance = LuaScriptEngine::CreateScriptInstance(lsc.ScriptPath);
            
            if (lsc.ScriptInstance.valid())
            {
                lsc.IsLoaded = true;

                // 注入 Entity 对象
                lsc.ScriptInstance["entity"] = Entity{ e, this };

                // 缓存回调函数
                lsc.OnCreateFunc = lsc.ScriptInstance["OnCreate"];
                lsc.OnUpdateFunc = lsc.ScriptInstance["OnUpdate"];
                lsc.OnDestroyFunc = lsc.ScriptInstance["OnDestroy"];
                lsc.OnCollisionEnterFunc = lsc.ScriptInstance["OnCollisionEnter"];
                // ...

                // 调用 OnCreate
                if (lsc.OnCreateFunc.valid())
                    lsc.OnCreateFunc(lsc.ScriptInstance);  // 传入 self
            }
        }
    }
}

6. 每帧更新

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void Scene::UpdateLuaScripts(Timestep ts)
{
    auto view = m_Registry.view<LuaScriptComponent>();
    for (auto e : view)
    {
        auto& lsc = view.get<LuaScriptComponent>(e);
        
        // 调用 OnUpdate(self, dt)
        if (lsc.IsLoaded && lsc.OnUpdateFunc.valid())
        {
            auto result = lsc.OnUpdateFunc(lsc.ScriptInstance, (float)ts);
            if (!result.valid())
            {
                sol::error err = result;
                // error
            }
        }
    }
}

7. 碰撞回调

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void Scene::ProcessLuaCollisionCallbacks()
{
    for (const auto& contact : m_ContactListener->GetBeginContacts())
    {
        Entity entityA = { (entt::entity)(uintptr_t)contact.EntityA, this };
        Entity entityB = { (entt::entity)(uintptr_t)contact.EntityB, this };

        if (entityA.HasComponent<LuaScriptComponent>())
        {
            auto& lsc = entityA.GetComponent<LuaScriptComponent>();
            
            if (contact.IsSensorA || contact.IsSensorB)
            {
                // 触发器回调
                if (lsc.OnTriggerEnterFunc.valid())
                    lsc.OnTriggerEnterFunc(lsc.ScriptInstance, entityB);
            }
            else
            {
                // 碰撞回调
                if (lsc.OnCollisionEnterFunc.valid())
                    lsc.OnCollisionEnterFunc(lsc.ScriptInstance, entityB);
            }
        }
        // entityB 同理...
    }
}

Lua 脚本示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
local PlayerController = {}

function PlayerController:OnCreate()
    print("PlayerController created!")
    
    self.speed = 3.0
    self.jumpForce = 6.0
    self.facingRight = true
    self.groundContacts = 0
end

function PlayerController:OnUpdate(dt)
    if not self.entity:IsValid() then
        return
    end

    local rb = self.entity:GetRigidbody()
    local vel = rb:GetLinearVelocity()
    local vx = 0
    
    -- 水平移动
    if Input.IsKeyPressed(Key.A) then
        vx = -self.speed
        self.facingRight = false
    end
    if Input.IsKeyPressed(Key.D) then
        vx = self.speed
        self.facingRight = true
    end
    
    -- 跳跃
    local vy = vel.y
    if self.groundContacts > 0 and Input.IsKeyPressed(Key.W) then
        vy = self.jumpForce
    end
    
    rb:SetLinearVelocity(vx, vy)
    
    -- 翻转
    local sprite = self.entity:GetSprite()
    sprite.FlipX = self.facingRight
end

function PlayerController:OnCollisionEnter(other)
    if other:IsValid() then
        local tag = other:GetTag()
        if string.find(tag, "Ground") then
            self.groundContacts = self.groundContacts + 1
        end
    end
end

function PlayerController:OnCollisionExit(other)
    if other:IsValid() then
        local tag = other:GetTag()
        if string.find(tag, "Ground") then
            self.groundContacts = self.groundContacts - 1
        end
    end
end

return PlayerController

执行流程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
flowchart TD
    A["Application 启动"] --> B

    subgraph B["LuaScriptEngine::Init()"]
        B1["创建 sol::state"]
        B2["打开标准库"]
        B3["注册 C++ 类型,常用Table"]
    end

    B --> C

    subgraph C["Scene::OnRuntimeStart()"]
        C1["InitializeLuaScripts()"]
        C2["CreateScriptInstance() 加载脚本"]
        C3["注入 entity 对象"]
        C4["缓存回调函数"]
        C5["调用 OnCreate(self)"]
    end

    C --> D

    subgraph D["游戏循环"]
        D1["UpdateLuaScripts(ts) → OnUpdate(self, dt)"]
        D2["物理模拟"]
        D4["碰撞、触发器回调"]
    end

    D --> E

    subgraph E["Scene::OnRuntimeStop()"]
        E1["DestroyLuaScripts()"]
        E2["调用 OnDestroy(self)"]
    end
Licensed under CC BY-NC-SA 4.0
最后更新于 Feb 02, 2026 22:00 CST
comments powered by Disqus

本博客已稳定运行 小时 分钟
共发表 7 篇文章 · 总计 27.20 k 字
本站总访问量