UE资源合规检查工具ResScannerUE

在游戏项目开发中,因为资源量大,涉及人员广,比较难自觉地统一资源规范,如果资源出现问题,手动排查要花费大量的人力,这也是资源管理的风险和痛点。
基于这种需求,我开发并开源了一款编辑器下的资源扫描规范工具ResScannerUE,可以非常方便地配置规则并实现自动化,由美术人员在资源提交前、定期执行等方式,提前检查项目中的资源是否合规,避免在打包后才发现问题。
本篇文章介绍ResScannerUE插件的使用方法、运行机制、自定义规则的扩展方式,以及后续的优化安排。

插件介绍

ResScannerUE是一个基于AssetRegistry和反射机制实现的资源扫描工具,具有以下优点:

  1. 名字、路径规则检查无需加载资源
  2. 名字、路径规则提供通配符支持
  3. 可以通过提供属性名的方式检查资源中的选项
  4. 非常方便自定义拓展检查规则(创建蓝图或C++类实现)
  5. 支持任意资源类型,绝大部分需求都能Zero-Code实现
  6. 支持配置的导入/导出
  7. 支持Commandlet实现自动化

命名、路径、属性可以严格匹配多个,也可以从多个规则中匹配一个,可以实现类似((expression) && (expression || expression),如以T_开头,以_x_d等结尾这种模式的匹配)。

界面预览(与HotPatcher风格保持一致):

插件参数

FScannerConfig

  • ConfigName:当前配置的名字
  • bByGlobalScanFilters:开启所有Rule使用的全局扫描配置,开启后每个规则中的资源配置不生效
  • GlobalScanFilters:依赖bByGlobalScanFilters,所有Rule使用的全局扫描配置(会忽略每个Rule中指定的扫描配置)
  • GlobalIgnoreFilters:在所有规则中都忽略的资源(路径、某个资源)
  • ScannerRules:FScannerMatchRule规则数组,用于指定资源检查规则
  • bSaveConfig:是否保存配置文件
  • bSaveResult:是否保存本次执行的结果
  • SavePath:Config和Result的存储路径

FScannerMatchRule

每个规则都支持以下参数:

  • RuleName:规则名
  • RuleDescribe:规则描述信息
  • bEnableRule:是否启用当前规则
  • ScanFilters:FDirectoryPath数组,指定扫描资源路径
  • ScanAssetTypes:UClass数组,指定要扫描的资源类型,建议每个规则只指定一个类型
  • NameMatchRules:FNameMatchRule数组,用于指定命名匹配规则
  • PathMatchRules:FPathMatchRule数组,用于指定资源的路径匹配规则
  • PropertyMatchRules:FPropertyMatchRule数组,用于匹配资源中的属性
  • CustomRules:TSubclassOf<UOperatorBase>数组,用于自定义匹配规则,可以指定C++/蓝图类
  • IgnoreFilters:本规则的忽略列表,(路径、某个资源)
  • bEnablePostProcessor:是否开启对扫描结果的后处理
  • PostProcessor:TArray<TSubclassOf<UScannnerPostProcessorBase>>数组,依赖bEnablePostProcessor,可以指定对当前规则扫描的结果的后处理,如自动修改属性、自动重命名等等

FNameMatchRule

每一个NameMatchRule其中包含了多个NameRule:

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
USTRUCT(BlueprintType)
struct FNameRule
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(EditAnywhere,BlueprintReadWrite)
ENameMatchMode MatchMode;
// 匹配规则,是必须的还是可选的,Necessary是必须匹配所有的规则,Optional则只需要匹配规则中的一个
UPROPERTY(EditAnywhere,BlueprintReadWrite)
EMatchLogic MatchLogic;
// UPROPERTY(EditAnywhere,BlueprintReadWrite,meta=(EditCondition="MatchLogic == EMatchLogic::Optional"))
int32 OptionalRuleMatchNum = 1;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
TArray<FString> Rules;

};

USTRUCT(BlueprintType)
struct FNameMatchRule
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(EditAnywhere,BlueprintReadWrite)
TArray<FNameRule> Rules;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
bool bReverseCheck;
};

FPathMatchRule

每一个PathMatchRule其中包含了多个PathRule:

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
USTRUCT(BlueprintType)
struct FPathRule
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(EditAnywhere,BlueprintReadWrite)
EPathMatchMode MatchMode;
// 匹配规则,是必须的还是可选的,Necessary是必须匹配所有的规则,Optional则只需要匹配规则中的一个
UPROPERTY(EditAnywhere,BlueprintReadWrite)
EMatchLogic MatchLogic;
// UPROPERTY(EditAnywhere,BlueprintReadWrite,meta=(EditCondition="MatchLogic == EMatchLogic::Optional"))
int32 OptionalRuleMatchNum = 1;
UPROPERTY(EditAnywhere,BlueprintReadWrite,meta = (RelativeToGameContentDir, LongPackageName))
TArray<FString> Rules;
};

