unlua-unreal 是什么
UnLua是虚幻引擎4下的特性丰富且高效的脚本解决方案,由腾讯G6团队与Epic Games China团队共同打造。它的特色在于利用引擎的反射机制按需动态导出,无需大量胶水代码;完备的静态导出功能,用于导出非反射系统的类、函数、枚举;可以覆写(override)所有'BlueprintEvent'、Replication Notify、Animation Notify、Input Event,通过Lua来扩展C++、Blueprint;可以替换线上系统原有Blueprint逻辑。
G6 Team : G6是腾讯GCloud体系下,致力于游戏框架搭建,提供游戏云服务的部门。
unlua-unreal 有什么功能
Unlua原理及流程介绍:
一.Unlua初始化流程图:
二.Unlua初始化具体步骤:
在LuaContext类中的RegisterDelegates方法中去绑定ue引擎的许多时刻的关键回调
其中FLuaContext::PreBeginPIE是在点击ue4引擎播放按钮时刻的回调,此时开始调用CreateState注册所有信息
在LuaContext类的CreateState方法中,创建一个主lua线程及注册/创建基本的库/元表和类等操作,主要包括:
- 创建主lua线程为lua_State,创建ObjectMap,StructMap,ArrayMap用来保存以后注册在lua的对象信息
- 注册碰撞通道枚举
- 注册外部静态导出类(c++纯F层类,也即非UObject层的类或者函数,需要通过静态导出的方式让unlua识别调用)
- 注册类信息
是将该类的所有信息描述保存到数组中
在FClassDesc中,保存该类所有信息描述,包含许多成员:类名(FString),类名(FName),类型(是int,bool还是string,table),size,所有接口的数组,所有Property的数组,所有Function的数组等,如下所示:
RegisterClass函数中会调用RegisterClassInternal函数,判断该类是否具有静态导出类模板属性,是就加到数组中,遍历所有静态导出类模板,调用RegisterClassCore
RegisterClassCore是设置Class对应的元表信息,这样lua table可以访问Uobject的属性和方法。
Unlua静态绑定和动态绑定的原理及流程:
一.静态绑定流程图:
二. 静态绑定具体步骤如下:
在UObject初始化时触发:在UObjectBase::UObjectBase的构造函数会调用到UObjectArray的NotifyUObjectCreated方法中
FUObjectCreateListener类可重写两个函数,用于重写当UObjectBase生成时的逻辑
在FLuaContext类中多继承两个类:FUObjectCreateListener(UObject生成时)和FUObjectDeleteListener(UObject结束时)
其中FLuaContext重写NotifyUObjectCreated方法,当UObject生成时,注册信息到unlua中
着重说下TryToBindLua函数,主要做了2件事
1.判断该类是否已经静态绑定(静态绑定是指在类继承unlua接口)
如果是,绑定Object,Class,ModuleName信息到UUnLuaManager中
在UUnLuaManager::Bind中会调用BindInternal函数
在BindInternal中使用GetFunctionList方法得到Module中定义的所有lua方法名。得到的结果存储于 TMap<FString, TSet<FName>> ModuleFunctions容器中,它是ModuleName与FunctionList的键值对,方便以后查找。
遍历得到的所有lua函数,从中找出lua覆写C++UFunction函数,目前支持"BlueprintEvent"和"RepNotifyFunc"两种宏类型。
接下来是关键的”hook“这些C++中要被覆写的UFunction。
首先,需要判断这个UFunction是这个UClass的还是它父类的,是UClass的则替换UFunction,是父类的则添加UFunction。
。
UnLua先把要覆写的UFunction作为TemplateFunction,新建NewFunction。通过DuplicateUFunction函数完成,会把TemplateFunction的Property逐个复制过去,然后Class把NewFunction添加到自己的FuncMap中,以后就能访问。接下来将NewFunc的字节码清空,这意味该TemplateFunction对应的蓝图逻辑执行不到了。
最后会调用LuaFunctionInjection::OverrideUFunction中,该函数为传入的函数添加新的专门识别lua的字节码,此时静态注册流程完毕,以后调用该函数时,因为替换了字节码,导致会走到lua的函数中去
三.动态绑定
动态绑定是通过SpawnActor动态将信息进行绑定,在lua中通过World:SpawnActor生成actor,会调用LuaLib_World中的UWorld_SpawnActor方法
在创建Actor之前,创建FScopedLuaDynamicBinding对象,传入Class,ModuleName,可选的InitializerTable参数。
其构造函数中会使用全局的GLuaDynamicBinding对象进行设置。
设置Class等对象属性。在Object创建后,执行TryToBindLua时,就知道这个对象的ModuleName已经记录,可以动态绑定。
当然,从FScopedLuaDynamicBinding类的名称就可以推测,它只会在这个作用域有效,它的析构函数,做了GLuaDynamicBinding的清理,因此动态绑定只会对这个对象有效。
动态绑定剩下的流程与静态绑定相同,都是注册Class,绑定lua module,替换UFunction等
Unlua反射及注册机制
一.反射注册类FReflectionRegistry
在Unlua插件中的ReflectionUtils文件夹中包含了反射注册类,及所有注册时Property,Function,Class,Enum的描述类(也可以理解为信息类)
Unlua有一个专门存储反射注册信息的类FReflectionRegistry,该类包含了许多的map信息,其中保存了如UStruct和FClassDesc的对应关系,UFunction和UFunctionDesc的对应关系。
二.UFunctionDesc类
该类保存了UFunction对应的各种信息
如:Ufunction *Function:对应UFunction地址
FParameterCollection *DefaultParams:默认参数信息地址
int32 FunctionRef:lua中对应函数地址
TArray<FPropertyDesc*> Properties:函数的参数描述列表
同时在UFunctionDesc类会根据其类型判断是调用在Lua中覆写了UFUNCTION的函数还是直接调用UE本身的UFUNCTION,可以理解为UFunctionDesc相当于一个中转站,从中可以调用UE的Function或Lua的UFunction或Delegate
其注释如下:
三.FPropertyDesc类
其Create静态方法,是根据其Property的类型生成对应的PropertyDesc类
该类主要做了2件事
1.静态Create函数传入FProperty,根据其类型生成对应类型的Desc类
2.Property中包含有许多类型,枚举,bool,float等等,每一种类型继承FPropertyDesc类。重写GetValueInternal,SetValueInternal方法,形成自己独有类型的Desc类,这些方法用于和lua交互。
GetValueInternal():lua获取属性值的接口,根据属性类型使用不同的push方式。Integer等基本类型会直接push值,而像UObject类型会push一个UserData。
SetValueInternal():lua中给属性赋值接口,从lua栈中取出lua中设置的值,给属性设置上,因此自然也要根据不同类型区分。
接口如下;
四:FBoolPropertyDesc类
五.FObjectPropertyDesc类
重写的GetValueInternal函数如下:
针对Object数组还是单个Object做不同处理,
在PushUObject函数中:
一个UObject如果与lua进行了绑定,那么lua中会有一张对应的table,该UObject指针在lua中对应的数据就是这个table。绑定及table创建可见NewLuaObject函数,table被创建后,会在lua中被保存到"ObjectMap"表中进行记录,键为该UObject的地址,值为table。
而如果UObject没有实现UnLuaInterface或没有被动态绑定,那Object本身与lua没有关系,在lua中不会创建table。这个UObject被传递到lua中时,会创建一个UserData,UserData值就是UObject的地址,并且设置metatable为对应Class的ClassMetatable。该操作由PushUObject函数完成:
创建完UserData后,同样会被记录到"ObjectMap"表中。
同理TArray,TSet等结构
C++调用unlua覆写blueprintevent流程
一.用法介绍:
在C++中加入BlueprintImplementableEvent和BlueprintNativeEvent关键字的函数,在Lua中可以覆写
二.原理和流程:
其流程图如下:
在UObject源码中的ProcessEvent函数,能根据虚拟机去执行UFunction
在其实现中会创建出一个可执行栈
被lua覆盖的UFunction,其字节码已经被替换,具体替换步骤如下:
替换后,当该BluePrintEvent函数再被调用时,会调用lua的函数,如下:
比较该Stack的字节码(code)是否等于EX_CallLua,从GReflectionRegistry中获取到注册好的FuncDesc类,该类包含了该函数所有信息(参数,返回值,包含在哪个类中)
直接调CallLua函数,首先将FunctionRef压栈,调用CallLuaInternal方法
在CallLuaInternal中遍历所有FPropertyDesc,之前讲到,FPropertyDesc用于描述每一个函数参数和类中变量,继承于FPropertyDesc类的有很多,FBoolPropertyDesc,FObjectPropertyDesc, FIntPropertyDesc等等,其重写的GetValueInternal方法都是将参数push到lua栈中,而SetValueInternal方法作用是给属性赋值,从lua栈中取出lua中设置的值,给属性设置上
分别执行函数,对引用传递的参数赋值,返回值赋值,并弹出
对于lua栈的结构顺序如下:
Lua中的Delegate
- 用法介绍
在lua中使用Delegate
(1)Add
self.ExitButton.OnClicked:Add(self, UMG_Main_C.OnClicked_ExitButton)
(2)Remove
self.ExitButton.OnClicked:Remove(self, UMG_Main_C.OnClicked_ExitButton)
(3)Clear
self.ExitButton.OnClicked:Clear()
(4)Broadcast
self.ExitButton.OnClicked:Broadcast()
c++中声明委托如下:
/** Signature of function to handle timeline vector track */
DECLARE_DYNAMIC_DELEGATE_OneParam( FOnTimelineVector, FVector, Output );
声明完代理后,我们可以把它作为Object或Struct的一个属性,这样lua就能访问到它了。
/** Function that the output from ValueCurve will be passed to */
UPROPERTY()
FOnTimelineFloat InterpFunc;
单播Delegate的传递由FDelegatePropertyDesc类完成。传递到lua中也由UserData表示,类型为FScriptDelegate
UnLua也对FScriptDelegate进行了静态导出。
导出包括三个方法,"Bind","Unbind","Execute"。
Bind
Bind接受三个参数,FScriptDelegate,UObject和luafunc,其中FScriptDeleate是TScriptDelegate的别名。关键处理部分如下:
类似于BlueprintEvent,克隆一个UFunction,生成对应的FuncDesc,保存新的UFunction和回调
ProcessDelegate中做的就是根据UFunction获取FSignatureDesc,之后调用Execute方法执行lua里的回调,最后还是调用CallLua
补充说明:FSignatureDesc可以理解为FuncDesc的扩展类,用于专门处理Delegate的回调函数,负责执行函数调用和之后的函数清理。Signatures容器中存储了UFunction和SignatureDesc的键值对,用于查找。
Execute
主要操作如下:其原理还是通过FScriptDelegate找到对应的SignatureFunctionDesc
再调用该UObject的ProcessEvent,执行函数
UnBind
主要是一些清理操作,解除ScriptDelegate引用和FSignatureDesc的保存
多播Delegate
其原理和单播基本相同,类型为FMulticastScriptDelegate,不同的是,一个MulticastScriptDelegate可关联多个UFunction。
图示多播委托1和多播委托2的关系:
如下图所示,一个多播委托对应的多个UFunction中,每个UFunction都可对应一个C++的Function和lua的Function
如果多播委托调用remove方法,会移除该函数和委托的联系,如果使用Clear,会清空该委托所关联的所有函数。