Unreal 5.0 C++- Custom Editor Tools through Plugin Development


This project is a UE5 C++ editor plugin that extends three core editor surfaces: the Content Browser, the Level Editor viewport context menu, and the Scene Outliner. I built it following a Udemy course on editor extension, but the interesting part of the work happened after the course ended — reviewing the architecture, fixing bugs I found along the way, and making the code match the standards I hold production code to.

📺 Demo Video  |  🔗 GitHub Repository  |  📖 Learning Source


What the plugin does

The plugin is structured around a central IModuleInterfaceFSuperManagerModule — that acts as coordinator for several independent subsystems. Rather than having each subsystem manage its own editor integrations, everything routes back through the module. That choice turned out to be load-bearing, and I’ll explain why below.

Content Browser extensions

Right-clicking a folder in the Content Browser adds three entries after the built-in Delete section, injected via FContentBrowserModule::GetAllPathViewContextMenuExtenders():

Delete Unused Assets scans the selected folder for assets with zero package referencers. Before doing anything, it calls FixUpRedirectors() — this matters because stale ObjectRedirector assets can make a genuinely unused asset appear as referenced. Skipping this step is a common mistake that produces false negatives.

Delete Empty Folders does a recursive scan for subdirectories that contain no assets, collects their paths, presents them to the user for confirmation, and then calls UEditorAssetLibrary::DeleteDirectory() on each.

Advanced Deletion opens a custom Slate dock tab.

The Advanced Deletion tab

This is the most involved piece of the plugin. It’s a nomad dock tab whose content is an SCompoundWidget subclass (SAdvancedDeletionTab) that renders an interactive asset list for the selected folder.

The layout has four rows: a title, a filter bar with a combo box and help text, a scrollable list view, and a bottom bar with batch action buttons. Each row in the list has a checkbox, the asset’s class name, its asset name, and a per-row Delete button.

The combo box supports three filter modes — List All, List Unused Assets, and List Assets With The Same Name. The filtering logic lives in the module (FillUnusedAssets, FillSameNameAssets), not in the widget. The widget calls back into the module and receives a filtered array — it doesn’t touch the Asset Registry directly. This was a deliberate architectural choice: keeping the Slate layer free of subsystem dependencies makes the widget easier to reason about and means the filtering logic can be called from anywhere.

One design detail worth noting: while the Advanced Deletion tab is open, both the “Delete Unused Assets” and “Delete Empty Folders” menu entries are blocked. This is enforced by checking DockTab.IsValid() as a simple mutex — the member is non-null only while the tab is open. The mutual exclusion prevents two operations from modifying the same folder’s asset list concurrently.

Actor selection locking

The Level Editor extension adds Lock/Unlock entries to the viewport context menu via FLevelEditorModule::GetAllLevelViewportContextMenuExtenders(). The lock mechanism itself is straightforward: lock = add a "Locked" tag to the actor, unlock = remove it. The interesting part is how the lock is enforced at selection time.

The module subscribes to USelection::SelectObjectEvent during StartupModule(). Every time the user clicks something in the viewport, this event fires. The handler casts the selected object to AActor, checks for the "Locked" tag, and immediately calls SetActorSelectionState(Actor, false) if it finds it. The result is that locked actors appear unselectable — the selection is reverted before the user sees it.

Keyboard shortcuts are registered via a TCommands<> subclass (FSuperManagerUICommands): Alt+W to lock, Alt+Shift+W to unlock all. The command list is appended to the Level Editor’s global actions in InitLevelEditorExtension() so the hotkeys work even when the context menu is not open.

Scene Outliner column

FOutlinerSelectionLockColumn implements ISceneOutlinerColumn and is registered as a default column type with FSceneOutlinerModule::RegisterDefaultColumnType<>(). It renders a 24px wide toggle button per actor row. The button uses a FCheckBoxStyle registered in the custom FSlateStyleSet, with distinct brushes for locked, unlocked, hovered, and pressed states — using FStyleColors so the icons respect the editor’s colour theme.

When the user clicks the toggle, the column calls FSuperManagerModule::ProcessLockingForOutliner(). Again, the column doesn’t implement the lock logic itself — it delegates to the module.

Asset and actor batch utilities

UQuickAssetAction (a UAssetActionUtility subclass) adds three UFUNCTION(CallInEditor) operations to the Content Browser right-click for selected assets: duplicate N times with numeric suffixes, add standard naming prefixes per class (SM_, T_, MI_, etc.), and remove unused assets from the selection. The prefix map handles the UMaterialInstanceConstant edge case — material instances may carry “M_” and “Inst” from their source material, which need to be stripped before “MI” is applied.

UQuickActorActionsWidget (a UEditorUtilityWidget subclass) exposes three Blueprint-callable functions: select all actors with a similar name (strips Unreal’s auto-appended _001-style suffix before matching), duplicate actors N times along a configurable world axis, and randomize transform with independent enable flags per axis for yaw, pitch, and roll, plus optional scale and XY location offset.


Bugs I found in the course code

Going through the implementation carefully turned up a few issues worth documenting.

Inverted logic in RandomizeActorTransform() — the guard condition that was supposed to block the operation when no randomization was enabled was actually inverted. It evaluated to true (and returned early) only when all flags were false simultaneously, which is exactly the condition where the operation should proceed. The variable was named bCanRandomize but its meaning was the opposite. Fixed by restructuring the condition to bNoRandomizationEnabled.

Pointer-to-bool coercionQuickAssetAction.cpp used if (Prefix == false) to check whether a pointer was null. This compiles and works due to implicit bool conversion, but it reads as a comparison between a pointer and a boolean, which is confusing. Fixed to if (!Prefix).

Double #define LOCTEXT_NAMESPACESuperManagerUICommands.cpp had #define LOCTEXT_NAMESPACE at both the top and bottom of the file, where the bottom one should have been #undef. Fixed.

Typo in method nameOutlinerSelectionLockColumn had a callback named OnRowWdigetCheckStateChanged (transposed ‘i’ and ‘t’ in “Widget”). Because the header, implementation, and binding all used the same typo, it compiled without issue — but it’s a problem for maintainability and code search. Fixed to OnRowWidgetCheckStateChanged throughout.

LogTemp everywhere — none of the original code used a custom log category. Every UE_LOG call went to LogTemp, which makes filtering the output log in any real project painful. Added DECLARE_LOG_CATEGORY_EXTERN(LogSuperManager, Log, All) in the utils header and replaced all LogTemp usages.


Key architectural decisions

The module as coordinator — having FSuperManagerModule be the single point of contact for asset deletion and actor locking means neither the Slate widget nor the outliner column need to include ObjectTools.h or manage their own UEditorActorSubsystem references. The widget calls DeleteAsset() on the module; the module calls ObjectTools::DeleteAssets(). Each layer only knows what it needs to know.

Tag-based lock state — using AActor::Tags to persist the lock state rather than a TSet in the module means the lock survives editor restarts (as long as the level is saved) without any custom serialization. The downside is that the “Locked” tag is visible in the actor’s Details panel, but that’s a reasonable trade-off for the simplicity it buys.

FSlateStyleSet for icon management — registering all brushes once in FSuperManagerStyle::CreateSlateStyleSet() under named FName keys means no call site hardcodes an icon path. Changing a texture is a one-line edit in one place.


Engine version: Unreal Engine 5.0

Leave a comment

Create a website or blog at WordPress.com

Up ↑