首页 > Personal > Game > 游戏引擎设计系列4-脚本引擎(Lua)
2018
09-25

游戏引擎设计系列4-脚本引擎(Lua)

上文回顾了反射系统,有了反射系统,脚本引擎实现起来就简单很多,不需要类似LuaBind的机制就可以实现。

Lua和C交互需要使用下列3个伪索引
LUA_REGISTRYINDEX // 注册表,只有C代码可以访问的全局表,使用时要注意Key,一般使用C地址以light userdata的形式做键。
LUA_ENVIRONINDEX // 当前模块的环境表,同lua_getfenv,lua_setfenv,类似于Lua中的_ENV, 相对于LUA_REGISTRYINDEX,不需要共享数据时,更推荐使用这个表,只作用于当前模块,可以增加安全性。
LUA_GLOBALSINDEX // 全局表,类似于Lua中的_G

同时C函数创建出来时,可能会把一些值和这个函数关联在一起,就是C closure,这些被关联起来的值叫upvalue,可以在函数被调用的时候访问到,upvalue都被放在指定的伪索引处,第一个关联到函数的值放在lua_upvalueindex(1)位置,依次类推。

定义LuaState,没有LuaState对应Lua中的一个lua_State,里面比较重要的内容如下,
typedef int (*lua_CFunction) (lua_State *L); // lua的C函数定义
typedef struct luaL_Reg {
const char *name;
lua_CFunction func;
} luaL_Reg; // lua的C函数注册结构

void OpenLibs() // 调用Lua的luaL_openlibs,同时使用lua_register注册相应的自定义函数,同时注册相关全局
lua_register(L, “print”, luaS_print); // L为lua_State,luaS_print为自定的C函数
PushCFunction(lua_ptr_typeof); SetGlobal(“ptr_typeof”); //在LUA_GLOBALSINDEX注册,获取TypeInfo的接口,
可以使用这个接口在Lua中获取对应的类型,如果定义了Ctor可以在Lua中创建对象

全局变量和函数
static const char * CreatePtrMetatableLuaCode =
“local __ptr_gc, __ptr_tostring, __ptr_eq, __ptr_cast, __ptr_call = …\n”
“return function(typename)\n”
” local function error_get(obj, index) error(tostring(index)..\” is not a getter of \”..tostring(typename), 3) end\n”
” local function error_set(obj, value, index) error(tostring(index)..\” is not a setter of \”..tostring(typename), 3) end\n”

” local mt, getters, setters = {}, {}, {}”
” mt.type = typename\n”
” mt.getters = getters\n”
” mt.setters = setters\n”
” mt.__index = function (obj, index) return (getters[index] or error_get)(obj, index) end\n”
” mt.__newindex = function (obj, index, value) return (setters[index] or error_set)(obj, value, index) end\n”
” mt.__gc = __ptr_gc\n”
” mt.__tostring = __ptr_tostring\n”
” mt.__eq = __ptr_eq\n”
” mt.__cast = __ptr_cast\n”
” mt.__call = __ptr_call\n”
” return mt\n”
“end\n”;

/// 用于获取引擎类型的MetaTable
void GetTypeMetatable(LuaState *L, const Core::Identifier & interfaceType) {
L->PushLightUserData((void*)interfaceType.Str()); // 以类型指针作为键值
L->GetTable(Lua::REGISTRYINDEX); // 因为需要数据共享,注册在注册表中
if (!L->IsNil(-1)) return; // 如果存在,直接返回
L->Pop(1); // pop掉nil
L->PushLightUserData((void*)CreatePtrMetatableLuaCode); // 以上面的Lua字符串指针作为键值
L->GetTable(Lua::REGISTRYINDEX); // 获取通用的MetaTable
if (L->IsNil(-1)) { 如果还没有通用的MetaTable,创建一个
L->Pop(1); // pop掉nil
L->PushCFunction(&Ptr_Meta_GC); // 作为CreatePtrMetatableLuaCode中__ptr_gc的C函数,lua在gc时会调用,如果是UserData,释放相应的SharedPtr
L->PushCFunction(&Ptr_Meta_ToString); // 作为CreatePtrMetatableLuaCode中__ptr_tostring的C函数
L->PushCFunction(&Ptr_Meta_Equals); // 作为CreatePtrMetatableLuaCode中__ptr_eq的C函数
L->PushCFunction(&Ptr_Meta_Cast); // 作为CreatePtrMetatableLuaCode中__ptr_cast的C函数
L->PushCFunction(&Ptr_Meta_Call); // 作为CreatePtrMetatableLuaCode中__ptr_call的C函数
const char * err = L->DoBuffer(CreatePtrMetatableLuaCode, strlen(CreatePtrMetatableLuaCode), “=ptr_metatable”, 5, 1);
L->PushLightUserData((void*)CreatePtrMetatableLuaCode);
L->PushValue(-2);
L->SetTable(Lua::REGISTRYINDEX);
}
L->PushIdentifier(interfaceType);
if (L->DoCall(1, 1)) {
const char * err = L->ToString(1);
PDE_ASSERT(0, “Error in create metatable”);
}
temp_ptr(TypeInfo) typeInfo = TypeInfo::FromName(interfaceType);
if (typeInfo){
/// 通过反射系统,在MetaTable中注册该类型相应的函数、属性和变量
}
/// 标识这个Table为Ptr
L->PushValue(-1);
L->PushBoolean(true);
L->SetTable(Lua::REGISTRYINDEX);
/// 把这个MetaTable通过类型键值加入到注册表中
L->PushLightUserData((void*)interfaceType.Str());
L->PushValue(-2);
L->SetTable(Lua::REGISTRYINDEX);
}

