Unreal Engine 的 RPC实现
Unreal Engine 的 RPC 注册
UE中的RPC依赖于其能够进行网络同步的AActor系统,所有RPC必须声明在AActor以及其拥有的UActorComponent上。下面是一个在APlayerController上的RPC声明样例:
UFUNCTION(BlueprintCallable, Category="HUD", Reliable, Client)
void ClientSetHUD(TSubclassOf<AHUD> NewHUDClass);
上面的声明中的UFUNCTION是一个提供给UE源代码预处理器UHT(Unreal Header Tool)的标注,有了这个标注之后,UHT将会生成一些胶水代码来实现UObject系统的反射功能。对于这个RPC声明,UHT将在PlayerController.generated.h中生成一个函数声明:
// PlayerController.generated.h
virtual void ClientSetHUD_Implementation(TSubclassOf<AHUD> NewHUDClass);
上面这个函数是逻辑层真正执行该RPC的代码,因此需要在对应的PlayerController.cpp中提供这个函数的实现:
void APlayerController::ClientSetHUD_Implementation(TSubclassOf<AHUD> NewHUDClass)
{
if ( MyHUD != NULL )
{
MyHUD->Destroy();
MyHUD = NULL;
}
FActorSpawnParameters SpawnInfo;
SpawnInfo.Owner = this;
SpawnInfo.Instigator = GetInstigator();
SpawnInfo.ObjectFlags |= RF_Transient; // We never want to save HUDs into a map
MyHUD = GetWorld()->SpawnActor<AHUD>(NewHUDClass, SpawnInfo );
}
而原始的ClientSetHUD函数的实现则出现在UHT生成的额外源文件PlayerController.gen.cpp中:
static FName NAME_APlayerController_ClientSetHUD = FName(TEXT("ClientSetHUD"));
void APlayerController::ClientSetHUD(TSubclassOf<AHUD> NewHUDClass)
{
PlayerController_eventClientSetHUD_Parms Parms;
Parms.NewHUDClass=NewHUDClass;
ProcessEvent(FindFunctionChecked(NAME_APlayerController_ClientSetHUD),&Parms);
}
这里的PlayerController_eventClientSetHUD_Parms也是UHT生成的一个结构体,用来包裹此RPC所需的所有参数,然后通过类型擦除执行ProcessEvent来调用真正执行的函数。
// PlayerController.generated.h
struct PlayerController_eventClientSetHUD_Parms \
{ \
TSubclassOf<AHUD> NewHUDClass; \
}; \
这个ProcessEvent会查询这个类型注册的所有UFunction的Map,执行对应的中转函数。
// PlayerController.gen.cpp
void APlayerController::StaticRegisterNativesAPlayerController()
{
UClass* Class = APlayerController::StaticClass();
static const FNameNativePtrPair Funcs[] = {
// other functions
{ "ClientSetHUD", &APlayerController::execClientSetHUD },
//other functions
};
}
这个中转函数的声明与定义都在UHT生成的相关文件中:
#define DECLARE_FUNCTION(func) static void func( UObject* Context, FFrame& Stack, RESULT_DECL )
// PlayerController.generated.h
DECLARE_FUNCTION(execClientSetHUD);
// PlayerController.gen.cpp
DEFINE_FUNCTION(APlayerController::execClientSetHUD)
{
P_GET_OBJECT(UClass,Z_Param_NewHUDClass);
P_FINISH;
P_NATIVE_BEGIN;
P_THIS->ClientSetHUD_Implementation(Z_Param_NewHUDClass);
P_NATIVE_END;
}
在这个生成的代码中,会从Stack参数中获取第一个参数的值转换为UClass类型,然后赋值到Z_Param_NewHUDClass。从Stack中获取所有的参数之后,再调用真正执行逻辑的函数ClientSetHUD_Implementation。至此,一个忽略了网络数据传递的RPC调用链基本完成。
Unreal Engine的RPC发送与接收
接下来我们来探究这个RPC是如何进行网络传递的。进入ProcessEvent后,发现对应UFunction是remotefunction即RPC,会直接交给NetDriver::ProcessRemoteFunction处理,而不是本地执行。这里我们为了简化讨论,只考虑默认的单播RPC情况。此时需要找到Actor所属的NetConnection,然后从Connection中找到Actor对应的ActorChannel,然后进入ProcessRemoteFunctionForChannelPrivate函数执行具体逻辑。
// Use the replication layout to send the rpc parameter values
TSharedPtr<FRepLayout> RepLayout = GetFunctionRepLayout(Function);
RepLayout->SendPropertiesForRPC(Function, Ch, TempWriter, Parms);
Ch->PrepareForRemoteFunction(TargetObj);
FNetBitWriter TempBlockWriter(Bunch.PackageMap, 0);
Ch->WriteFieldHeaderAndPayload(TempBlockWriter, ClassCache, FieldCache, NetFieldExportGroup, TempWriter);
ParameterBits = TempBlockWriter.GetNumBits();
HeaderBits = Ch->WriteContentBlockPayload(TargetObj, Bunch, false, TempBlockWriter);
Ch->SendBunch(&Bunch, true);
对于每个Actor,其中同步的属性与RPC都有一个唯一的NetIndex,这个NetIndex的计算是通过获取当前RPC名字在当前类型上所有的网络同步的属性名和RPC函数名排序之后的名字数组中的位置来确定的。发送RPC的名字时,只需发送对应的NetIndex即可。接收端能找到NetIndex对应UFunction,然后再通过反射执行。对于有参数的RPC,参数使用FRepLayout进行序列化。上面代码里的SendPropertiesForRPC会根据下面自动生成的代码来遍历当前UFunction的所有参数进行序列化:
// PlayerController.gen.cpp
struct Z_Construct_UFunction_APlayerController_ClientSetHUD_Statics
{
static const UE4CodeGen_Private::FClassPropertyParams NewProp_NewHUDClass;
// PropPointers记录当前函数里所有参数的描述结构的指针
static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[];
// 最终对外的函数参数描述结构
static const UE4CodeGen_Private::FFunctionParams FuncParams;
};
// 这里提供了NewHudClass这个RPC参数的所有相关信息 包括参数名 参数类型名 参数在Params里的偏移 参数大小等
const UE4CodeGen_Private::FClassPropertyParams Z_Construct_UFunction_APlayerController_ClientSetHUD_Statics::NewProp_NewHUDClass = { "NewHUDClass", nullptr, (EPropertyFlags)0x0014000000000080, UE4CodeGen_Private::EPropertyGenFlags::Class, RF_Public|RF_Transient|RF_MarkAsNative, 1, STRUCT_OFFSET(PlayerController_eventClientSetHUD_Parms, NewHUDClass), Z_Construct_UClass_AHUD_NoRegister, Z_Construct_UClass_UClass, METADATA_PARAMS(nullptr, 0) };
// 这里将所有的参数描述结构的指针都收集起来 构造一个PropPointers数组
const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UFunction_APlayerController_ClientSetHUD_Statics::PropPointers[] = {
(const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UFunction_APlayerController_ClientSetHUD_Statics::NewProp_NewHUDClass,
};
// 这里提供的是逻辑层真正对接的函数参数描述结构 FuncParams 内部会使用上面构造的PropPointers数组进行数据填充
const UE4CodeGen_Private::FFunctionParams Z_Construct_UFunction_APlayerController_ClientSetHUD_Statics::FuncParams = { (UObject*(*)())Z_Construct_UClass_APlayerController, nullptr, "ClientSetHUD", nullptr, nullptr, sizeof(PlayerController_eventClientSetHUD_Parms), Z_Construct_UFunction_APlayerController_ClientSetHUD_Statics::PropPointers, UE_ARRAY_COUNT(Z_Construct_UFunction_APlayerController_ClientSetHUD_Statics::PropPointers), RF_Public|RF_Transient|RF_MarkAsNative, (EFunctionFlags)0x05020CC0, 0, 0, METADATA_PARAMS(Z_Construct_UFunction_APlayerController_ClientSetHUD_Statics::Function_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UFunction_APlayerController_ClientSetHUD_Statics::Function_MetaDataParams)) };
UFunction* Z_Construct_UFunction_APlayerController_ClientSetHUD()
{
static UFunction* ReturnFunction = nullptr;
if (!ReturnFunction)
{
UE4CodeGen_Private::ConstructUFunction(ReturnFunction, Z_Construct_UFunction_APlayerController_ClientSetHUD_Statics::FuncParams);
}
return ReturnFunction;
}
RPC参数的序列化结果会跟在NetIndex之后, 这些内容会写入一个新创建的Bunch,写入后bunch内容如下:

