vault backup: 2024-10-12 17:19:45

This commit is contained in:
2024-10-12 17:19:46 +08:00
parent ff94ddca61
commit 244c0c52f6
960 changed files with 31348 additions and 10 deletions

View File

@@ -0,0 +1,154 @@
# MatchedSerializers
- **功能描述:** 指定类支持文本结构序列化
- **引擎模块:** Serialization
- **元数据类型:** bool
- **作用机制:** 在ClassFlags中增加[CLASS_MatchedSerializers](../../../../Flags/EClassFlags/CLASS_MatchedSerializers.md)在Meta中添加[MatchedSerializers](../../../../Meta/Serialization/MatchedSerializers.md)
- **常用程度:** 0
该标识符只允许在NoExportTypes.h中使用属于是引擎自用的内部标识符。
基本上大部分的类都拥有该标记除了自身不导出的类一般包括NoExportTypes.h定义的除非手动加上MatchedSerializers比如UObject或者靠DECLARE_CLASS_INTRINSIC直接在源码里定义的元数据。
因此实际上大部分的类都拥有该标记。因为在UHT中只要不是NoExport的就会自动的加上这个标记。
```cpp
// Force the MatchedSerializers on for anything being exported
if (!ClassExportFlags.HasAnyFlags(UhtClassExportFlags.NoExport))
{
ClassFlags |= EClassFlags.MatchedSerializers;
}
```
## 结构化序列化器:
如果一个类支持文本格式则StructuredArchive的结构的意思是会把类里的字段树形展开来序列化展示出来从而方便人类理解。而如果不支持文本格式则会把所有的字段值压进一个二进制buffer里Data字段这也是runtime时候用的方式。
测试代码:
```cpp
UCLASS(Blueprintable, BlueprintType,editinlinenew)
class INSIDER_API UMyClass_MatchedSerializersSub :public UObject
{
public:
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MyInt_Default = 123;
};
UCLASS(Blueprintable, BlueprintType)
class INSIDER_API UMyClass_MatchedSerializersTestAsset:public UDataAsset
{
public:
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MyInt_Default = 123;
UPROPERTY(EditAnywhere, BlueprintReadWrite,Instanced)
UMyClass_MatchedSerializersSub* SubObject;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UStruct* MyStructType;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UClass* MyClassType;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UEnum* MyEnumType;
};
void UMyClass_MatchedSerializers_Test::ApplyClassFlag()
{
UMyClass_MatchedSerializersTestAsset::StaticClass()->ClassFlags |= CLASS_MatchedSerializers;
UMyClass_MatchedSerializersSub::StaticClass()->ClassFlags |= CLASS_MatchedSerializers;
}
void UMyClass_MatchedSerializers_Test::RemoveClassFlag()
{
UMyClass_MatchedSerializersTestAsset::StaticClass()->ClassFlags &= ~CLASS_MatchedSerializers;
UMyClass_MatchedSerializersSub::StaticClass()->ClassFlags &= ~CLASS_MatchedSerializers;
}
```
在编辑器中创建测试数据Asset
![Untitled](Untitled.png)
然后在Editor选项里打开TextAssetFormatSupport(UEditorExperimentalSettings::bTextAssetFormatSupport)
![Untitled](Untitled%201.png)
然后在资产上就出现3个菜单支持把资产导出为文本。
![Untitled](Untitled%202.png)
ExportToTextFormat会在蓝图资产的同目录生成一个.utxt的文件格式为json。通过动态的增删CLASS_MatchedSerializers这个标记来对比这个标记产生的差异
![Untitled](Untitled%203.png)
可以发现序列化出来的内容有明显的差异不带有CLASS_MatchedSerializers标记的产生的右侧结果把所有的字段值压进一个二进制buffer里Data字段
## 内部机制原理:
CLASS_MatchedSerializers这个标记在UClass::IsSafeToSerializeToStructuredArchives中被使用标明采用结构序列化器。是否支持文本导入导出只在编辑器情况下使用。
在发生作用的只有SavePackage2.cpp和LinkerLoad.cpp因此是只发生在保存UPackage的时候作为子类对象。所以不能用简单的内存里Archive序列化来进行测试。
```cpp
bool UClass::IsSafeToSerializeToStructuredArchives(UClass* InClass)
{
while (InClass)
{
if (!InClass->HasAnyClassFlags(CLASS_MatchedSerializers))
{
return false;
}
InClass = InClass->GetSuperClass();
}
return true;
}
//LinkerLoad.cpp
bool bClassSupportsTextFormat = UClass::IsSafeToSerializeToStructuredArchives(Object->GetClass());
if (IsTextFormat())//如果Ar序列化是文本格式
{
FStructuredArchiveSlot ExportSlot = GetExportSlot(Export.ThisIndex);
if (bClassSupportsTextFormat) //如果类本身支持文本格式
{
Object->GetClass()->SerializeDefaultObject(Object, ExportSlot);
}
else
{
FStructuredArchiveChildReader ChildReader(ExportSlot);
FArchiveUObjectFromStructuredArchive Adapter(ChildReader.GetRoot());
Object->GetClass()->SerializeDefaultObject(Object, Adapter.GetArchive());
}
}
//SavePackage2.cpp
#if WITH_EDITOR
bool bSupportsText = UClass::IsSafeToSerializeToStructuredArchives(Export.Object->GetClass());
#else
bool bSupportsText = false;
#endif
if (bSupportsText)
{
Export.Object->GetClass()->SerializeDefaultObject(Export.Object, ExportSlot);
}
else
{
FArchiveUObjectFromStructuredArchive Adapter(ExportSlot);
Export.Object->GetClass()->SerializeDefaultObject(Export.Object, Adapter.GetArchive());
Adapter.Close();
}
```
文本格式只在编辑器环境下生效。
可以从源码看到如果类本身支持文本格式序列化则在Ar是文本格式的时候直接可以序列化采用默认的SerializeTaggedProperties。否则得采用FArchiveUObjectFromStructuredArchive 来适配一下把对象指针转换为object path+ int32 Index的组合。
在引擎中打印出所有包含或不包含CLASS_MatchedSerializers的类发现UStruct继承链下面的类开始包含但是UClass却不包含而上面UField的类则不包含比如各种Property。类列表见Doc下txt文件。

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,8 @@
# NonTransient
- **功能描述:** 使继承自基类的Transient说明符无效。
- **引擎模块:** Serialization
- **元数据类型:** bool
- **作用机制:** 在ClassFlags中移除[CLASS_Transient](../../../Flags/EClassFlags/CLASS_Transient.md)
- **关联项:** [Transient](Transient/Transient.md)
- **常用程度:** ★★★

