网络序列化
RPC 传输这个结构体的时候,可以自己定制序列化的方式,降低传输数据量
定义 NetSerialize 和 TStructOpsTypeTraits
USTRUCT(BlueprintType)
struct FXXX
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float FloatParam = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 IntParam = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FVector> VectorParams;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FRotator RotatorParam = FRotator(0,0,0);
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FHitResult HitParam;
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
};
template<>
struct TStructOpsTypeTraits<FXXX> : public TStructOpsTypeTraitsBase2<FXXX>
{
enum
{
WithNetSerializer = true,
// 不存在Object指针的情况下可以使用 WithNetSharedSerialization = true,
// 详见 https://zhuanlan.zhihu.com/p/412517987
};
};
bool FXXX::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
bool bLocalSuccess = true;
uint8 RepBits = 0;
if (Ar.IsSaving())
{
if (!FMath::IsNearlyZero(FloatParam))
{
RepBits |= (1 << 0);
}
if (IntParam != 0)
{
RepBits |= (1 << 1);
}
if (VectorParams.Num() > 0)
{
RepBits |= (1 << 2);
}
if (RotatorParam != FRotator(0, 0, 0))
{
RepBits |= (1 << 3);
}
if (HitParam.HasValidHitObjectHandle())
{
RepBits |= (1 << 4);
}
}
Ar.SerializeBits(&RepBits, 5);
if (RepBits & (1 << 0))
{
Ar << FloatParam;
}
if (RepBits & (1 << 1))
{
Ar << IntParam;
}
if (RepBits & (1 << 2))
{
Ar << VectorParams;
}
if (RepBits & (1 << 3))
{
Ar << RotatorParam;
}
if (RepBits & (1 << 4))
{
HitParam.NetSerialize(Ar, Map, bLocalSuccess);
}
bOutSuccess = bLocalSuccess;
return !Ar.IsError();
}
序列化
可以自己定义结构体的序列化方式,降低存储该结构体的存储大小
// TStructOpsTypeTraits 增加成员
WithSerializer = true,
// 定义 Serialize
bool Serialize(FArchive& Ar)
重载等号
序列化的时候会判断和CDO的哪些属性不同,就是用的这个。还有属性同步的时候,检测变化的属性也是这个。
// TStructOpsTypeTraits 增加成员
WithIdenticalViaEquality = true,
// 重载==
bool operator==(FXXX const& Other) const
// TStructOpsTypeTraits 增加成员
WithIdentical = true,
// 定义函数Identical
bool Identical(const FXXX* Other, uint32 PortFlags) const;
具体的调用位置如下,所以两者只需要编写一个即可
在属性同步的时候,会调用 PropertiesAreIdentical 进行 ShadowData 和 Object 之间的比较,如果属性是Struct,就会调用到 FStructProperty::Identical,UScriptStruct::CompareScriptStruct。
重载复制
// TStructOpsTypeTraits 增加成员
WithCopy = true,
// 重载=
void operator=(FXXX const& Other)
导出导入
在编辑器内复制粘贴结构体,就是导出成字符串。SaveConfig内部调用的也是这个
使用:
// TStructOpsTypeTraits 增加成员
WithExportTextItem = true,
WithImportTextItem = true,
// 定义函数
bool ExportTextItem(FString& ValueStr, FTestStruct const& DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope) const
bool ImportTextItem( const TCHAR*& Buffer, int32 PortFlags, UObject* Parent, FOutputDevice* ErrorText )
参考:
TMap<int32, double> Doubles;
bool ExportTextItem(FString& ValueStr, FTestStruct const& DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope) const
{
ValueStr += TEXT("(");
for (TMap<int32, double>::TConstIterator It(Doubles); It; ++It)
{
ValueStr += FString::Printf( TEXT("(%d,%f)"),It.Key(), It.Value());
}
ValueStr += TEXT(")");
return true;
}
bool ImportTextItem( const TCHAR*& Buffer, int32 PortFlags, UObject* Parent, FOutputDevice* ErrorText )
{
check(*Buffer == TEXT('('));
Buffer++;
Doubles.Empty();
while (1)
{
const TCHAR* Start = Buffer;
while (*Buffer && *Buffer != TEXT(','))
{
if (*Buffer == TEXT(')'))
{
break;
}
Buffer++;
}
if (*Buffer == TEXT(')'))
{
break;
}
int32 Key = FCString::Atoi(Start);
if (*Buffer)
{
Buffer++;
}
Start = Buffer;
while (*Buffer && *Buffer != TEXT(')'))
{
Buffer++;
}
double Value = FCString::Atod(Start);
if (*Buffer)
{
Buffer++;
}
Doubles.Add(Key, Value);
}
if (*Buffer)
{
Buffer++;
}
return true;
}
序列化转换
在序列化Load、加载存档的时候,会使用一段内存和FPropertyTag进行还原一个FProperty,这个时候可能会因为版本升级,导致数据是老版本的数据类型,但是要还原的FProperty的类型是新类型。这个时候就需要使用原来的数据,转变到新的类型内。
使用:
// TStructOpsTypeTraits 增加成员
WithStructuredSerializeFromMismatchedTag = true,
// 定义函数
bool SerializeFromMismatchedTag(const struct FPropertyTag& Tag, FStructuredArchive::FSlot Slot);
参考:
bool FVector2Int::SerializeFromMismatchedTag(const FPropertyTag& Tag, FStructuredArchive::FSlot Slot)
{
if (Tag.Type == NAME_StructProperty)
{
if(Tag.GetType().IsStruct(NAME_Vector2D))
{
FVector2D OldValue;
Slot << OldValue;
*this = FVector2Int(OldValue);
return true;
}
else if (Tag.GetType().IsStruct(NAME_IntVector2))
{
FIntVector2 OldValue;
Slot << OldValue;
*this = FVector2Int(OldValue);
return true;
}
else if (Tag.GetType().IsStruct(NAME_Vector))
{
FVector OldValue;
Slot << OldValue;
*this = FVector2Int(OldValue.X, OldValue.Y);
return true;
}
}
return false;
}
// 从基类FAnimNode_Root转到子类FAnimNode_StateResult
bool FAnimNode_StateResult::SerializeFromMismatchedTag(const FPropertyTag& Tag, FStructuredArchive::FSlot Slot)
{
if(Tag.Type == NAME_StructProperty && Tag.StructName == FAnimNode_Root::StaticStruct()->GetFName())
{
FAnimNode_Root OldValue;
FAnimNode_Root::StaticStruct()->SerializeItem(Slot, &OldValue, nullptr);
*static_cast<FAnimNode_Root*>(this) = OldValue;
return true;
}
return false;
}
或者如下,其实是一致的
// TStructOpsTypeTraits 增加成员
WithSerializeFromMismatchedTag = true,
// 定义函数
bool SerializeFromMismatchedTag(struct FPropertyTag const& Tag, FArchive& Ar)
调用位置:
void UStruct::SerializeVersionedTaggedProperties(FStructuredArchive::FSlot Slot, uint8* Data, UStruct* DefaultsStruct, uint8* Defaults, const UObject* BreakRecursionIfFullyLoad) const
EConvertFromTypeResult FStructProperty::ConvertFromType(const FPropertyTag& Tag, FStructuredArchive::FSlot Slot, uint8* Data, UStruct* DefaultsStruct)
布尔(FBoolProperty)的实现如下
EConvertFromTypeResult FBoolProperty::ConvertFromType(const FPropertyTag& Tag, FStructuredArchive::FSlot Slot, uint8* Data, UStruct* DefaultsStruct)
// 如果原来的数据类型是int32,
if (Tag.Type == NAME_IntProperty)
LoadFromType<int32>(this, Tag, Slot, Data);
...
else
// 使用序列化
return EConvertFromTypeResult::UseSerializeItem;
// 转换成功
return EConvertFromTypeResult::Converted;
template<typename T>
void LoadFromType(FBoolProperty* Property, const FPropertyTag& Tag, FStructuredArchive::FSlot Slot, uint8* Data)
T IntValue;
Slot << IntValue;
// 读取原来的数据到int,判断是否是0设置新的布尔数据
if (IntValue != 0)
Property->SetPropertyValue_InContainer(Data, true, Tag.ArrayIndex);
else
Property->SetPropertyValue_InContainer(Data, false, Tag.ArrayIndex);
垃圾回收
可以直接使用UProperty进行管理,或者继承FGCObject,然后重写
void AddReferencedObjects( FReferenceCollector& Collector )
进行引用管理。
但是有些情况下,不想基础FGCObject但是需要管理非UProperty的成员、过程中的非成员变量
使用:
// TStructOpsTypeTraits 增加成员
WithAddStructReferencedObjects = true,
// 定义函数
void AddStructReferencedObjects(FReferenceCollector& Collector) const;
参考:
USTRUCT()
struct ENGINE_API FRootMotionSourceGroup
TArray< TSharedPtr<FRootMotionSource> > RootMotionSources;
void FRootMotionSourceGroup::AddStructReferencedObjects(FReferenceCollector& Collector) const
for (const TSharedPtr<FRootMotionSource>& RootMotionSource : RootMotionSources)
if (RootMotionSource.IsValid())
RootMotionSource->AddReferencedObjects(Collector);
空构造
使用默认构造函数并不会递归所有的成员的构造函数,而是直接内存置为0
使用:
// TStructOpsTypeTraits 增加成员
WithZeroConstructor = true,
void UScriptStruct::ClearScriptStruct(void* Dest, int32 ArrayDim) const
if (TheCppStructOps->HasZeroConstructor())
FMemory::Memzero(PropertyData, Stride);
else
TheCppStructOps->Construct(PropertyData);
属性同步
https://ikrima.dev/ue4guide/networking/network-replication/detailed-network-serialization/