如果让我挑一个近期在工作当中使用 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 | [ConsoleHistory] |
实现原理
需求分析
J2 的代码很简单,我花了半个小时就把代码写完了。虽然功能很简单,但还是要进行需求分析,了解究竟都是要实现什么样的功能,以及需要引擎如何支持,从而记录我实现工具的思考过程。
- 仅在 Editor 下运行,无需 Runtime 模块
- 通过 Console 唤起命令,执行函数、传递参数
- 从 Console 命令里提取参数
- 检测参数是否是有效的路径或资源
- 检测传入的路径类型,是目录还是资源(PackageName 或 Copy Refernce)
- 在当前 ContentBrowser 窗口内定位目录或资源路径
唤起命令
可以使用 FAutoConsoleCommand 定义一个 Command,并且传递一个回调函数进去:
1 | static FAutoConsoleCommand J2Cmd( |
回调是一个接受原型为 void(const TArray< FString >&)
的函数,可以获取 Console 传递进来的参数。
接受参数
从 FAutoConsoleCommand
对象绑定的回调函数的参数中即可获得:
1 | void J2(const TArray<FString>& Args) |
因为只需要跳转到一个路径,所以只取第一个参数即可。
检查参数是否为有效
对于传递进来的参数,需要检测是否为有效的资源或路径。
资源
对于资源而言,为了同时支持 LongPackageName 和 Copy Reference 的两种形式,需要进行两次检测。
如果传递的是 LongPackageName,则路径形式为 /Game/XXXX/YYYY
,则可以直接使用 FPackageName
的 DoesPackageExist
函数进行检查:
1 | bool bPackageExist = FPackageName::DoesPackageExist(JumpTo); |
如果是 Copy Reference
的资源形式,其表达形式为:
1 | Class'/Game/XXX/YYY.YYYY' |
对它的检测可以通过正则表达式来进行,其正则模式为'[^']*/[^']*'
:
1 | TArray<FString> OutRegStrings; |
IsMatchRegExp
是我在插件内基于引擎内的 FRegexMatcher
实现的,用于检查字符串是否匹配某个正则的函数,并会输出匹配正则的子串,借此也可以提取出来 CopyReference 的实际资源路径。
并且它也可以用在所有需要匹配正则表达式的逻辑中,我在 ResScanner 的命名与路径的正则匹配都使用了它。完整代码如下:
1 | bool UFlibJumpToHelper::IsMatchRegExp(const FString& Str, const FString& RegExp,TArray<FString>& OutMatchStrs) |
路径
对于传进来的参数是路径,就需要检查该路径是否为有效的资源目录,来决定是否执行跳转。
至于什么是有效的资源目录,首先要牵扯出来一个 RootPaths 的概念,当引擎项目和插件注册时,会添加模块目录的 Content 到 RootPaths 里,可以识别不同的模块中的资源。
如:
1 | /Engine/xxx |
等等形式都是基于所属模块的 RootPath。当然,我需要 j2 执行跳转任何模块中的资源,而不局限于 /Game
或者 /Engine
。
这就需要有一个通用的检查资源目录是否存在的方式,但我没查到能够直接使用的函数,于是想了一个变通的方法。
在 UE 中,FPackageName
有一个 TryConvertLongPackageNameToFilename
函数,可以把资源的 LongPackagName 转换为磁盘的 uasset 路径。
1 | FString AssetAbsPath; |
将会获取到:
1 | E:\UnrealProjects\BlankUE5\Content\StarterContent\Textures\T_Burst_M.uasset |
它的作用就是把引擎中的资产路径转换为磁盘上的 uasset 路径,但并不绝对。通过控制参数也能够实现对目录的转换。
如,想要获取 /Game/StarterContent
的磁盘目录路径:
1 | FString StarterContentAbsPath; |
即把目录也当作资源,只是传递空的 Extension
参数。它就能转换为绝对路径了:
1 | E:\UnrealProjects\BlankUE5\Content\StarterContent |
然后再使用 FPaths::DirectoryExists
检查绝对路径是否存在即可,这样就能实现对引擎中所有 RootPath 目录的检查了。
跳转到路径
其实在 Content Browser 中,对目录右键,就有一个 Show In New Content Browser
的选项:
我想要的就是这样的功能,但是打开的路径由代码控制。
查看了引擎中的代码,该功能的代码为:
1 | void SContentBrowser::OpenNewContentBrowser() |
其中的 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 | while(JumpTo.RemoveFromEnd(TEXT("/"))){}; |
这样就能够实现在当前 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 | FAssetData GetAssetDataByLongPackageName(FName LongPackageNames) |
注意,如果传递的是 LongPackageName,则需要转换为
ObjectPath
的形式。
然后调用 SyncBrowserToAssets
即可(注意需要检查获取到的 FAssetData
是否有效):
1 | FAssetData AssetData = GetAssetDataByLongPackageName(*JumpTo); |
这样就能够实现在 ContentBrowser 里跳转到资源的效果了。
结语
通过该插件,我再也不担心从路径中找资源了。就算别人发来一长串路径,复制粘贴文本就能跳转,心情非常舒畅。
顺便将其作为一个插件开发的案例,梳理了从发现需求,到构想解决,再到实际实现的全过程。作为我日常开发中处理问题的一个缩影。
只有真正的发现问题,分析问题。那么解决问题则就是处理一个个具体点的过程,再充分利用引擎已有的特性,自然就很顺畅了。
v1.4.14