如果让我挑一个近期在工作当中使用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传递进来的参数。
然后在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里跳转到资源的效果了。
结语
通过该插件,我再也不担心从路径中找资源了。就算别人发来一长串路径,复制粘贴文本就能跳转,心情非常舒畅。
顺便将其作为一个插件开发的案例,梳理了从发现需求,到构想解决,再到实际实现的全过程。作为我日常开发中处理问题的一个缩影。
只有真正的发现问题,分析问题。那么解决问题则就是处理一个个具体点的过程,再充分利用引擎已有的特性,自然就很顺畅了。