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++类型,支持一些基本的元方法重载(比如四则运算、比大小、迭代(pair或ipair)等)。
[!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_v、any_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::constructors或sol::no_construction;其中``meta::unqualified_t是std::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<...> | true | false (0) | 必须是偶数 (% 2 == 0) |
| 无构造器 | 普通 key(如 "x") | false | true (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 表。通常用来:
- 创建命名空间(如
Input、Math) - 注册枚举值(如
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>(); // 持有脚本返回的表
|
执行流程如下:
- 执行 Lua 脚本:
loadResult() 执行加载的脚本 - 获取返回值:Lua 脚本可以
return 一个值,这个值被包装成 sol::object - 类型检查:
obj.is<sol::table>() 检查返回值是否是表 - 类型转换:
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
|