UE中利用反射为资产建立属性缓存

Use reflection to create properties cache for assets in UE

在前一篇文章UE中资源自修正的设计与实现方案中,我介绍了利用ResScannerUE的资产检查进行过滤,然后自动化处理的实现方案。

在通常情况下,如果想要检查资源内的某个属性,就需要把资产加载进来,获取对象:

但是,如果想要批量地检查资源的属性,用这种形式挨个加载资源的耗时很久,尤其是在没有DDC的情况下,加载资源就会触发DDC缓存的构建,这部分耗时很久,对于机器性能占比也较高。

而且随着资产规模的扩大,如果想要完整扫描一遍工程里的所有资源,耗时就会异常夸张。所以,我设想能否实现一种无需加载资源,但能够获取资源内属性的方法。

经过研究发现,通过一种取巧的方法做到这一点,利用了反射、资产序列化、以及AssetRegistry的特性组合起来。并且是非侵入式的,无需修改任何现有资源类型的代码。

本篇文章会介绍其实现原理,以及对引擎中相关逻辑的分析,并介绍该机制在工程中的实际应用。

前言

在最初的设想中,我的一个思路是在编辑时把资源的属性缓存下来单独存储,并会记录缓存所属资源的UUID值,用于验证资源与缓存的一致性,后续就从这个缓存中统一读取。

但后来放弃了这个想法,因为资源变更涉及到很多的人,每个人本地都是一个独立的运行环境,该如何同步生成的属性缓存呢?如果每台机器独立生成,假如多人编辑同一份资源,该如何解决冲突呢?由一个问题带出多个潜在的问题,在这种情况只能用作本地的临时缓存,而无法作为通用的全局数据。

但期望建立资产属性缓存的总体思路没变,关键就是如何把属性缓存起来能够在整个项目中共用。流程如下:

并且最好不需要额外的管理和同步成本,完全无感知是最好的。

属性缓存

本篇文章的核心部分,在于建立资产的属性缓存,以及如何存储。

在UE中,其实本身能够对部分属性的值进行缓存。在ContentBrowser中鼠标Hover资源,就会弹出资源的AssetTips展示资产的基础信息以及部分属性:
AnimSequence

Texture
而引擎获取展示出来的这些属性和值,不需要加载资源。这些属性都是在资产所属的类型的C++里标记了AssetRegistrySearchable的,以UTexture为例:

Engine/Texture.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** A bias to the index of the top mip level to use. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=LevelOfDetail, meta=(DisplayName="LOD Bias"), AssetRegistrySearchable)
int32 LODBias;

/** Compression settings to use when building the texture. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Compression, AssetRegistrySearchable)
TEnumAsByte<enum TextureCompressionSettings> CompressionSettings;

/** The texture filtering mode to use when sampling this texture. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Texture, AssetRegistrySearchable, AdvancedDisplay)
TEnumAsByte<enum TextureFilter> Filter;

/** The texture mip load options. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Texture, AssetRegistrySearchable, AdvancedDisplay)
ETextureMipLoadOptions MipLoadOptions;

/** Texture group this texture belongs to */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=LevelOfDetail, meta=(DisplayName="Texture Group"), AssetRegistrySearchable)
TEnumAsByte<enum TextureGroup> LODGroup;