上面就是基本的RPC发送流程,在数据接收端收到一个RPC的Bunch之后,识别出其中的NetIndex并寻找到对应的UFunction,然后进入FObjectReplicator::ReceivedRPC函数来处理该RPC.
uint8* Parms = new(FMemStack::Get(), MEM_Zeroed, Function->ParmsSize)uint8;
// Use the replication layout to receive the rpc parameter values
UFunction* LayoutFunction = Function;
while (LayoutFunction->GetSuperFunction())
{
LayoutFunction = LayoutFunction->GetSuperFunction();
}
TSharedPtr<FRepLayout> FuncRepLayout = Connection->Driver->GetFunctionRepLayout(LayoutFunction);
if (!FuncRepLayout.IsValid())
{
UE_LOG(LogRep, Error, TEXT("ReceivedRPC: GetFunctionRepLayout returned an invalid layout."));
return false;
}
FuncRepLayout->ReceivePropertiesForRPC(Object, LayoutFunction, OwningChannel, Reader, Parms, UnmappedGuids);
// Call the function.
Object->ProcessEvent(Function, Parms);
上面的代码是FObjectReplicator::ReceivedRPC处理RPC的核心逻辑,首先分配一段合适大小的内存来接受该RPC的所有参数,然后获取该UFunction的FunctionRepLayout来获取参数描述结构,通过这个结构来从Reader提供的数据bit流中解析所有的参数,解析完参数之后,最终转向ProcessEvent进行前面的execXXX函数调用,从而最终调用到XXX_implementation函数。
Unreal Engine的数据序列化
在理清楚网络传递RPC的基本脉络之后,我们再来探究RPC的相关参数是如何序列化的。在UE中数据的序列化和反序列化都是通过FArchive这个结构体来实现的,在\Engine\Source\Runtime\Core\Public\Serialization\Archive.h中提供了各种基础类型和FString的序列化与反序列化的实现。
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, bool& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int8& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, uint16& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int16& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, uint32& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int32& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, uint64& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int64& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, float& Value);
FORCEINLINE friend FArchive& operator<<(FArchive& Ar, double& Value);
friend CORE_API FArchive& operator<<(FArchive& Ar, FString& Value);
这里的FArchive operator<<既可以当作序列化来使用,也可以当作反序列化来使用,内部通过一个Flag来确定是在序列化还是在反序列化:
/** Returns true if this archive is for loading data. */
FORCEINLINE bool IsLoading() const
{
return ArIsLoading;
}
/** Returns true if this archive is for saving data, this can also be a pre-save preparation archive. */
FORCEINLINE bool IsSaving() const
{
return ArIsSaving;
}
这里的序列化与反序列化是以bit为单位进行操作的,所以外部在使用时经常使用更底层的接口来优化数据大小,例如下面的这个接口就是在我们在已知一个uint32的取值范围之后进行bit裁剪,避免直接使用32bit进行数据传输:
void FBitWriter::WriteIntWrapped(uint32 Value, uint32 ValueMax)
{
check(ValueMax >= 2);
const int32 LengthBits = FMath::CeilLogTwo(ValueMax);
if (AllowAppend(LengthBits))
{
uint32 NewValue = 0;
for (uint32 Mask=1; NewValue+Mask < ValueMax && Mask; Mask*=2, Num++)
{
if (Value & Mask)
{
Buffer[Num>>3] += GShift[Num&7];
NewValue += Mask;
}
}
}
else
{
SetOverflowed(LengthBits);
}
}
UE中的RPC参数列表内置了上述列出的各种基础类型的支持,还支持Actor和ActorComponent类型的指针。如果想要在Rpc参数中使用自定义的结构体,需要其类型上定义如下的序列化接口函数:
USTRUCT()
struct FMyCustomNetSerializableStruct
{
UPROPERTY()
float SomeProperty;
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
Ar << SomeProperty;
bOutSuccess = true;
return true;
}
}
template<>
struct TStructOpsTypeTraits<FMyCustomNetSerializableStruct> : public TStructOpsTypeTraitsBase2<FMyCustomNetSerializableStruct>
{
enum
{
WithNetSerializer = true
};
};
注意最后一部分的TStructOpsTypeTraits特化模板,这个模板的作用是告诉引擎该ustruct定义了自定义的NetSerializer函数。如果不添加该代码,我们添加的NetSerialize方法将永远不会被调用。
Unreal Engine RPC中的指针参数处理
Actor和ActorComponent类型的指针能够作为RPC参数的前提就是该指针指向的对象需要设置bReplicate=true,即参与网络同步。当这样的指针第一次被同步到客户端时,会赋予其当前Connection内唯一的uint32_t FNetworkGuid。指针指向的对象同步到客户端后,会在客户端建立起其FNetworkGuid到这个指针的映射。然后再将这个指针序列化为FNetworkGuid,这样客户端在处理这个RPC时,会通过接收的FNetworkGuid查找已同步过来的网络对象的指针,作为对应RPC参数的实参进行传入。理想情况下看上去很美好,但是不同的ActorChannel在客户端的创建时机是无法保证的,有可能我们在Rpc调用时收到了FNetworkGuid但是对应的对象还没有在客户端创建,此时查找到的UObject*的值为nullptr,情况就变得复杂起来。此时我们有两种选择:
- 直接使用
nullptr作为参数调用RPC - 将这个
RPC缓存起来,等到相应的FNetworkGuid关联的Actor创建好了之后再执行
UE中提供给了我们一个全局变量来选择哪一种方案,默认情况下就直接用nullptr去处理了:
static TAutoConsoleVariable<int32> CVarDelayUnmappedRPCs(
TEXT("net.DelayUnmappedRPCs"),
0,
TEXT("If true delay received RPCs with unmapped object references until they are received or loaded, ")
TEXT("if false RPCs will execute immediately with null parameters. ")
TEXT("This can be used with net.AllowAsyncLoading to avoid null asset parameters during async loads."),
ECVF_Default);
运行时会读取这个变量的值来决定我们是否需要对这个RPC进行延迟执行:
// bool FObjectReplicator::ReceivedBunch(FNetBitReader& Bunch, const FReplicationFlags& RepFlags, const bool bHasRepLayout, bool& bOutHasUnmapped)
const bool bCanDelayRPCs = (CVarDelayUnmappedRPCs.GetValueOnGameThread() > 0) && !bIsServer;
bool bSuccess = ReceivedRPC(Reader, RepFlags, FieldCache, bCanDelayRPCs, bDelayFunction, UnmappedGuids);
if (!bSuccess)
{
return false;
}
else if (bDelayFunction)
{
// This invalidates Reader's buffer
PendingLocalRPCs.Emplace(FieldCache, RepFlags, Reader, UnmappedGuids);
bOutHasUnmapped = true;
bGuidsChanged = true;
bForceUpdateUnmapped = true;
}
如果支持延迟RPC 则会放到PendingLocalRPCs这个数组里。但是UE的可靠RPC在同一个ActorChannle内是严格按照服务端调用顺序执行的。因此客户端接收到的RPC在执行的时候,如果发现PendingLocalRPCs数组不为空, 则当前RPC也不会被执行, 会被标记为DelayRPC,并最终放入到PendingLocalRPCs数组中:
// bool FObjectReplicator::ReceivedRPC(FNetBitReader& Reader, const FReplicationFlags& RepFlags, const FFieldNetCache* FieldCache, const bool bCanDelayRPC, bool& bOutDelayRPC, TSet<FNetworkGUID>& UnmappedGuids)
if (bCanDelayUnmapped && (UnmappedGuids.Num() > 0 || PendingLocalRPCs.Num() > 0))
{
// If this has unmapped guids or there are already some queued, add to queue
bOutDelayRPC = true;
}
然后在NetDriver::Tick中会调用void FObjectReplicator::UpdateUnmappedObjects(bool & bOutHasMoreUnmapped),这里会尝试检查这些被延迟RPC的所等待的网络同步对象客户端是否已经都创建了,如果满足则重试执行:
// Handle pending RPCs, in order
for (int32 RPCIndex = 0; RPCIndex < PendingLocalRPCs.Num(); RPCIndex++)
{
FRPCPendingLocalCall& Pending = PendingLocalRPCs[RPCIndex];
const FFieldNetCache* FieldCache = ClassCache->GetFromIndex(Pending.RPCFieldIndex);
FNetBitReader Reader(Connection->PackageMap, Pending.Buffer.GetData(), Pending.NumBits);
bool bIsGuidPending = false;
for (const FNetworkGUID& Guid : Pending.UnmappedGuids)
{
if (PackageMapClient->IsGUIDPending(Guid))
{
bIsGuidPending = true;
break;
}
FunctionName = FieldCache->Field.GetName();
bSuccess = ReceivedRPC(Reader, Pending.RepFlags, FieldCache, bCanDelayRPCs, bFunctionWasUnmapped, UnmappedGuids);
PendingLocalRPCs.RemoveAt(RPCIndex);
RPCIndex--;
}
}
值得注意的是,为了维持RPC客户端调用的有序性,按照顺序遍历PendingLocalRPCs数组时,如果发现某个RPC的执行条件无法被满足,则停止处理后续的RPC,即使后续RPC所依赖的网络同步对象已经全都满足了。