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

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

If I had to choose one recent experience in working with UE that felt the most meaningless and torturous, it would definitely be trying to find a resource in the ContentBrowser based on a long string path.

The inability to quickly jump directly to a directory or resource makes me feel like I’m wasting my life every time I have to navigate deeply nested folders one step at a time.

To address this pain point, I wrote a small tool that significantly alleviates this manual anxiety. I named it j2 (jump to), which is a minimalist ContentBrowser jump tool, open-source on GitHub: hxhb/JumpTo.

This article will introduce the usage of this project, the thought process behind conceptualizing the solution, and the step-by-step code implementation. Although the functionality itself is a very simple plugin, the discovery, analysis, and solution process for the actual pain points is worth documenting.

j2

What is j2?

j2 is a tool that allows you to quickly locate directories or resources in the ContentBrowser through the Console in UE without having to manually click through each folder level, enabling direct jumps via commands.

Usage

The plugin provides a command named j2 (jump to), which can be invoked via the Console (` key):

Parameters can be passed for directories:

1
j2 /Game/StarterContent/Maps

Or resource paths:

1
j2 /Game/StarterContent/Textures/T_Concrete_Panels_N

It also supports paths for resources copied from the ContentBrowser:

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

When the passed parameter is a resource path, it jumps to the directory containing the resource and selects it.

History

Since it’s based on the Console, it can naturally save execution history, allowing quick switching between recorded commands:

Note: The Console execution history is stored in [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

Implementation Principle

Requirement Analysis

The code for j2 is very simple; I finished writing it in half an hour. Although the functionality is straightforward, a requirement analysis was still necessary to understand what features to implement and what support was needed from the engine, thus documenting my thought process in creating the tool.

  1. Only runs in the Editor; no need for Runtime modules.
  2. Invoke commands via Console, executing functions and passing parameters.
  3. Extract parameters from Console commands.
  4. Check if parameters are valid paths or resources.
  5. Determine the type of incoming path: is it a directory or resource (PackageName or Copy Reference).
  6. Locate the directory or resource path within the current ContentBrowser window.

Invoking Commands

You can use FAutoConsoleCommand to define a command and pass in a callback function:

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

The callback is a function with the prototype void(const TArray< FString >&), which can retrieve parameters passed from the Console.

Then it can be recognized in the Console:

Accepting Parameters

Parameters can be obtained from the callback function bound to the FAutoConsoleCommand object:

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

Since only one path needs to be jumped to, we only take the first parameter.

Validating Parameters

Incoming parameters need to be checked to determine whether they are valid resources or paths.

Resources

For resources, we need to check both LongPackageName and Copy Reference formats simultaneously, requiring two checks.

If the input is a LongPackageName, its format is /Game/XXXX/YYYY, so we can check it directly using FPackageName‘s DoesPackageExist function:

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

If it’s in the Copy Reference format, it looks like:

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

Validation can be performed using a regular expression with the pattern '[^']*/[^']*':

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

IsMatchRegExp is a function I implemented in the plugin based on the engine’s FRegexMatcher, used to check if a string matches a specific regex and can also extract the actual resource path from the Copy Reference.

It can also be used in all logical scenarios requiring regex matching; I used it in ResScanner for naming and path regex matching. The complete code is as follows:

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();
}

Paths

For parameters passed as paths, we need to check whether they are valid resource directories to determine if the jump should proceed.

The concept of RootPaths comes into play here. When the engine project and plugin are registered, the content directory is added to RootPaths, allowing recognition of resources from different modules.

For example:

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

All these forms are based on the respective module’s RootPath. Of course, I want j2 to jump to resources in any module, not limited to /Game or /Engine.
This requires a universal method to check if a resource directory exists, but I couldn’t find a directly usable function, so I devised an alternative approach.

In UE, FPackageName has a function TryConvertLongPackageNameToFilename, which can convert the resource’s LongPackageName to the disk path of the uasset.

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

This will return:

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

Its purpose is to transform the asset path in the engine into the corresponding uasset path on disk, though it’s not absolute. Moreover, converting a directory path can be achieved by controlling parameters.

For example, to get the disk path of /Game/StarterContent:

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

Just treat the directory as a resource and pass an empty Extension parameter. It will convert to the absolute path:

1
E:\UnrealProjects\BlankUE5\Content\StarterContent

Then use FPaths::DirectoryExists to check if the absolute path exists, which allows for checking all RootPath directories in the engine.

Jumping to a Path

In the Content Browser, right-clicking a directory gives a Show In New Content Browser option:

What I want is a function like this, but controlled by code regarding the path opened.

I examined the engine code, and the functionality is implemented with:

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

The SyncBrowserToItems function is what we need.

A brief look at the defining class FContentBrowserSingleton revealed an even better-suited function, SyncBrowserToFolders, which only requires a string to be passed. The prototype is as follows:

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

We can directly call this in the J2 function and pass the path received from the Console to FolderList.

Note: Be sure to remove any trailing slashes from the path, such as /Game/XXXX/.

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

This enables locating the specified directory in the current ContentBrowser. If the current editor layout does not contain a ContentBrowser, one will be automatically created.
If you want to open in a new ContentBrowser panel, you can pass true for the bNewSpawnBrowser parameter.

Jumping to a Resource

Jumping to a resource has some differences compared to jumping to a path; essentially, it first jumps to the resource’s directory and then navigates to the resource within the current directory.

In the class FContentBrowserSingleton, there’s a similar interface:

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

Similar to opening a directory but requires passing the resource’s AssetData.

Since the LongPackageName and Copy Reference we pass are full resource paths, we can retrieve the corresponding FAssetData from the AssetRegistry.

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;
}

Note: If the input is in LongPackageName format, it needs to be converted to ObjectPath format.

Then we can call SyncBrowserToAssets (also ensure that the obtained FAssetData is valid):

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

This achieves the effect of jumping to the resource in the ContentBrowser.

Conclusion

With this plugin, I no longer worry about finding resources from paths. Even if someone sends a long string of paths, I can jump to it by simply copying and pasting, which is incredibly satisfying.

Additionally, this serves as a case study in plugin development, tracing the entire process from discovering the need, to conceptualizing a solution, and finally implementing it. It reflects my daily problem-solving approach in development.

Only by truly discovering issues and analyzing them can we approach resolving them as a process of handling specific points while making full use of the existing features of the engine to achieve a smooth outcome.

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE plug-in and tool development:j2 design ideas and implementation
Author:LIPENGZHA
Publish Date:2023/10/14 20:23
Word Count:6.4k Words
Link:https://en.imzlp.com/posts/86105/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!