/** This should be unchecked if using alpha channels individually as masks. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Texture, meta=(DisplayName="sRGB"), AssetRegistrySearchable)
uint8 SRGB:1;

它们会在保存资产时被序列化到uasset之内,并且会在引擎启动时为资产生成AssetData的数据并添加到AssetRegistry,用于在编辑器内快速筛选资产(如只展示某种类型的资源),这也是ContentBrowser中能够快速搜索和过滤资产的原因。

而我就是要利用这个特性来实现对资源属性缓存需求。

属性反射FLAG

前面讲到了,能够在AssetTips中展示的属性,都是被标记了为AssetRegistrySearchable的,那么我们要分析一下,UPROPERTY的这个反射标记具体做了什么内容。

同样以UTexture中的CompressionSettingsSRGB为例:

1
2
3
4
5
6
/** Compression settings to use when building the texture. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Compression, AssetRegistrySearchable)
TEnumAsByte<enum TextureCompressionSettings> CompressionSettings;
/** This should be unchecked if using alpha channels individually as masks. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Texture, meta=(DisplayName="sRGB"), AssetRegistrySearchable)
uint8 SRGB:1;

其反射属性生成的反射代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// CompressionSettings
const UE4CodeGen_Private::FBytePropertyParams Z_Construct_UClass_UTexture_Statics::NewProp_CompressionSettings = {
"CompressionSettings",
nullptr,
(EPropertyFlags)0x0010010000000005,
UE4CodeGen_Private::EPropertyGenFlags::Byte,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(UTexture, CompressionSettings),
Z_Construct_UEnum_Engine_TextureCompressionSettings,
METADATA_PARAMS(Z_Construct_UClass_UTexture_Statics::NewProp_CompressionSettings_MetaData, UE_ARRAY_COUNT(Z_Construct_UClass_UTexture_Statics::NewProp_CompressionSettings_MetaData))
};

// SRGB
const UE4CodeGen_Private::FBoolPropertyParams Z_Construct_UClass_UTexture_Statics::NewProp_SRGB = {
"SRGB",
nullptr,
(EPropertyFlags)0x0010010000000005,
UE4CodeGen_Private::EPropertyGenFlags::Bool ,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
sizeof(uint8),
sizeof(UTexture),
&Z_Construct_UClass_UTexture_Statics::NewProp_SRGB_SetBit,
METADATA_PARAMS(Z_Construct_UClass_UTexture_Statics::NewProp_SRGB_MetaData, UE_ARRAY_COUNT(Z_Construct_UClass_UTexture_Statics::NewProp_SRGB_MetaData))
};

它们的共同之处,是PropertyFlags都为0x0010010000000005,它是一个掩码方式存储的值,具有多个FLAG值:

1
2
3
4
CPF_Edit                         = 0x0000000000000001,  ///< Property is user-settable in the editor.
CPF_BlueprintVisible = 0x0000000000000004, ///< This property can be read by blueprint code
CPF_AssetRegistrySearchable = 0x0000010000000000, ///< asset instances will add properties with this flag to the asset registry automatically
CPF_NativeAccessSpecifierPublic = 0x0010000000000000, ///< Public native access specifier

用于标记该反射属性的用途。在这个例子里即:一个编辑器中可编辑、能够在蓝图中显示、能够被AssetRegistry搜索到的属性、并且是public的。
在编辑器或者其他需要访问该反射属性的地方,就能够通过这些FLAG来判断当前属性是否应该进行处理。

而把UPROPERTY标记加上AssetRegistrySearchable就会给反射属性加上CPF_AssetRegistrySearchable,使其能够被AssetRegistry系统识别,并且能够正确地序列化到uasset中去。

序列化

前一节提到了给属性添加AssetRegistrySearchable标记,就能够序列化到uasset之内,那么引擎是如何实现它的存储以及进行读取的,会在本节介绍。

保存

在引擎中保存资源时,会触发AssetRegistryData的序列化,调用栈如下:

其中会获取当前资源中所有的Object中标记了AssetRegistrySearchable的属性的值TAG,并将其序列化到资产之内:

其Tag的最终序列化内容为:

它是一个Key-Value形式的数组,Key就是该反射属性的字符串名字,而Value就是当前资源该属性的值,并且是以ExportTextureItem的形式导出的字符串值(与UE中资源自修正的设计与实现方案#通用的属性替换实现文章中的实现思路异曲同工),从而实现任意属性的通用存储。

读取

在文章资源管理:UASSET资源加密方案中,我曾介绍了UE中对资源加密的内容,其中具有Package Summary的部分。

在Summary中,有一个专门的属性,用于标记AssetRegistryData在当前资产中的文件偏移:

基于这个偏移,就能够在只读取了PackageSummary的情况下,无需完整地加载资源,就能够直接获取到AssetRegistryData

FPackageReader::ReadAssetRegistryData中会读取uasset文件,并构造出FAssetData

然后将其添加到AssetDataList中:

1
2
3
4
5
6
7
8
9
10
11
12
// Create a new FAssetData for this asset and update it with the gathered data
AssetDataList.Add(
new FAssetData(
FName(*PackageName),
FName(*PackagePath),
FName(*AssetName),
FName(*ObjectClassName),
MoveTemp(TagsAndValues),
PackageFileSummary.ChunkIDs,
PackageFileSummary.PackageFlags
)
);

这样就能够直接从AssetRegistry里获取已缓存资源的AssetData了。

非侵入式实现

前面介绍了反射属性添加AssetRegistrySearchable的反射代码,以及在资源保存时存储、引擎启动时从uasset加载AssetData的实现逻辑。

简单来说,通过给属性添加AssetRegistrySearchable的FLAG,就能够把属性序列化到资产之内,并且无需加载资源就能够访问。

这就把我们前面列举的需求实现了一半了。除此之外还有一个很重要的问题:对于没有在代码中添加AssetRegistrySearchable反射标记的属性该怎么办呢?
如果只能手动添加AssetRegistrySearchable到需要的属性中,每当有一个新的属性想要获取,就要去改它的C++代码,这是不可接受的,侵入式地修改局限太大。

那么,有什么可以不用修改代码,就能够动态地给属性添加AssetRegistrySearchable吗?

有!就是利用UE的反射动态修改UClass中FProperty的反射FLAG,在之前的文章UE:Hook UObject#修改UClass控制反射属性中我曾介绍过类似的实现方法。

而对于本篇文章中的需求来说,就需要给FProperty添加CPF_AssetRegistrySearchable这个FLAG:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
FProperty* GetPropertyByName(UClass* Class, FName PropertyName)
{
FProperty* Property = nullptr;
for(TFieldIterator<FProperty> PropertyIter(Class);PropertyIter;++PropertyIter)
{
FProperty* PropertyIns = *PropertyIter;
if(PropertyIns->GetName().Equals(PropertyName.ToString()))
{
Property = PropertyIns;
}
}
return Property;
}

void AddAssetRegistrySearchableToProperty(UClass* Class, const FString& PropertyName,
bool bSearchable)
{
FProperty* Property = GetPropertyByName(Class,*PropertyName);
if(!Property->HasAnyPropertyFlags(EPropertyFlags::CPF_AssetRegistrySearchable))
{
if(bSearchable)
{
Property->SetPropertyFlags(EPropertyFlags::CPF_AssetRegistrySearchable);
}
else
{
Property->ClearPropertyFlags(EPropertyFlags::CPF_AssetRegistrySearchable);
}
}
}

基于前面资源保存时对AssetRegistryData序列化的分析,我们只需要在实际保存资源之前,修改资源类型的UClass即可。

AnimSequence里的BoneCompressionSettings属性为例,它并没有添加AssetRegistrySearchable标记:

1
2
3
/** The bone compression settings used to compress bones in this sequence. */  
UPROPERTY(Category = Compression, EditAnywhere, meta = (ForceShowEngineContent))
class UAnimBoneCompressionSettings* BoneCompressionSettings;

