Unreal Struct序列化、复制、比较、导出、类型转换、垃圾回收

文章介绍了如何在RPC传输过程中自定义结构体的序列化方法,以降低传输数据量。通过定义NetSerialize和TStructOpsTypeTraits,可以控制哪些字段在网络中发送,从而减少不必要的数据传输。此外,还提到了结构体的序列化、比较、复制、导出导入以及处理版本不匹配的策略,这些都有助于提高效率并适应游戏网络同步的需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

网络序列化

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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值