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
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!