UE 插件与工具开发:j2 的设计思路与实现

UE plug-in and tool development:j2 design ideas and implementation

如果让我挑一个近期在工作当中使用 UE 感觉最无意义和折磨的事情,那一定是根据一大串字符串路径,在 ContentBrowser 里找到它。

由于无法快速地直接跳转到目录和资源,每次逐级点开一个深层目录都给我带来一种浪费人生的感觉。

基于这种痛点,我写了一个小工具,可以极大地缓解这种手动焦虑。我把它命名为 j2(jump to),是一个极简的 ContentBrowser 跳转工具,开源在 github 上:hxhb/JumpTo

本片文章会介绍该项目的用法,以及构想解决方法的思考过程和逐步的代码实现。虽然功能本身是一个极简的插件,但对于实际痛点的发现、分析和解决过程值得记录。

j2

What is j2?

j2 是一个 UE 中可以通过 Console 快速定位到 ContentBrower 中目录或资源的工具,无需逐级手动点选目录,通过命令直接跳转。

Usage

插件提供了一个名为 j2(jump to) 的命令,利用 Console 唤起(` 键):

参数可以传递目录:

1
j2 /Game/StarterContent/Maps

或者资源路径:

1
j2 /Game/StarterContent/Textures/T_Concrete_Panels_N

并且能够支持 ContentBrowser 中对资源进行 Copy Reference 的路径:

1
j2 /Script/Engine.Texture2D'/Game/StarterContent/Textures/T_Concrete_Panels_N.T_Concrete_Panels_N'

当检测到传递的参数是资源路径,就会跳转到它所在目录下,并选中该资源。

History

由于是基于 Console 实现的,所以它天然能够保存执行的历史,实现在历史记录中快速切换:

注:Console 的执行历史记录在 [PROJECT_DIR]/Saved/Config/ConsoleHistory.ini 中。

1
2
3
4
5
[ConsoleHistory]
History=j2 /Game/StarterContent/Maps
History=j2 /Game/StarterContent/Textures/T_Concrete_Panels_N
History=j2 /Script/Engine.Texture2D'/Game/StarterContent/Textures/T_Concrete_Panels_N.T_Concrete_Panels_N'
History=j2 /Game

实现原理

需求分析

J2 的代码很简单,我花了半个小时就把代码写完了。虽然功能很简单,但还是要进行需求分析,了解究竟都是要实现什么样的功能,以及需要引擎如何支持,从而记录我实现工具的思考过程。

  1. 仅在 Editor 下运行,无需 Runtime 模块
  2. 通过 Console 唤起命令,执行函数、传递参数
  3. 从 Console 命令里提取参数
  4. 检测参数是否是有效的路径或资源
  5. 检测传入的路径类型,是目录还是资源(PackageName 或 Copy Refernce)
  6. 在当前 ContentBrowser 窗口内定位目录或资源路径

唤起命令

可以使用 FAutoConsoleCommand 定义一个 Command,并且传递一个回调函数进去:

1
2
3
4
5
static FAutoConsoleCommand J2Cmd(  
TEXT("j2"),
TEXT("jump to directory or asset."),
FConsoleCommandWithArgsDelegate::CreateStatic(&UFlibJumpToHelper::J2)
);

回调是一个接受原型为 void(const TArray< FString >&) 的函数,可以获取 Console 传递进来的参数。

然后在 Console 中就能识别:

接受参数

FAutoConsoleCommand 对象绑定的回调函数的参数中即可获得:

1
2
3
4
5
6
7
void J2(const TArray<FString>& Args)
{
if(Args.Num())
{
FString JumpTo = Args[0];
}
}

因为只需要跳转到一个路径,所以只取第一个参数即可。

检查参数是否为有效

对于传递进来的参数,需要检测是否为有效的资源或路径。

资源

对于资源而言,为了同时支持 LongPackageName 和 Copy Reference 的两种形式,需要进行两次检测。

如果传递的是 LongPackageName,则路径形式为 /Game/XXXX/YYYY,则可以直接使用 FPackageNameDoesPackageExist 函数进行检查:

1
bool bPackageExist = FPackageName::DoesPackageExist(JumpTo);

如果是 Copy Reference 的资源形式,其表达形式为:

1
2
Class'/Game/XXX/YYY.YYYY'
/Script/Engine.Texture2D'/Game/StarterContent/Textures/T_Concrete_Panels_N.T_Concrete_Panels_N'

对它的检测可以通过正则表达式来进行,其正则模式为'[^']*/[^']*'

1
2
TArray<FString> OutRegStrings;
bool bIsCopyRef = UFlibJumpToHelper::IsMatchRegExp(JumpTo,REF_REGEX_TEXT,OutRegStrings) && OutRegStrings.Num();

IsMatchRegExp 是我在插件内基于引擎内的 FRegexMatcher 实现的,用于检查字符串是否匹配某个正则的函数,并会输出匹配正则的子串,借此也可以提取出来 CopyReference 的实际资源路径。

并且它也可以用在所有需要匹配正则表达式的逻辑中,我在 ResScanner 的命名与路径的正则匹配都使用了它。完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool UFlibJumpToHelper::IsMatchRegExp(const FString& Str, const FString& RegExp,TArray<FString>& OutMatchStrs)
{
FRegexPattern Pattern(RegExp);
FRegexMatcher PattenMatcher(Pattern,Str);
struct FRegexMatchResult{ int32 Begin,End; };
TArray<FRegexMatchResult> Results;
while(PattenMatcher.FindNext())
{
FRegexMatchResult MatchStrInfo;
MatchStrInfo.Begin = PattenMatcher.GetMatchBeginning();
MatchStrInfo.End = PattenMatcher.GetMatchEnding();
if(MatchStrInfo.End > MatchStrInfo.Begin)
{
Results.Add(MatchStrInfo);
FString MidStr = Str.Mid(MatchStrInfo.Begin,MatchStrInfo.End - MatchStrInfo.Begin);
OutMatchStrs.AddUnique(MidStr);
}
}
return !!Results.Num();
}

路径

对于传进来的参数是路径,就需要检查该路径是否为有效的资源目录,来决定是否执行跳转。

至于什么是有效的资源目录,首先要牵扯出来一个 RootPaths 的概念,当引擎项目和插件注册时,会添加模块目录的 Content 到 RootPaths 里,可以识别不同的模块中的资源。

如:

1
2
3
4
5
/Engine/xxx
/Game/XXX
/Paper2D/XXX
/HotPatcher/XXX
/ResScanner/xxx

等等形式都是基于所属模块的 RootPath。当然,我需要 j2 执行跳转任何模块中的资源,而不局限于 /Game 或者 /Engine
这就需要有一个通用的检查资源目录是否存在的方式,但我没查到能够直接使用的函数,于是想了一个变通的方法。

在 UE 中,FPackageName 有一个 TryConvertLongPackageNameToFilename 函数,可以把资源的 LongPackagName 转换为磁盘的 uasset 路径。

1
2
FString AssetAbsPath;  
FPackageName::TryConvertLongPackageNameToFilename(TEXT("/Game/StarterContent/Textures/T_Burst_M"),AssetAbsPath,FPackageName::GetAssetPackageExtension());

将会获取到:

1
E:\UnrealProjects\BlankUE5\Content\StarterContent\Textures\T_Burst_M.uasset

它的作用就是把引擎中的资产路径转换为磁盘上的 uasset 路径,但并不绝对。通过控制参数也能够实现对目录的转换。

如,想要获取 /Game/StarterContent 的磁盘目录路径:

1
2
FString StarterContentAbsPath;  
FPackageName::TryConvertLongPackageNameToFilename(TEXT("/Game/StarterContent"),StarterContentAbsPath,TEXT(""));

即把目录也当作资源,只是传递空的 Extension 参数。它就能转换为绝对路径了:

1
E:\UnrealProjects\BlankUE5\Content\StarterContent

然后再使用 FPaths::DirectoryExists 检查绝对路径是否存在即可,这样就能实现对引擎中所有 RootPath 目录的检查了。

跳转到路径

其实在 Content Browser 中,对目录右键,就有一个 Show In New Content Browser 的选项:

我想要的就是这样的功能,但是打开的路径由代码控制。

查看了引擎中的代码,该功能的代码为:

Engine\Source\Editor\ContentBrowser\Private\SContentBrowser.cpp
1
2
3
4
5
void SContentBrowser::OpenNewContentBrowser()  
{
const TArray<FContentBrowserItem> SelectedFolders = PathContextMenu->GetSelectedFolders();
FContentBrowserSingleton::Get().SyncBrowserToItems(SelectedFolders, false, true, NAME_None, true);
}

其中的 SyncBrowserToItems 就是我们需要的函数啦。

顺便查看所属类 FContentBrowserSingleton 的定义,发现还有一个 SyncBrowserToFolders 函数,只传递字符串就可以了,更能够符合需要。原型如下:

1
virtual void SyncBrowserToFolders(const TArray<FString>& FolderList, bool bAllowLockedBrowsers = false, bool bFocusContentBrowser = true, const FName& InstanceName = FName(), bool bNewSpawnBrowser = false);

在 J2 函数中直接调用,根据 Console 传递进来的路径传递给 FolderList 即可。

注意,需要删除路径结尾多余的 /,如 /Game/XXXX/

1
2
while(JumpTo.RemoveFromEnd(TEXT("/"))){};  
ContentBrowserModule.Get().SyncBrowserToFolders(TArray<FString>{JumpTo},true, true, NAME_None, false);

这样就能够实现在当前 ContentBrower 里定位到指定目录了,如果当前编辑器 Layout 不存在 ContentBrowser,会自动创建一个。
如果还想要在新的 ContentBrowser 面板里打开,可以给 bNewSpawnBrowser 参数传递 true

跳转到资源

跳转到资源与跳转到路径有些区别,本质上是先跳转到资源所在路径,再在当前路径下定位到资源。

在类 FContentBrowserSingleton 的定义中也有如下接口:

1
virtual void SyncBrowserToAssets(const TArray<struct FAssetData>& AssetDataList, bool bAllowLockedBrowsers = false, bool bFocusContentBrowser = true, const FName& InstanceName = FName(), bool bNewSpawnBrowser = false);

与打开目录的形式类似,但需要传递资源的 AssetData

因为我们传递资源的 LongPackageName 和 Copy Reference 都是资源全路径,所以能够从 AssetRegistry 中获取传递资源的 FAssetData

1
2
3
4
5
6
7
8
FAssetData GetAssetDataByLongPackageName(FName LongPackageNames)  
{
UAssetManager& AssetManager = UAssetManager::Get();
FAssetData AssetDataForPath;
FSoftObjectPath PackageObjectPath = LongPackageNameToPackagePath(LongPackageNames.ToString());
AssetManager.GetAssetDataForPath(PackageObjectPath, AssetDataForPath);
return AssetDataForPath;
}

注意,如果传递的是 LongPackageName,则需要转换为 ObjectPath 的形式。

然后调用 SyncBrowserToAssets 即可(注意需要检查获取到的 FAssetData 是否有效):

1
2
3
4
5
FAssetData AssetData = GetAssetDataByLongPackageName(*JumpTo);  
if(AssetData.IsValid())
{
ContentBrowserModule.Get().SyncBrowserToAssets(TArray<FAssetData>{AssetData},true, true, NAME_None, false);
}

这样就能够实现在 ContentBrowser 里跳转到资源的效果了。

结语

通过该插件,我再也不担心从路径中找资源了。就算别人发来一长串路径,复制粘贴文本就能跳转,心情非常舒畅。

顺便将其作为一个插件开发的案例,梳理了从发现需求,到构想解决,再到实际实现的全过程。作为我日常开发中处理问题的一个缩影。

只有真正的发现问题,分析问题。那么解决问题则就是处理一个个具体点的过程,再充分利用引擎已有的特性,自然就很顺畅了。

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

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

本文标题:UE 插件与工具开发:j2 的设计思路与实现
文章作者: 查利鹏
发布时间:2023/10/14 20:23
本文字数:3k 字
原始链接:https://imzlp.com/posts/86105/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!
Powered By Valine
v1.4.14