Unlua原理剖析

Posted zhangxiaofan666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unlua原理剖析相关的知识,希望对你有一定的参考价值。

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线程及注册/创建基本的库/元表和类等操作,主要包括:

  1. 创建主lua线程为lua_State,创建ObjectMap,StructMap,ArrayMap用来保存以后注册在lua的对象信息

  1. 注册碰撞通道枚举

  1. 注册外部静态导出类(c++纯F层类,也即非UObject层的类或者函数,需要通过静态导出的方式让unlua识别调用)

  1. 注册类信息

是将该类的所有信息描述保存到数组中

在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++中加入BlueprintImplementableEventBlueprintNativeEvent关键字的函数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

1Add

self.ExitButton.OnClicked:Add(self, UMG_Main_C.OnClicked_ExitButton)

2Remove

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,会清空该委托所关联的所有函数。

 

以上是关于Unlua原理剖析的主要内容,如果未能解决你的问题,请参考以下文章

UE4 Unlua源码解析11 - 非UE4反射支持的静态类导出给Lua使用原理

UE4 Unlua源码解析9 - 静态绑定和动态绑定的实现原理

UE4 Unlua源码解析9 - 静态绑定和动态绑定的实现原理

UE4 Unlua源码解析9 - 静态绑定和动态绑定的实现原理

UE4 Unlua源码解析7 - Lua通过UE命名空间访问C++类型的实现原理

UE4 Unlua源码解析7 - Lua通过UE命名空间访问C++类型的实现原理