/// push引擎数据到LuaState中
void LuaState::PushPtr(by_ptr(void) ptr, const Core::Identifier & interfaceType) {
temp_ptr(TypeInfo) typeInfo = TypeInfo::FromName(interfaceType);
if (typeInfo){
/// 根据类型push相应数据
/// 基本类型直接push,如int、bool等
lua_pushboolean(LL, b)
/// 枚举,如果在引擎中注册过,就push相应字符串,如果没有就push整型值
/// 如果是智能指针Ptr,获取相应MetaTable并设置
shared_ptr(void) * userdata = static_cast<shared_ptr(void)*>(NewUserData(sizeof(ptr))); // 创建UserData
if (userdata) {
new(userdata) shared_ptr(void)(ptr); // 把智能指针赋值给UserData
GetTypeMetatable(this, interfaceType);
SetMetatable(-2);
}
/// 如果都不是,push nil
}
}

/// 将Lua堆栈中的数据转换为引擎数据
shared_ptr(void) LuaState::ToPtr(int idx, const Core::Identifier & wantedType) {
temp_ptr(PdeTypeInfo) typeInfo = PdeTypeInfo::FromName(wantedType);
if (typeInfo) {
/// 根据类型获取相应数据
/// 基本类型直接转换,如int、bool等
lua_toboolean(LL, idx)
/// 枚举,如果时整型直接转换,如果时字符串通过EnumItemInfo转换
/// 如果是智能指针Ptr,lua_touserdata(LL, idx)获取指针并调用Ptr的QueryInterface转换相应类型
}
}

以上就是核心的Lua和引擎交互的代码说明。这种实现方式,只是把所有在c++提供了反射信息的对象和Lua交互,对于没有反射还需要交互的内容就需要特殊处理了,如果结合越深使用的C++特性越多,耦合的代码就会越复杂,而且Lua是作为解释型ByteCode运行时执行的,相应的效率也会有损失,虽然Lua后续也可以支持JIT。后面我们还尝试了使用google的V8代替Lua,因为V8支持JIT。
下面分析一下其他使用脚本的引擎,
Unity使用C#作为脚本语言,C#编译为IL,在通过嵌入的Mono VM在运行时通过JIT(IOS使用AOT)运行IL,因为Mono的跨平台性,上层使用的C#也是跨平台的,至于Unity底层的C++代码还是需要处理跨平台问题。C++可以通过Mono接口获取C#的类(MonoClass)、对象(MonoObject)、和方法(MonoMethod),C#同样可以通过C++的声明直接访问C++的接口(PInvoke),C#的主要类型MonoBehaviour就是通过C++的管理器调用C#的函数实现Start、Update等函数的运行的。对于IOS环境的AOT,由于不能实现动态加载代码,可以借用Mono的Cecil来注入IL来实现C#的Hotfix。同时Unity还有另外一种机制,通过IL2Cpp将C#的IL转化为c++代码,在编译为平台相关的Native代码,最后在运行时通过IL2CPP VM管理代码的GC和多线程等问题,因为使用C++作为编译语言,可以根据不同平台的编译器进行优化,效率比使用Mono有一定的提升,但是相当于放弃了JIT的特性,所有平台都是用AOT来运行。

Unreal,在4中取消了UScript,完全使用c++开发,同时提供了GC和反射系统,就像他们自己说的,因为需要更多的提供c++功能到脚本,作为减少开发复杂度的沙盒存在的脚本复杂度和c++没有什么区别了,同时还增加了相互交互的复杂性,这样脚本已经没有存在的需求了。所以,他们舍弃了脚本,以优化的c++作为开发语言,同时提供了Kismet演变来的Blueprint作为可视化脚本给非程序员使用(个人觉得确实使用方便,但是如果复杂的逻辑不建议使用,可能几行代码的问题,需要搞得非常复杂)。虽然按这种实现机制,绑定脚本并不复杂,而且已经有很多免费的实现方案了,但是还是建议如果使用Unreal4还是使用c++开发,如果实在需要可以使用Blueprint,这可能也是开发团队的设计初衷。这个设计思路带来了另外一个问题,引擎只有开源一条路了。

C另外需要提一下的是Node.js,底层使用c,上层使用google的v8(JavaScript),c用来实现最核心的代码,其他的大部分功能都使用javascript实现,包括大部分库也都是使用脚本实现的,不过即便这样也摆脱不了脚本和底层c的交互,这样看来可能Unreal4的选择会更纯粹一些。

几个设计方案。我个人可能更喜欢Unreal4这种方式,但是虽然是优化过的c++,c++相比这些更高级的语言如C#来说,不论从开发难度还是开发效率都是有很大差距的。所以总的来说各有优势吧。

最后编辑:
作者:wy182000
这个作者貌似有点懒,什么都没有留下。