在引擎运行时就可以执行代码:

1
AddAssetRegistrySearchableToProperty(UAnimSequence::StaticClass(),TEXT("BoneCompressionSettings"),true);

然后保存资源,在GetAssetRegistryTagFromProperty中就能够检测到它具有CPF_AssetRegistrySearchableFLAG了:

并且能够把实际的属性值导出为字符串:

这样就能完美地实现我们想要把一些属性序列化到资源之内的需求了。

复用与一致性

因为AssetData是直接序列化到资源之内的,会随着每次资源的保存而更新。所以只要能够更新到最新的资源,就能够保证其属性值的准确性。

而且对于协同来说,每个人都会保存自己修改的资源,如果不同提交间的资源产生冲突,可以通过版本控制解决。这同样能够保证远程仓库里的资源和其缓存属性值的一致性(都存储在uasset单个文件之内),不会出现混乱。

缓存属性读取

至此,对于资产属性的缓存已经完全实现了,那么如何应用它呢?

在ResScanner的应用:

1
2
3
4
5
6
7
auto GetPropertyValueByName = [&Rule,this](const FAssetData& AssetData,const FString& PropertyName)  
{
FString AssetDataTagValue;
if(!AssetData.GetTagValue(*PropertyName,AssetDataTagValue))
{ AssetDataTagValue = UFlibAssetParseHelper::GetPropertyValueByName(GetPropertyOwning(AssetData,Rule),PropertyName);
} return AssetDataTagValue;
};

只有在从TagsAndValues获取不到属性时,再进行资源加载。

对于ResScannerUE来说,可能存在很多进行属性检查的规则。在扫描模块启动前会自动获取所有包含属性检查的规则,并为要检查的属性添加CPF_AssetRegistrySearchable,不管添加了多少规则,都能达到无感知的目的。

结语

本文涉及到的一些知识,在博客中其他文章内有更具体的总结和分析,可以作为扩展阅读。

本篇文章分析并利用了引擎中类型反射、资产序列化、以及AssetRegistry数据生成等特性,并把它们组合起来实现了一种非侵入式的,无需修改任何现有资源类型的代码进行资源属性缓存的机制。

实现在不加载资源的情况下去获取资源内的属性,对于“资源的属性检查”这种需求,就不会随着资源规模的扩张导致检查耗时的大幅增加,也不会因为本地环境没有DDC在扫描时生成,能够大幅度提升检查效率。

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:UE中利用反射为资产建立属性缓存
文章作者:查利鹏
发布时间:2023年10月25日 15时27分
本文字数:本文一共有3.7k字
原始链接:https://imzlp.com/posts/71162/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!