getvalue函数怎么使用_UnLua解析(二)使用Lua函数覆盖C++函数

efd762335923840fa843f5267d91064c.png

相关文章:

南京周润发: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栈

cc92f8880d48f0ebbb34f7fc51bc38a7.png

2. 遍历局部变量Properties数组,使用FPropertyDesc::GetValue()方法把函数参数push到lua栈中。Stack的locals属性存储了函数的参数,是一个uint8类型的数组,因此需要以正确顺序解析它,这个工作就由Properties数组完成,Properties中元素的顺序是能够对应的。PropDesc会用之前介绍的GetValueInternal()方法,从locals中获取参数值,然后push到lua中。

c61e3d2ca55b9b3136439567ec4bee10.png

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栈:

7bd97b55a164367eebeedfc9bfacf043.png

至此,C++调用lua覆写的blueprintevent流程算走完了。总体感觉就是UnLua为了性能做了不少努力。

UnLua官网也有一个例子:

3e825556e91f33cdcd16dd15b977d002.png

这个函数有多个返回值,可以注意下返回值的顺序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值