I am working on a multiplayer game in which I have need to send custom and often a layer nested UScriptStruct data over RPC call parameters. In many case, this UScriptStruct is a FBlackboard struct which is similar to Blackboard in Mover but support replication. FBlackboard roughly looks like:
USTRUCT(BlueprintType)
struct FBlackboard
{
UPROPERTY()
FBlackboardValues Values;
UPROPERTY()
TArray<FBlackboardStruct> Structs;
}
USTRUCT()
struct FBlackboardValues
{
...
UPROPERTY(NotReplicated)
TMap<FName, FBlackboardObject> Objects;
UPROPERTY()
TArray<FName> Objects_Key;
UPROPERTY()
TArray<FBlackboardObject> Objects_Value;
bool Serialize(FArchive& Ar);
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
};
USTRUCT()
struct FBlackboardStruct
{
...
bool Serialize(FArchive& Ar);
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
...
UPROPERTY()
FName StructKey = NAME_None;
UPROPERTY()
TArray<uint8> StructData;
UPROPERTY(NotReplicated)
const UScriptStruct* StructType;
UPROPERTY()
FString StructPathName;
UPROPERTY(NotReplicated)
TMap<FName, FBlackboardObject> Objects;
UPROPERTY()
TArray<FName> Objects_Key;
UPROPERTY()
TArray<FBlackboardObject> Objects_Value;
};
USTRUCT()
struct FBlackboardObject
{
...
UObject* ResolveObject() const;
bool Serialize(FArchive& Ar);
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
friend FArchive& operator<<(FArchive& Ar, FBlackboardObject& Object);
#if WITH_EDITORONLY_DATA
UPROPERTY()
int32 PIEInstanceID = INDEX_NONE;
#endif
UPROPERTY()
bool bObjectNull = true;
UPROPERTY()
bool bObjectStatic = false;
UPROPERTY()
bool bObjectNetworked = false;
UPROPERTY()
UClass* ObjectClass = nullptr;
UPROPERTY()
FString ObjectPathName;
UPROPERTY()
FString ObjectName;
UPROPERTY()
UObject* Object = nullptr;
FNetworkGUID ObjectNewGUID;
};
What I have issues with is how to correctly serialize FBlackboardStruct, which should work with any custom UScriptStruct declared in scripts (AngelScript). I used FObjectReader / FObjectWriter and StructData -> TArray<uint8>, mostly in following code FBlackboardStruct::CopyScriptStructTo / FBlackboardStruct::CopyScriptStructFrom. I also record NetGUID / Actor Path etc for UObject store them in FBlackboardObject so that I could resolve UObject cross network (not shown in code sample). I am quite sure this is NOT ideal way to send nested struct over network, but it works. I used to try FInstancedStruct, UE complains my UObject is not implementing IsSupportedForNetworking etc, it is not quite working as intended (and I stopped following this path). Until recently, my code still works. But recently in cooked game, InStructType->SerializeBin reliably crash at FBlackboardStruct::CopyScriptStructTo (this is RPC receiver call site) -> SerializeBin -> FObjectProperty::PostSerializeObjectItem, where ObjectValue points to a unallocated address. In this crash case, I send a struct contains a should-be replicated AActor pointer reference.
I also tried ScriptStruct->SerializeItem (after studying a bit from FInstancedStruct::Serialize), it needs more data size and it introduces even more unknown property correctness issues which I don’t quite understand yet.
Before I dive into the crash, I’d like some advice. Is my previous approach actually doable, or will it just not work?
void FBlackboardStruct::CopyScriptStructTo(const UScriptStruct* InStructType, void* OutStructMemory) const
{
FMemory::Memzero(OutStructMemory, InStructType->GetStructureSize());
FObjectReader ObjectReader(StructData);
TObjectPtr<const UScriptStruct> ScriptStruct = InStructType;
InStructType->SerializeBin(ObjectReader, OutStructMemory);
// ConstCast(ScriptStruct)->SerializeItem(ObjectReader, OutStructMemory, ScriptStruct);
// Search object property to resolve separately
...
}
void FBlackboardStruct::CopyScriptStructFrom(const UScriptStruct* InStructType, void* InStructMemory)
{
TArray<uint8> ObjectBuffer;
FObjectWriter ObjectWriter(ObjectBuffer);
TObjectPtr<const UScriptStruct> ScriptStruct = InStructType;
InStructType->SerializeBin(ObjectWriter, InStructMemory);
// ConstCast(ScriptStruct)->SerializeItem(ObjectWriter, InStructMemory, ScriptStruct);
StructData = TArray<uint8>(ObjectBuffer.GetData(), ObjectBuffer.Num());
StructPathName = InStructType->GetPathName();
}
[Attachment Removed]