相关文章:
南京周润发:UnLua解析(一)Object绑定lua
南京周润发:UnLua解析(三)Lua访问Object的property和function
南京周润发:UnLua解析(四)数据在C++和lua间的相互传递
南京周润发:UnLua解析(五)Delegate实现
之前介绍了UnLua中UObject的绑定,简单提过UFunction绑定,在此介绍下其中细节。
使用lua的一个主要原因就是想替换蓝图,把一些本来在蓝图中实现的函数由lua实现,使游戏功能可以灵活修改。
UnLua支持BlueprintEvent和RepNotify类型函数的覆盖。
FFunctionDesc
被替换的UFunction会有一个对应的FFunctionDesc结构体,全局的GReflectionRegistry中有个叫做Functions的map容器,记录了UFunction和FFunctionDesc的对应关系。FFunctionDesc是UFunction与lua函数的桥梁,缓存了一些元信息,对提升效率有很大帮助。
Ufunction *Function:对应UFunction信息
FParameterCollection *DefaultParams:默认参数信息
int32 FunctionRef:lua中对应函数地址
TArray<FPropertyDesc*> Properties:函数的参数描述列表
FPropertyDesc
FFunctionDesc有一个属性为Properties,它是一个FPropertyDesc类型的数组,用于存储该Function的参数信息,而FPropertyDesc则用于描述每一个函数参数。FPropertyDesc不仅在FuncDesc中使用,在ClassDesc中也有,使用非常广泛。
FPropertyDesc只是一个基类,不同类型的属性会有不同的子类描述,比如int,float类型等等,它们通常会覆写GetValueInternal方法和SetValueInternal方法,这些方法用于和lua交互。
常用方法:
GetValueInternal():lua获取属性值的接口,根据属性类型使用不同的push方式。Integer等基本类型会直接push值,而像UObject类型会push一个UserData。
SetValueInternal():lua中给属性赋值接口,从lua栈中取出lua中设置的值,给属性设置上,因此自然也要根据不同类型区分。
调用覆盖的UFunction
被lua覆盖的UFunction,其字节码已经被替换,具体替换步骤如下:
/**
如果非SHIPPING版本,就直接加下面三个字节码。如果是SHIPPING版本,会把UFunction对应FunDesc指针也加入到字节码中,这样会省一次map查找操作,有助于提升效率,属于用空间换时间。
替换后,当该blueprintevent函数再被调用时,就会执行到我们的逻辑了。
字节码中关键是EX_CallLua,是UnLua自定义的功能,对应的实现函数如下:
/**
* Custom thunk function to call Lua function
*/
DEFINE_FUNCTION(FLuaInvoker::execCallLua)
{
bool bUnpackParams = false;
UFunction *Func = Stack.Node;
FFunctionDesc *FuncDesc = nullptr;
if (Stack.CurrentNativeFunction)
{
if (Func != Stack.CurrentNativeFunction)
{
Func = Stack.CurrentNativeFunction;
#if UE_BUILD_SHIPPING || UE_BUILD_TEST
FMemory::Memcpy(&FuncDesc, &Stack.CurrentNativeFunction->Script[1], sizeof(FuncDesc));
#endif
bUnpackParams = true;
}
else
{
Stack.SkipCode(1); // skip EX_CallLua
}
}
#if UE_BUILD_SHIPPING || UE_BUILD_TEST
if (!FuncDesc)
{
FMemory::Memcpy(&FuncDesc, Stack.Code, sizeof(FuncDesc));
Stack.SkipCode(sizeof(FuncDesc)); // skip 'FFunctionDesc' pointer
}
#else
FuncDesc = GReflectionRegistry.RegisterFunction(Func);
#endif
bool bRpcCall = false;
#if SUPPORTS_RPC_CALL
AActor *Actor = Cast<AActor>(Stack.Object);
if (Actor)
{
ENetMode NetMode = Actor->GetNetMode();
if ((Func->HasAnyFunctionFlags(FUNC_NetClient) && NetMode == NM_Client) || (Func->HasAnyFunctionFlags(FUNC_NetServer) && (NetMode == NM_DedicatedServer || NetMode == NM_ListenServer)))
{
bRpcCall = true;
}
}
#endif
FuncDesc->CallLua(Stack, (void*)RESULT_PARAM, bRpcCall, bUnpackParams);
}
首先,会获取到当前正在被调用的UFunction,即变量Func。
然后根据Func寻找已注册的FuncDesc,如果是SHIPPING版本,就直接从字节码里面获取FunDesc指针,如果非SHIPPING,就去map里面查找,或现场注册一个。
最后再执行FuncDesc的CallLua方法。
其中Stack可以理解为蓝图虚拟机,是比较重要的数据结构,用于支撑蓝图调用功能。里面存储了蓝图字节码和数据,在调用一个blueprintevent函数时,都会创建一个Stack。Stack会贯穿调用lua函数的整个过程,包括函数参数传递,函数返回值传递等。
执行到FunDesc后,就好办了,因为其中存储了很多关于Function的元数据。
1. 根据存储的lua函数地址FunctionRef,把FunctionRef和objet都push到lua栈中。如果FunctionRef意外为空,则再去lua里根据Function名称再找一遍lua函数。
当前lua栈
2. 遍历局部变量Properties数组,使用FPropertyDesc::GetValue()方法把函数参数push到lua栈中。Stack的locals属性存储了函数的参数,是一个uint8类型的数组,因此需要以正确顺序解析它,这个工作就由Properties数组完成,Properties中元素的顺序是能够对应的。PropDesc会用之前介绍的GetValueInternal()方法,从locals中获取参数值,然后push到lua中。
3. 使用lua_pcall调用函数。既然函数和参数都已具备,就可以进行函数调用了,使用lua_pcall接口即可。
4. 获取返回值。lua函数调用完成后会把返回值push到lua栈中,Unlua需要以一定顺序取出这些返回值,并设置到正确的C++属性上。lua函数支持多返回值,C++函数只能有一个返回值,但可以使用引用参数实现多返回值效果,因此UnLua支持lua函数直接返回多值给C++。
UE中,BlueprintEvent函数的返回值存储在两个地方,对于普通意义上的单个返回值,存储在eventparms的ReturnValue属性上,对于引用传递的参数,会存储为eventparms上普通属性,但UHT自动生成的代码,会把Stack.locals中对应修改后的属性赋值给原先传进来的参数。这里涉及到UE4的反射机制,可能不是很好理解,举个例子说明一下:
C++中声明如下函数:
UFUNCTION(BlueprintImplementableEvent)
int TestFunc(int a, int &b);
返回一个int值,并且参数b按引用传递
UHT会为TestFunc的参数生成一个struct,该struct之后会作为processevent的Parameters参数。
#define FPS_Source1_Source_FPS_Source1_FPS_Source1Character_h_14_EVENT_PARMS
struct FPS_Source1Character_eventTestFunc_Parms
{
int32 a;
int32 b;
int32 ReturnValue;
/** Constructor, initializes return property only **/
FPS_Source1Character_eventTestFunc_Parms()
: ReturnValue(0)
{
}
};
再看为TestFunc生成的函数体,在调用完blueprintevent,即执行完ProcessEvent后,Parms已经被改变,因此会把参数b的值设置上。
int32 AFPS_Source1Character::TestFunc(int32 a, int32& b)
{
FPS_Source1Character_eventTestFunc_Parms Parms;
Parms.a=a;
Parms.b=b;
ProcessEvent(FindFunctionChecked(NAME_AFPS_Source1Character_TestFunc),&Parms);
b=Parms.b;
return Parms.ReturnValue;
}
这是UE4蓝图调用的过程,UnLua要做的,就是把lua返回值写到Parms参数中。FuncDesc使用OutPropertyIndices数组记录哪些属性是引用传递变量,并且ReturnValue地址之前也已知,因此数据基础已经具备,看下UnLua的做法:
int32 NumParams = HasReturnProperty() ? Properties.Num() : 1 + Properties.Num();
int32 NumResult = GetNumOutProperties();
bool bSuccess = CallFunction(L, NumParams, NumResult); // pcall
if (!bSuccess)
{
return false;
}
int32 OutPropertyIndex = -NumResult;
FOutParmRec *OutParam = OutParams;
for (int32 i = 0; i < OutPropertyIndices.Num(); ++i)
{
FPropertyDesc *OutProperty = Properties[OutPropertyIndices[i]];
OutParam = GetNonConstOutParmRec(OutParam, OutProperty->GetProperty());
check(OutParam);
int32 Type = lua_type(L, OutPropertyIndex);
if (Type == LUA_TNIL)
{
bSuccess = OutProperty->CopyBack(OutParam->PropAddr, OutProperty->GetProperty()->ContainerPtrToValuePtr<void>(InParams)); // copy back value to out property
if (!bSuccess)
{
UNLUA_LOGERROR(L, LogUnLua, Error, TEXT("Can't copy value back!"));
}
}
else
{
OutProperty->SetValueInternal(L, OutParam->PropAddr, OutPropertyIndex, true); // set value for out property
}
OutParam = OutParam->NextOutParm;
++OutPropertyIndex;
}
if (ReturnPropertyIndex > INDEX_NONE)
{
check(RetValueAddress);
Properties[ReturnPropertyIndex]->SetValueInternal(L, RetValueAddress, -1, true); // set value for return property
}
首先遍历OutProperties,调用PropDesc的SetValueInternal方法,把引用参数写回Stack.locals,即Parms结构体。然后判断函数是否有返回值,如果有就把Parms中ReturnValue部分也写上值。观察SetValueInternal函数的栈元素索引就能发现,引用参数按顺序被压入栈中,而返回值位于栈顶。
返回值的lua栈:
至此,C++调用lua覆写的blueprintevent流程算走完了。总体感觉就是UnLua为了性能做了不少努力。
UnLua官网也有一个例子:
这个函数有多个返回值,可以注意下返回值的顺序。