USTRUCT(BlueprintType)
struct FPathMatchRule
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(EditAnywhere,BlueprintReadWrite)
TArray<FPathRule> Rules;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
bool bReverseCheck;

};

FPropertyMatchRule

每个属性匹配规则,包含多个属性规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
USTRUCT(BlueprintType)
struct FPropertyRule
{
GENERATED_USTRUCT_BODY()
public:
// 匹配规则,是必须的还是可选的,Necessary是必须匹配所有的规则,Optional则只需要匹配规则中的一个
UPROPERTY(EditAnywhere,BlueprintReadWrite)
EMatchLogic MatchLogic;
// UPROPERTY(EditAnywhere,BlueprintReadWrite,meta=(EditCondition="MatchLogic == EMatchLogic::Optional"))
int32 OptionalRuleMatchNum = 1;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
TArray<FPropertyMatchMapping> Rules;

};
USTRUCT(BlueprintType)
struct FPropertyMatchRule
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(EditAnywhere,BlueprintReadWrite)
TArray<FPropertyRule> Rules;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
bool bReverseCheck;
};

匹配规则

我给ResScannerUE默认提供了三种匹配方式:命名匹配、路径匹配、属性匹配,可以实现大多数的资源扫描需求。对于每种匹配方式,也可以添加了多个约束条件。
以贴图命名规则为例:

  1. 贴图必须以T_*开头
  2. 根据贴图应用类型决定以_c_d结尾

其实就是需要由与或运算符组成两个限定条件:(T_) && ( _x || _d ),在ResScannerUE的实现中,每个NameRule,就是一个表达式,通过MatchLogic来决定内部是and/or

每个NameRule都具有一个EMatchLogic的属性,Necessary是必须严格匹配所有的Rule(T_开头),Optional是只需要对Rule中所有列出的匹配一个即可(_x/_d),路径和属性匹配也支持设置MatchLogic模式,同理。

命名匹配

对不同类型的资源制定命名规则是最常见的规范之一,如对Texture,通常以T_开头。
在一个资源检测规则中,也可以添加多个路径匹配规则,每个匹配规则有三种模式:

  1. StartWith
  2. EndWith
  3. Wildcard

并且提供了bReverseCheck,可以在所有匹配规则检测的结果上进行取反,可以实现反向匹配的作用。

以Texture的命名规范为例:

提供了T_*的通配符检查, 并且勾上了bReverseCheck,会记录不匹配这个规则的资源:

result.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"matchedAssets": [
{
"ruleName": "Texture2D_Name_Rule",
"ruleDescribe": "texture name need startwith \"T_\"",
"ruleId": 0,
"assetPackageNames": [
"/Game/Mannequin/Character/Textures/UE4_LOGO_CARD",
"/Game/Mannequin/Character/Textures/UE4_Mannequin__normals",
"/Game/Mannequin/Character/Textures/UE4_Mannequin_MAT_MASKA",
"/Game/Mannequin/Character/Textures/UE4Man_Logo_N",
"/Game/Texture/20210914162516"
]
}
]
}

路径匹配

检查某种类型的资源是否按照指定的路径存储,也是最常用的规则之一。同样支持多种路径规范,每种规范默认支持两种模式:

  1. WithIn
  2. Wildcard

