引子
这个问题的探究来自于日常开发过程中遇到的一次崩溃
示例代码
//1. 结构体1
USTRUCT(BlueprintType)
struct FSubStructParamParent
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite, EditAnywhere)
int64 SubParentIntParam = 0;
virtual bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
Ar<<SubParentIntParam;
return true;
}
};
template <>
struct TStructOpsTypeTraits<FSubStructParamParent> : public TStructOpsTypeTraitsBase2<FSubStructParamParent>
{
enum
{
WithNetSerializer = true,
};
};
//2. 结构体2
USTRUCT(BlueprintType)
struct FSubStructParam : public FSubStructParamParent
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite, EditAnywhere)
int32 SubIntParam = 0;
virtual bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) override
{
Super::NetSerialize(Ar, Map, bOutSuccess);
Ar<<SubIntParam;
return true;
}
};
template <>
struct TStructOpsTypeTraits<FSubStructParam> : public TStructOpsTypeTraitsBase2<FSubStructParam>
{
enum
{
WithNetSerializer = true,
};
};
代码中尝试同步FSubStructParam结构体,但是在调用NetSerialize时产生了崩溃。
崩溃堆栈
Fatal error!
Unhandled Exception: EXCEPTION_ACCESS_VIOLATION reading address 0x0000000000000000
0x00007ff912356fc8 UnrealEditor-PackProjectTest.dll!UScriptStruct::TCppStructOps<FSubStructParam>::NetSerialize() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Public\UObject\Class.h:1314]
0x00007ff968a6505c UnrealEditor-CoreUObject.dll!FStructProperty::NetSerializeItem() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\PropertyStruct.cpp:192]
0x00007ff923afc52d UnrealEditor-Engine.dll!FRepLayout::SerializeProperties_r() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\RepLayout.cpp:6659]
0x00007ff923af6ff2 UnrealEditor-Engine.dll!FRepLayout::SendPropertiesForRPC() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\RepLayout.cpp:6965]
0x00007ff923459794 UnrealEditor-Engine.dll!UNetDriver::ProcessRemoteFunctionForChannelPrivate() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\NetDriver.cpp:2736]
0x00007ff923446c83 UnrealEditor-Engine.dll!UNetDriver::InternalProcessRemoteFunctionPrivate() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\NetDriver.cpp:2557]
0x00007ff92344655b UnrealEditor-Engine.dll!UNetDriver::InternalProcessRemoteFunction() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\NetDriver.cpp:2459]
0x00007ff9234584b8 UnrealEditor-Engine.dll!UNetDriver::ProcessRemoteFunction() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\NetDriver.cpp:7555]
0x00007ff92264bc1f UnrealEditor-Engine.dll!UActorComponent::CallRemoteFunction() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\Components\ActorComponent.cpp:837]
0x00007ff968ab4cc3 UnrealEditor-CoreUObject.dll!UObject::CallFunction() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\ScriptCore.cpp:1130]
0x00007ff968acec2e UnrealEditor-CoreUObject.dll!UObject::ProcessContextOpcode() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\ScriptCore.cpp:3086]
0x00007ff968ad3088 UnrealEditor-CoreUObject.dll!ProcessLocalScriptFunction() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\ScriptCore.cpp:1206]
0x00007ff968aa4fed UnrealEditor-CoreUObject.dll!ProcessScriptFunction<void (__cdecl*)(UObject *,FFrame &,void *)>() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\ScriptCore.cpp:1039]
0x00007ff968ad2ad4 UnrealEditor-CoreUObject.dll!ProcessLocalFunction() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\ScriptCore.cpp:1276]
0x00007ff968ad3088 UnrealEditor-CoreUObject.dll!ProcessLocalScriptFunction() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\ScriptCore.cpp:1206]
0x00007ff968ad1fef UnrealEditor-CoreUObject.dll!UObject::ProcessInternal() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\ScriptCore.cpp:1304]
0x00007ff968740962 UnrealEditor-CoreUObject.dll!UFunction::Invoke() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\Class.cpp:6847]
0x00007ff968ad0745 UnrealEditor-CoreUObject.dll!UObject::ProcessEvent() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\CoreUObject\Private\UObject\ScriptCore.cpp:2144]
0x00007ff921ee2597 UnrealEditor-Engine.dll!AActor::ProcessEvent() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\Actor.cpp:1092]
0x00007ff921cb58b3 UnrealEditor-Engine.dll!TScriptDelegate<FNotThreadSafeDelegateMode>::ProcessDelegate<UObject>() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Core\Public\UObject\ScriptDelegates.h:448]
0x00007ff9227c0cc4 UnrealEditor-Engine.dll!FInputActionHandlerDynamicSignature_DelegateWrapper() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Intermediate\Build\Win64\UnrealEditor\Inc\Engine\UHT\InputComponent.gen.cpp:73]
0x00007ff9242211e7 UnrealEditor-Engine.dll!FInputActionUnifiedDelegate::Execute() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Classes\Components\InputComponent.h:302]
0x00007ff92421c657 UnrealEditor-Engine.dll!UPlayerInput::EvaluateInputDelegates() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\UserInterface\PlayerInput.cpp:1516]
0x0000027d00d2de0a UnrealEditor-EnhancedInput.dll!UEnhancedPlayerInput::EvaluateInputDelegates() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Plugins\EnhancedInput\Source\EnhancedInput\Private\EnhancedPlayerInput.cpp:749]
0x00007ff9242687d7 UnrealEditor-Engine.dll!UPlayerInput::ProcessInputStack() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\UserInterface\PlayerInput.cpp:1149]
0x00007ff9238c09c9 UnrealEditor-Engine.dll!APlayerController::ProcessPlayerInput() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\PlayerController.cpp:2738]
0x00007ff9238e0a1e UnrealEditor-Engine.dll!APlayerController::TickPlayerInput() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\PlayerController.cpp:5035]
0x00007ff9238bb967 UnrealEditor-Engine.dll!APlayerController::PlayerTick() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\PlayerController.cpp:2342]
0x00007ff9238de29b UnrealEditor-Engine.dll!APlayerController::TickActor() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\PlayerController.cpp:5194]
0x00007ff921eabf96 UnrealEditor-Engine.dll!FActorTickFunction::ExecuteTick() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\Actor.cpp:238]
0x00007ff92404878b UnrealEditor-Engine.dll!FTickFunctionTask::DoTask() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\TickTaskManager.cpp:278]
0x00007ff92404f005 UnrealEditor-Engine.dll!TGraphTask<FTickFunctionTask>::ExecuteTask() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Core\Public\Async\TaskGraphInterfaces.h:1235]
0x00007ff95cb210aa UnrealEditor-Core.dll!FNamedTaskThread::ProcessTasksNamedThread() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Core\Private\Async\TaskGraph.cpp:760]
0x00007ff95cb2174e UnrealEditor-Core.dll!FNamedTaskThread::ProcessTasksUntilQuit() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Core\Private\Async\TaskGraph.cpp:651]
0x00007ff95cb2e4de UnrealEditor-Core.dll!FTaskGraphCompatibilityImplementation::WaitUntilTasksComplete() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Core\Private\Async\TaskGraph.cpp:2122]
0x00007ff9240786c5 UnrealEditor-Engine.dll!FTickTaskSequencer::ReleaseTickGroup() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\TickTaskManager.cpp:556]
0x00007ff92407fa8b UnrealEditor-Engine.dll!FTickTaskManager::RunTickGroup() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\TickTaskManager.cpp:1583]
0x00007ff922ff6fff UnrealEditor-Engine.dll!UWorld::RunTickGroup() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\LevelTick.cpp:775]
0x00007ff9230059b6 UnrealEditor-Engine.dll!UWorld::Tick() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Engine\Private\LevelTick.cpp:1644]
0x00007ff943afcdde UnrealEditor-UnrealEd.dll!UEditorEngine::Tick() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Editor\UnrealEd\Private\EditorEngine.cpp:2041]
0x00007ff944785ba6 UnrealEditor-UnrealEd.dll!UUnrealEdEngine::Tick() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Editor\UnrealEd\Private\UnrealEdEngine.cpp:550]
0x00007ff7adc38e02 UnrealEditor.exe!FEngineLoop::Tick() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp:5924]
0x00007ff7adc5e1bc UnrealEditor.exe!GuardedMain() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Launch\Private\Launch.cpp:180]
0x00007ff7adc5e2aa UnrealEditor.exe!GuardedMainWrapper() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:118]
0x00007ff7adc61724 UnrealEditor.exe!LaunchWindowsStartup() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:258]
0x00007ff7adc76ed4 UnrealEditor.exe!WinMain() [D:\Trunk\trunk\UnrealEngine-5.4\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:298]
0x00007ff7adc7a17a UnrealEditor.exe!__scrt_common_main_seh() [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288]
0x00007ffa2db17374 KERNEL32.DLL!UnknownFunction []
最后定位到导致该崩溃原因为:在同步结构体时,自定义了结构体的NetSerialize方法,且该方法被定为了虚方法导致的。
解决方案:避免使用virtual;或者在外层包一层结构体的Handle,在Handle层定义NetSerialize方法,类似于GAS中FGameplayEffectContextHandle和FGameplayEffectContext
下面对UE常见数据同步进行一次回顾,主要讨论结构体、UObject、Actor三种数据的同步,基础类型数据不再赘述。
UE的同步方式主要是:RPC和属性同步。
一、结构体的同步
-
需要同步的结构体
USTRUCT(BlueprintType) struct FRPCStruct { GENERATED_BODY() UPROPERTY(BlueprintReadWrite, EditAnywhere) int32 IntParam = 0; UPROPERTY(BlueprintReadWrite, EditAnywhere) float FloatParam = 0.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere) FSubStructParam StructParam; UPROPERTY(BlueprintReadWrite, EditAnywhere) TObjectPtr<UPackProjectParam> ParamObj = nullptr; FString ToString() const { FString Result = FString::Printf( TEXT("FRPCStructInfo,Int:%d, Float:%f, SubInt:%d, SubParamInt:%lld"), IntParam, FloatParam, StructParam.SubIntParam, StructParam.SubParentIntParam); if (IsValid(ParamObj)) { Result += FString::Printf(TEXT("___ExtraParamObj:%d"), ParamObj->ObjIntParam); } return Result; } bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess) { Ar<<IntParam; Ar<<FloatParam; Ar<<StructParam.SubIntParam; Ar<<StructParam.SubParentIntParam; uint8 RepBits = 0; if (Ar.IsSaving()) { if (IsValid(ParamObj)) { RepBits |= 1 << 0; } } Ar.SerializeBits(&RepBits, 1); if (RepBits & (1 << 0)) { Ar << ParamObj; } bOutSuccess = true; return bOutSuccess; } }; template <> struct TStructOpsTypeTraits<FRPCStruct> : public TStructOpsTypeTraitsBase2<FRPCStruct> { enum { WithNetSerializer = true, }; };
Note:这里NetSerialize方法一定不能为Virtual
-
RPC 同步Struct
-
C2S
UFUNCTION(BlueprintCallable, Server, Reliable) void SendStructToServer(const FRPCStruct& InStruct);
-
S2C
UFUNCTION(BlueprintCallable, Client, Reliable) void SendStructToClient(const FRPCStruct& InStruct);
-
-
属性同步Struct
Note:
在属性同步中,无论对结构体整体进行赋值,或者修改结构体中某一字段(基础类型),都会触发OnRep方法。
若结构体中嵌套UObject指针,但UObject为新构造对象,则无法一同同步。
若UObject本身支持同步,仅修改了UObject对象中某字段的值也是无法触发结构体的OnRep方法。
-
关于自定义结构体NetSerialize方法
结构体本身若是基础数据类型的集合,不用自定义NetSerialize方法也是可以同步的,还可以通过UPROPERTY,NotReplicated标记符控制是否某字段参与同步。
若需要更深度定义同步数据内容,则可通过自定义NetSerialize方法,而要支持自定义NetSerialize,需要定义以下结构体,这样引擎会根据配置的枚举去萃取结构体的属性。
自定义数据较为常见的场景应用:支持TMap的同步、某些字段满足条件后才参与同步等
-
常见方法
- 正在进行序列化:Ar.IsSaving()
- 正在进行反序列化:Ar.IsLoading()
- 序列化、反序列比特:Ar.SerializeBits
-
TMap同步方式实现思路:TMap本身是不可同步的,但是TArray可同步,因此常常通过在IsSaving的时候将TMap的Key、Value通过打包成结构体数组同步,在IsLoading时,再将同步来的TArray数据重新解析为TMap。
-
条件同步
uint8 RepBits = 0; if (Ar.IsSaving()) { if (IsValid(ParamObj)) { RepBits |= 1 << 0; } } Ar.SerializeBits(&RepBits, 1); if (RepBits & (1 << 0)) { Ar << ParamObj; }
上述代码的思路,在序列化结构体(Ar.IsSaving())时,判断ParamObj的有效性,若有效,则将RepBits的第一位置为1。
无论序列化还是反序列化,都会调用Ar.SerializeBits(),将RepBits读入或写出后,判断ParamObj数据是否进行序列化和反序列化。
-
扩展思考
-
Struct 中嵌套 UObject
若UObject本身不支持同步,也无法跟随Struct同步。
-
Struct中嵌套Actor
-
FArchive*、UPackageMap* 的作用是什么?
Map->GetObjectFromNetGUID() Map->GetNetGUIDFromObject()
二、UObject同步
UObject对象默认在UE中是不支持同步的,但通过某些方式或某些特殊的UObject也支持同步。
-
RPC UObject对象
-
C2S
UFUNCTION(BlueprintCallable, Server, Reliable) void SendUObjectToServer(const UObject* Param);
这里分为三种UObject类型
- 新构造的UObject,无法同步
- InstancedObject,可以同步,但是修改Object某字段值是无法同步的
- 支持ReplicatedObject,可以同步,但是修改Object某字段值是无法同步的
-
S2C
UFUNCTION(BlueprintCallable, Client, Reliable) void SendUObjectToClient(const UObject* Param);
这里分为三种UObject类型
- 新构造的UObject,无法同步
- InstancedObject,可以同步,但是修改Object某字段值是无法同步的
- 支持ReplicatedObject,可以同步,若修改了某字段值,因为RPC快于属性同步,其在RPC中获取Object该字段值是为旧值。(其本质是因为RPC并未同步数据内容)
-
-
属性同步UObject对象
-
UObject对象定义
这里需要重写IsSupportedForNetworkding方法,返回为true
-
Actor中标记该UObject为网络复制
-
在服务将UObject对象创建后,调用AddReplicatedSubObject方法
UStructRPCComponent::UStructRPCComponent() { bReplicateUsingRegisteredSubObjectList = true; } void UStructRPCComponent::BeginPlay() { Super::BeginPlay(); if(GetOwner()->HasAuthority()) { PackProjectParam = NewObject<UPackProjectParam>(this, UPackProjectParam::StaticClass()); PackProjectParam->ObjIntParam = 10; AddReplicatedSubObject(PackProjectParam); } FTimerHandle TimerHandle; GetWorld()->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateWeakLambda(this,[this]() { if(IsValid(PackProjectParam)) { PackProjectParam->ObjIntParam = 20; } }), 2, false); }
Note:在Actor中定义的OnRep方法只会在创建赋值后执行一次,若后续若只是修改对象里的值,则此OnRep不会再执行。
而定义在UObject对象内部的OnRep函数,不仅会在初次赋值UObject对象指针时调用一次,后续变更值也会调用,且时序早于上述Actor中定义的OnRep函数。
-
-
获取可复制UObject NetworkGUID的方法
GetWorld()->GetNetDriver()->GuidCache->GetObjectFromNetGUID(); GetWorld()->GetNetDriver()->GuidCache->GetNetGUID();
扩展思考
- 为什么InstancedObject的UObject对象可以支持同步呢?但在服务器上修改UObject对象中某字段值确不能同步呢?
这里其实不难理解,InstancedObject实际是通过Package路径序列化在本地的,这里同步的其实是Package路径,同步后,通过Load恢复UObject指针内容。
三、Actor同步(UE同步的基本单位)
Actor需被标记为Replicates,本质上Actor是特殊的UObject对象,为什么他可以被同步呢?
-
RPC Actor对象
UFUNCTION(BlueprintCallable, Client, Reliable) void SendActorToClient(const AActor* InActor); UFUNCTION(BlueprintCallable, Server, Reliable) void SendActorToServer(const AActor* InActor);
同上述同步UObject类似,如果Actor本身支持同步,才能通过RPC上行发到服务器,且客户端修改的内容并不会发送到服务器
这里更多讨论下行,即SendActorToClient,这里有两种情景
-
case1:Actor是早已同步到客户端的Actor,通过RPC再发送Actor指针,能正确在客户端上获取该Actor指针。
-
case2:Actor是在服务器上临时SpawnActor,立刻通过RPC发送到客户端,那能否获得该Actor的指针呢?
答案是获取不到。这种情况的应用其实挺多的,比如在制作坐骑系统时,可能需要临时在服务器上spawn一个坐骑actor,然后possess该actor,但是该actor还未同步到客户端,就会有问题;还比如我们网络刚连接进入游戏,会在服务器创建玩家的Pawn,此时Pawn还未复制到客户端,服务器就进行了Possess。
那如何解决case2的问题呢?
-
其实引擎提供了一个控制台变量,如下
[ConsoleVariables] net.DelayUnmappedRPCs=1
其会保证RPC中Actor*若还未复制到客户端,会延迟到Actor复制到客户端后再调用对应的RPC。
-
-
属性同步Actor对象
Actor本身未标记未Replicates,那么则不可复制。Actor本身标记为Replicates
-
case1:Actor早已复制到客户端,那么在服务器赋值该字段,可以正常在OnRep方法中获取到Actor对象。
-
case2:Actor为临时Spawn的Actor,能否正确在OnRep中获取到Actor对象。
答案是OnRep可以正确获取到该AActor对象。那说明AActor的复制早于当前属性同步吗?
-
-
获取ActorNetworkGUID的方法
GetWorld()->GetNetDriver()->GetGUIDForActor()
GetWorld()->GetNetDriver()->GetActorForGUID()
扩展讨论
-
UE后续版本支持了InstancedStruct,那么InstancedStruct支持直接同步吗?
-
UE的同步单位是什么呢?
-
网络通信相关对象:UNetDriver、UNetConnection、Actor、ActorChannel、FOutBunch
-
Socket、网络底层模型(TCP/IP、OSI)
- UE中DS的常见优化方式
结语
网络同步的基础,是基于OSI七层模型或TCP/IP四层模型,在传输层基于TCP或UDP协议来提供端到端的通信。
而一般网络序列化的思路:将数据结构序列化为二进制数据流进行网络传输,再将二进制数据流反序列化为数据结构。
以UE为例子:
- 序列化:将UStruct->FString(数据结构格式:Json、Xml等)->TArray
- 端到端通信:TCP/UDP
- 反序列化:TArray
->FString->UStruct