View File

@@ -0,0 +1,275 @@
# Optional
- **功能描述:** 标记该类的对象是可选的在Cooking的时候可以选择是否要忽略保存它们。
- **引擎模块:** Serialization
- **作用机制:** 在ClassFlags中添加[CLASS_Optional](../../../../Flags/EClassFlags/CLASS_Optional.md)
- **常用程度:** ★
标记该类的对象是可选的在Cooking的时候可以选择是否要忽略保存它们。
- 一般为EditorOnly的数据如MetaData等在游戏运行时不存在保存在其他的特定文件中。
- Optional的对象一般也包在WITH_EDITORONLY_DATA宏里只在编辑器下使用。
- 引擎在cook的时候会根据EDITOROPTIONAL的配置来加上SAVE_Optional从而选择是否一起序列化该对象值比如metadata。
## 示例代码:
```cpp
//ClassFlags: CLASS_Optional | CLASS_MatchedSerializers | CLASS_Native | CLASS_RequiredAPI | CLASS_TokenStreamAssembled | CLASS_Intrinsic | CLASS_Constructed
UCLASS(Optional)
class INSIDER_API UMyClass_Optional :public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MyProperty = 123;
};
UCLASS()
class INSIDER_API UMyClass_NotOptional :public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MyProperty = 123;
};
UCLASS()
class INSIDER_API UMyClass_Optional_Test :public UObject
{
GENERATED_BODY()
public:
#if WITH_EDITORONLY_DATA
UPROPERTY()
UMyClass_Optional* MyOptionalObject;
#endif // WITH_EDITORONLY_DATA
public:
UPROPERTY()
UMyClass_NotOptional* MyNotOptionalObject;
public:
static void CreatePackageAndSave();
static void LoadPackageAndTest();
};
void UMyClass_Optional_Test::CreatePackageAndSave()
{
FString packageName = TEXT("/Game/MyOptionTestPackage");
FString assetPath = FPackageName::LongPackageNameToFilename(packageName, FPackageName::GetAssetPackageExtension());
IFileManager::Get().Delete(*assetPath, false, true);
UPackage* package = CreatePackage(*packageName);
FSavePackageArgs saveArgs{};
//saveArgs.TopLevelFlags = EObjectFlags::RF_Public | EObjectFlags::RF_Standalone;
saveArgs.Error = GError;
saveArgs.SaveFlags=SAVE_NoError;
//SAVE_Optional = 0x00008000, ///< Indicate that we to save optional exports. This flag is only valid while cooking. Optional exports are filtered if not specified during cooking.
UMyClass_Optional_Test* testObject = NewObject<UMyClass_Optional_Test>(package, TEXT("testObject"));
#if WITH_EDITORONLY_DATA
testObject->MyOptionalObject = NewObject<UMyClass_Optional>(testObject, TEXT("MyOptionalObject"));
testObject->MyOptionalObject->MyProperty = 456;
#endif
testObject->MyNotOptionalObject = NewObject<UMyClass_NotOptional>(testObject, TEXT("MyNotOptionalObject"));
testObject->MyNotOptionalObject->MyProperty = 456;
FString str = UInsiderSubsystem::Get().PrintObject(package, EInsiderPrintFlags::All);
FString str2 = UInsiderSubsystem::Get().PrintObject(testObject, EInsiderPrintFlags::All);
FString str3 = UInsiderSubsystem::Get().PrintObject(UMyClass_Optional::StaticClass(), EInsiderPrintFlags::All);
FString str4 = UInsiderSubsystem::Get().PrintObject(UMyClass_NotOptional::StaticClass(), EInsiderPrintFlags::All);
bool result = UPackage::SavePackage(package, testObject, *assetPath, saveArgs);
}
void UMyClass_Optional_Test::LoadPackageAndTest()
{
FString packageName = TEXT("/Game/MyOptionTestPackage");
FString assetPath = FPackageName::LongPackageNameToFilename(packageName, FPackageName::GetAssetPackageExtension());
UPackage* package = LoadPackage(nullptr, *assetPath, LOAD_None);
package->FullyLoad();
UMyClass_Optional_Test* newTestObject = LoadObject<UMyClass_Optional_Test>(package, TEXT("testObject"), *assetPath);
//UMyClass_Transient_Test* newTestObject = nullptr;
/*const TArray<FObjectExport>& exportMap = package->GetLinker()->ExportMap;
for (const auto& objExport : exportMap)
{
if (objExport.ObjectName == TEXT("testObject"))
{
newTestObject = Cast<UMyClass_Transient_Test>(objExport.Object);
break;
}
}*/
FString str = UInsiderSubsystem::Get().PrintObject(package, EInsiderPrintFlags::All);
}
```
## 示例效果:
正常的SavePackage发现是没有作用的依然会序列化保存。特殊的保存方式在Cook阶段本例就没有专门测试了。
![Untitled](Untitled.png)
在源码里搜索Optional可以看到一般是EditorOnlyData和CookedMetaData类在使用。
```cpp
UCLASS(Optional, Within=Enum)
class ENGINE_API UEnumCookedMetaData : public UObject
UCLASS(Optional, Within=ScriptStruct)
class ENGINE_API UStructCookedMetaData : public UObject
UCLASS(Optional, Within=Class)
class ENGINE_API UClassCookedMetaData : public UObject
UMaterialInterfaceEditorOnlyData* UMaterialInterface::CreateEditorOnlyData()
{
const UClass* EditorOnlyClass = GetEditorOnlyDataClass();
check(EditorOnlyClass);
check(EditorOnlyClass->HasAllClassFlags(CLASS_Optional));
const FString EditorOnlyName = MaterialInterface::GetEditorOnlyDataName(*GetName());
const EObjectFlags EditorOnlyFlags = GetMaskedFlags(RF_PropagateToSubObjects);
return NewObject<UMaterialInterfaceEditorOnlyData>(this, EditorOnlyClass, *EditorOnlyName, EditorOnlyFlags);
}
```
引擎里也有一些验证:
```cpp
UnrealTypeDefinitionInfo.cpp:
// Validate if we are using editor only data in a class or struct definition
if (HasAnyClassFlags(CLASS_Optional))
{
for (TSharedRef<FUnrealPropertyDefinitionInfo> PropertyDef : GetProperties())
{
if (PropertyDef->GetPropertyBase().IsEditorOnlyProperty())
{
PropertyDef->LogError(TEXT("Cannot specify editor only property inside an optional class."));
}
else if (PropertyDef->GetPropertyBase().ContainsEditorOnlyProperties())
{
PropertyDef->LogError(TEXT("Do not specify struct property containing editor only properties inside an optional class."));
}
}
}
```
通过源码发现:
//SAVE_Optional = 0x00008000, ///< Indicate that we to save optional exports. This flag is only valid while cooking. Optional exports are filtered if not specified during cooking.
这个SAVE_Optional 作用于UUserDefinedEnumUUserDefinedStructUBlueprintGeneratedClass的MetaData对象上
```cpp
void UUserDefinedStruct::PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext)
{
Super::PreSaveRoot(ObjectSaveContext);
if (ObjectSaveContext.IsCooking() && (ObjectSaveContext.GetSaveFlags() & SAVE_Optional))
{
//这个对象是以this为Outer的标记RF_Standalone | RF_Public会造成该子对象被序列化下来
UStructCookedMetaData* CookedMetaData = NewCookedMetaData();
CookedMetaData->CacheMetaData(this);
if (!CookedMetaData->HasMetaData())
{
PurgeCookedMetaData();//清理掉这个CookedMetaData对象
}
}
else
{
PurgeCookedMetaData();
}
}
```
另外在cook的时候如果指定
bCookEditorOptional = Switches.Contains(TEXT("EDITOROPTIONAL")); // Produce the optional editor package data alongside the cooked data.
则会加上CookEditorOptional 的标识
CookFlags |= bCookEditorOptional ? ECookInitializationFlags::CookEditorOptional : ECookInitializationFlags::None;
再之后则会传达SAVE_Optional 给Package的SaveFlags
SaveFlags |= COTFS.IsCookFlagSet(ECookInitializationFlags::CookEditorOptional) ? SAVE_Optional : SAVE_None;
从而在包括Package的时候IsSaveOptional()的判断会造成是否创建Optional的Realm
```cpp
TArray<ESaveRealm> FSaveContext::GetHarvestedRealmsToSave()
{
TArray<ESaveRealm> HarvestedContextsToSave;
if (IsCooking())
{
HarvestedContextsToSave.Add(ESaveRealm::Game);
if (IsSaveOptional())
{
HarvestedContextsToSave.Add(ESaveRealm::Optional);
}
}
else
{
HarvestedContextsToSave.Add(ESaveRealm::Editor);
}
return HarvestedContextsToSave;
}
```
还有如果发现Object有CLASS_Optional则不把它当做Export(子对象)而是当做Import(引用的对象)Optional对象有可能放在外部独立的文件中
```cpp
ESavePackageResult HarvestPackage(FSaveContext& SaveContext)
{
// If we have a valid optional context and we are saving it,
// transform any harvested non optional export into imports
// Mark other optional import package as well
if (!SaveContext.IsSaveAutoOptional() &&
SaveContext.IsSaveOptional() &&
SaveContext.IsCooking() &&
SaveContext.GetHarvestedRealm(ESaveRealm::Optional).GetExports().Num() &&
SaveContext.GetHarvestedRealm(ESaveRealm::Game).GetExports().Num())
{
bool bHasNonOptionalSelfReference = false;
FHarvestedRealm& OptionalContext = SaveContext.GetHarvestedRealm(ESaveRealm::Optional);
for (auto It = OptionalContext.GetExports().CreateIterator(); It; ++It)
{
if (!It->Obj->GetClass()->HasAnyClassFlags(CLASS_Optional))
{
// Make sure the export is found in the game context as well
if (FTaggedExport* GameExport = SaveContext.GetHarvestedRealm(ESaveRealm::Game).GetExports().Find(It->Obj))
{
// Flag the export in the game context to generate it's public hash
GameExport->bGeneratePublicHash = true;
// Transform the export as an import
OptionalContext.AddImport(It->Obj);
// Flag the package itself to be an import
bHasNonOptionalSelfReference = true;
}
// if not found in the game context and the reference directly came from an optional object, record an illegal reference
else if (It->bFromOptionalReference)
{
SaveContext.RecordIllegalReference(nullptr, It->Obj, EIllegalRefReason::ReferenceFromOptionalToMissingGameExport);
}
It.RemoveCurrent();
}
}
// Also add the current package itself as an import if we are referencing any non optional export
if (bHasNonOptionalSelfReference)
{
OptionalContext.AddImport(SaveContext.GetPackage());
}
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,184 @@
# Transient
- **功能描述:** 指定该类的所有对象都略过序列化。
- **引擎模块:** Serialization
- **元数据类型:** bool
- **作用机制:** 在ClassFlags中添加[CLASS_Transient](../../../../Flags/EClassFlags/CLASS_Transient.md)
- **关联项:** [NonTransient](../NonTransient.md)
- **常用程度:** ★★★
指定该类的所有对象都略过序列化。
- 从不将属于此类的对象保存到磁盘。此说明符会传播到子类但是可由NonTransient说明符覆盖。可以在子类被改写.
- 会造成相应Object的RF_Transient标记。
- 注意UPROPERTY(Transient)只是指定这一个特定的属性不序列化。而UCLASS(Transient)是作用于该类的所有对象。
## 示例代码:
```cpp
UCLASS(Blueprintable, Transient)
class INSIDER_API UMyClass_Transient :public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MyProperty = 123;
};
UCLASS(Blueprintable, NonTransient)
class INSIDER_API UMyClass_NonTransient :public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MyProperty = 123;
};
UCLASS(Blueprintable, BlueprintType)
class INSIDER_API UMyClass_Transient_Test :public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UMyClass_Transient* MyTransientObject;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UMyClass_NonTransient* MyNonTransientObject;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MyInt_Normal=123;
UPROPERTY(EditAnywhere, BlueprintReadWrite,Transient)
int32 MyInt_Transient =123;
};
```
序列化对象指针关键是采用FObjectProperty里的SerializeItem方法Slot << ObjectValue;
不能采用Actor的Class Default来测试因为
Transient properties are serialized for (Blueprint) Class Default Objects but should not be serialized for any 'normal' instances of classes.
```cpp
UCLASS(Blueprintable, BlueprintType)
class INSIDER_API AMyActor_Transient_Test :public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UMyClass_Transient* MyTransientObject;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UMyClass_NonTransient* MyNonTransientObject;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MyInt_Normal=123;
UPROPERTY(EditAnywhere, BlueprintReadWrite,Transient)
int32 MyInt_Transient =123;
};
```
也不能用FObjectAndNameAsStringProxyArchive测试因为FObjectAndNameAsStringProxyArchive内部在序列化对象的时候会先查找名字
```cpp
FArchive& FObjectAndNameAsStringProxyArchive::operator<<(UObject*& Obj)
{
if (IsLoading())
{
// load the path name to the object
FString LoadedString;
InnerArchive << LoadedString;
// look up the object by fully qualified pathname
Obj = FindObject<UObject>(nullptr, *LoadedString, false);
// If we couldn't find it, and we want to load it, do that
if(!Obj && bLoadIfFindFails)
{
Obj = LoadObject<UObject>(nullptr, *LoadedString);
}
}
else
{
// save out the fully qualified object name
FString SavedString(Obj->GetPathName());
InnerArchive << SavedString;
}
return *this;
}
```
因此采用Package的测试
```cpp
FString packageName = TEXT("/Game/MyTestPackage");
FString assetPath = FPackageName::LongPackageNameToFilename(packageName, FPackageName::GetAssetPackageExtension());
UPackage* package = CreatePackage(*packageName);
FSavePackageArgs saveArgs{};
saveArgs.Error = GError;
//ObjectFlags: RF_NoFlags
UMyClass_Transient_Test* testObject = NewObject<UMyClass_Transient_Test>(package, TEXT("testObject"));
//ObjectFlags: RF_Transient
testObject->MyTransientObject = NewObject<UMyClass_Transient>(testObject, TEXT("MyTransientObject"));
//ObjectFlags: RF_NoFlags
testObject->MyNonTransientObject = NewObject<UMyClass_NonTransient>(testObject, TEXT("MyNonTransientObject"));
testObject->MyTransientObject->MyProperty = 456;
testObject->MyNonTransientObject->MyProperty = 456;
testObject->MyInt_Normal = 456;
testObject->MyInt_Transient = 456;
bool result = UPackage::SavePackage(package, testObject, *assetPath, saveArgs);
```
在保存完成之后重新加载Package:
```cpp
FString packageName = TEXT("/Game/MyTestPackage");
FString assetPath = FPackageName::LongPackageNameToFilename(packageName, FPackageName::GetAssetPackageExtension());
UPackage* package = LoadPackage(nullptr, *assetPath, LOAD_None);
package->FullyLoad();
UMyClass_Transient_Test* newTestObject=LoadObject<UMyClass_Transient_Test>(package, TEXT("testObject"),*assetPath);
```
## 示例效果:
可以看到MyTransientObject 并没有被序列化到磁盘上因此不会加载出来
![Untitled](Untitled.png)
## 原理:
在SavePackage的时候RF_Transient会导致这个对象不会被HarvestExport不会被放进SaveContext里
```cpp
void FPackageHarvester::TryHarvestExport(UObject* InObject)
{
// Those should have been already validated
check(InObject && InObject->IsInPackage(SaveContext.GetPackage()));
// Get the realm in which we should harvest this export
EIllegalRefReason Reason = EIllegalRefReason::None;
ESaveRealm HarvestContext = GetObjectHarvestingRealm(InObject, Reason);
if (!SaveContext.GetHarvestedRealm(HarvestContext).IsExport(InObject))
{
SaveContext.MarkUnsaveable(InObject);
bool bExcluded = false;
if (!InObject->HasAnyFlags(RF_Transient))
{
bExcluded = ConditionallyExcludeObjectForTarget(SaveContext, InObject, HarvestContext);
}
if (!InObject->HasAnyFlags(RF_Transient) && !bExcluded)
{
// It passed filtering so mark as export
HarvestExport(InObject, HarvestContext);
}
// If we have a illegal ref reason, record it
if (Reason != EIllegalRefReason::None)
{
SaveContext.RecordIllegalReference(CurrentExportDependencies.CurrentExport, InObject, Reason);
}
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB