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