可以检查是否位于某一目录下,或者路径是否满足通配符规则。同样以Texture为例,检查Texture是否都被存储于命名为*/Textures/*的目录中:

提供了*/Textures/*的通配符检查, 并且勾上了bReverseCheck,会记录不匹配这个规则的资源:

result.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"matchedAssets": [
{
"ruleName": "Texture2D_Path_Rule",
"ruleDescribe": "Texture nedd within */Textures/* folder.",
"ruleId": 1,
"assetPackageNames": [
"/Game/Mannequin/Character/Materials/MaterialLayers/T_ML_Rubber_Blue_01_D",
"/Game/Mannequin/Character/Materials/MaterialLayers/T_ML_Rubber_Blue_01_N",
"/Game/Mannequin/Character/Materials/MaterialLayers/T_ML_Aluminum01",
"/Game/Mannequin/Character/Materials/MaterialLayers/T_ML_Aluminum01_N",
"/Game/Texture/20210914162516",
"/Game/Texture/T_20210914162516"
]
}
]
}

属性匹配

检查某个资源中,是否启用了某个选项,或者检查某个属性的值,是资源合规检查中的重头。同样,对于资源属性检测也可以添加多个规则。

我基于反射实现了资源的属性匹配机制,对于任意资源类型,只要有反射标记的属性,都可以通过属性名检测值来进行匹配,对于值的检测提供了两种模式:

  1. Equal
  2. NotEqual

如:CompressionSettings被设置为TC_Default并且CompressionQuality被设置为TCQ_HighnestTCQ_High的贴图。
前面已经介绍了通过MatchLogic通过数组来实现逻辑处理的过程,在属性检测这里也格为重要。

能够检查出匹配的资源:

1
2
3
4
5
6
7
8
9
10
11
12
{
"matchedAssets": [
{
"ruleName": "Texture_CompressSetting_Rule",
"ruleDescribe": "Texture2D need Set Compress",
"ruleId": 2,
"assetPackageNames": [
"/Game/Texture/20210914162516"
]
}
]
}

注意:属性匹配,是基于属性的反射名字进行检测,UE里有DisplayName,在编辑器中显示的名字可能与真正的反射名字不一致,需要注意。

自定义规则

前面介绍了我提供的几种默认匹配规则,但是,如果有一些特殊的检测需求,我提供了一种方便的扩展方式,可以继承UOperatorBase类重载Match函数来实现自定义检测的需求。

1
2
3
4
5
6
7
8
UCLASS(Blueprintable,BlueprintType)
class UOperatorBase : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintImplementableEvent,BlueprintCallable)
bool Match(UObject* Object,const FString& AssetType);
};

会传递当前资源的UObject以及字符串的AssetType,可以通过这两个数据进行自定义的检测。

在Rule的CustomRules中指定使用的类(TSubclassOf<UOperatorBase>):

自定义规则

当然,也可以通过继承UOperatorBase创建蓝图类,一样实现重载,更方便:

当执行扫描的时候,会获取指定类的CDO,调用Match函数,获取匹配结果。

匹配后处理

插件提供了对匹配规则执行后处理的支持,如对不规范的资源属性进行自动化设置、自动重命名等需求。需要能够获取到匹配的结果。

我给每个FScannerMatchRule提供了一个指定UScannnerPostProcessorBase类的接口,如果想要对匹配规则的资源进行处理,可以继承UScannnerPostProcessorBase类实现Processor函数,会在当前规则扫描完毕之后将扫描结果传递过来。

1
2
3
4
5
6
7
8
9
10
11
for(const auto& PostProcessorClass:Rule.PostProcessors)
{
if(IsValid(PostProcessorClass))
{
UScannnerPostProcessorBase* PostProcessorIns = Cast<UScannnerPostProcessorBase>(PostProcessorClass->GetDefaultObject());
if(PostProcessorIns)
{
PostProcessorIns->Processor(RuleMatchedInfo,Rule.ScanAssetType->GetName());
}
}
}

最终扫描结果

所有规则扫描完毕之后,结果是一个FMatchedResult结构,它包含了一个FRuleMatchedInfo数据,用于记录每个规则匹配的资源列表:

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
// Rule Matched info
USTRUCT(BlueprintType)
struct FRuleMatchedInfo
{
GENERATED_USTRUCT_BODY()
public:
FRuleMatchedInfo():RuleName(TEXT("")),RuleDescribe(TEXT("")),RuleID(-1){}

UPROPERTY(EditAnywhere,BlueprintReadWrite)
FString RuleName;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
FString RuleDescribe;
// 该规则在配置数组中的下标
UPROPERTY(EditAnywhere,BlueprintReadWrite)
int32 RuleID;
UPROPERTY(EditAnywhere,BlueprintReadWrite, transient)
TArray<FAssetData> Assets;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
TArray<FString> AssetPackageNames;
};
// final result
USTRUCT(BlueprintType)
struct FMatchedResult
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(EditAnywhere,BlueprintReadWrite)
TArray<FRuleMatchedInfo> MatchedAssets;
};

RuleNameRuleDescribeRuleID可以比较方便地看出资源命中了哪条规则。

最终这个结构会被序列化为Json输出,如果想要获取原始的结果或者做一些特殊的处理,可以在UResScannerProxy调用GetScanResult()获取。

自动化检查

ResScannerUE提供了Commandlet执行的方法,可以通过指定配置文件的方式启动:

1
UE4Editor-cmd.exe PROJECT_NAME.uproject -run=ResScanner -config="res_scanner.json"

可以添加-wait参数,在执行完毕之后等待输入,可以预览结果。

使用我之前开源的另一个工具UELauncher可以比较方便地实现配置:

优化安排

一些待优化思路,有时间会逐步实现。

  • 实现属性名字的选择而不是输入
  • Detail面板根据具体类型创建属性
  • 多线程规则检测,并行化处理
全文完,若有不足之处请评论指正。

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

本文标题:UE资源合规检查工具ResScannerUE
文章作者:查利鹏
发布时间:2021年09月15日 15时29分
本文字数:本文一共有3.6k字
原始链接:https://imzlp.com/posts/11750/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!