Build a Level Audit Editor Plugin in UE5
Build a Slate-based editor plugin that scans levels for actor issues in UE5.
Introduction
30 Second Summary
Every panel, button, and dropdown in the Unreal Editor was built with Slate, a C++-only UI framework. Learning to extend the editor with custom tools is one of the skills that defines senior tools and gameplay programmers at professional studios.
In this project, you will build a "Level Audit Tool" editor plugin that scans your current level for common actor issues and displays them in a clickable, dockable Slate tab inside Unreal Engine 5.7.
What You'll Build
A custom editor-only plugin that adds a dockable tab to the Unreal Editor's Window menu, scans the current level for actors with potential issues, and lets you click any result to select the offending actor in the viewport.
By the end of this project, you'll have:
- A complete editor-only plugin with a .uplugin descriptor, Build.cs, and module source implementing the IModuleInterface lifecycle.
- A custom Slate widget built with SCompoundWidget and an SListView that displays audit results with data binding.
- A toolbar integration via UToolMenus that registers your tool under the Window menu and opens a dockable tab via FGlobalTabmanager.
- Secret Mission: Add a "Fix All" button that batch-renames actors with auto-generated names to clean, descriptive labels with full undo support.
Are there any prerequisites?
You need basic C++ familiarity (variables, functions, classes) and both Unreal Engine 5.7 and Visual Studio already installed on your machine.
No prior Slate or plugin development experience is required. This project teaches those from scratch.
Set Up the Project and Plugin Folder Structure
Every editor extension in Unreal Engine lives inside a plugin. A plugin is a self-contained module with its own folder structure, build configuration, and lifecycle.
In this step, you will create a fresh UE5 C++ project, build the plugin folder hierarchy by hand, and verify that Unreal's build system recognizes your new plugin in Visual Studio.
In this step, get ready to:
- Create a C++ Unreal Engine project from the Third Person template.
- Build the plugin folder hierarchy for LevelAuditTool.
- Regenerate project files and verify the plugin appears in Visual Studio.
Create a new UE5 project
You need a C++ project as the host for your plugin. The Third Person template gives you a level with actors already placed, which is perfect for testing an audit tool later.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Unreal Engine and press Enter to open it.
- When the Project Browser opens, select the Games category on the left.
- Select the Third Person template.
- Under Project Defaults, change the implementation dropdown from Blueprint to C++.
- Set the project location to your Desktop.
- Name the project LevelAuditProject.
- Click Create.
What happens when you click Create?
Unreal Engine generates a C++ solution and opens both the Unreal Editor and Visual Studio automatically. The editor will also begin compiling shaders for the Third Person level.
Wait for the shader compilation progress bar (bottom right of the editor) to finish before continuing. This can take a few minutes depending on your hardware.
Close the auto-opened Visual Studio
Visual Studio opens automatically with the project solution. You can close it for now. You will reopen it after regenerating project files with the plugin included.
Create the plugin folder structure
Plugins in Unreal Engine live inside a Plugins folder at the root of your project directory. You will create the full folder hierarchy and two empty files that define the plugin.
- Open File Explorer and navigate to your LevelAuditProject folder on your Desktop.
- Open a terminal inside this folder by clicking in the address bar, typing powershell, and pressing Enter.
Why open PowerShell here?
Opening PowerShell from the address bar starts it with the working directory already set to your project folder. This means you can run folder-creation commands without navigating first.
- Create the plugin folder hierarchy by running these commands:
mkdir Plugins\LevelAuditTool\Source\LevelAuditTool\Public
mkdir Plugins\LevelAuditTool\Source\LevelAuditTool\Private
What does this create?
These two commands create the standard UE5 plugin folder layout. Public/ holds header files that other modules can include. Private/ holds implementation files that stay internal to your plugin.
- Create the empty plugin descriptor file by running:
New-Item Plugins\LevelAuditTool\LevelAuditTool.uplugin
- Create the empty build configuration file by running:
New-Item Plugins\LevelAuditTool\Source\LevelAuditTool\LevelAuditTool.Build.cs
What are these two files for?
The .uplugin file is a JSON descriptor that tells the engine your plugin exists, what modules it contains, and when to load them.
The .Build.cs file is a C# script that tells Unreal Build Tool which engine modules your plugin depends on. You will populate both files in the next step.
- Verify the folder structure by running:
Get-ChildItem Plugins\LevelAuditTool -Recurse
You should see the full hierarchy: Public and Private folders, plus LevelAuditTool.uplugin and LevelAuditTool.Build.cs listed as files.
Not seeing the expected output?
Make sure your PowerShell session is inside the LevelAuditProject folder, not a subfolder. Run pwd to check your current directory.
If you see an error about the path not existing, double-check the spelling matches exactly: LevelAuditTool (no spaces, capital L, A, and T).
Help me fix my plugin folder structure in PowerShell.
Regenerate project files and open in Visual Studio
Unreal Engine's build system only knows about your plugin after you regenerate the project files. This updates the .sln file so Visual Studio includes your new plugin in the Solution Explorer.
- Close the Unreal Editor (the LevelAuditProject editor window).
- In File Explorer, navigate to your LevelAuditProject folder on the Desktop.
- Find the LevelAuditProject.uproject file.
- Right-click LevelAuditProject.uproject and select Generate Visual Studio project files.
What does this do?
Unreal Build Tool scans your project directory (including the new Plugins folder) and regenerates the .sln solution file along with all .vcxproj files. This is how the engine discovers new plugins that you add manually.
- Wait for the generation to complete. A progress window will appear and close automatically.
- Double-click LevelAuditProject.sln in the same folder to open the solution in Visual Studio.
- In the Solution Explorer panel (usually on the right), expand Games > LevelAuditProject > Plugins.
- Verify that LevelAuditTool appears in the list.
Don't see LevelAuditTool under Plugins?
Make sure you closed the Unreal Editor before regenerating. The engine locks certain files while running, which can prevent a full regeneration.
Double-check that your LevelAuditTool.uplugin file exists at Plugins\LevelAuditTool\LevelAuditTool.uplugin. The .uplugin file must be present for Unreal Build Tool to recognize the folder as a plugin.
Help me get my plugin to show up in Visual Studio.
Your plugin skeleton is in place and the build system knows about it. Next up, you will populate the descriptor and build files to bring the module to life.
Configure the Plugin Module
Your plugin folder structure is in place. Now you need to fill those empty files with the configuration that tells Unreal Engine what your plugin is and how to build it.
Every UE5 plugin needs three layers of configuration: a .uplugin descriptor that declares metadata, a Build.cs file that lists module dependencies, and a C++ module class that hooks into the engine lifecycle. In this step, you will write all three.
In this step, get ready to:
- Write the plugin descriptor and build configuration.
- Declare module dependencies for Slate, editor APIs, and ToolMenus.
- Create the module class and verify a successful build.
Write the .uplugin descriptor
The .uplugin file is a JSON descriptor that tells the engine everything it needs to know about your plugin: its name, what modules it contains, and when those modules should load.
- In Visual Studio, expand the LevelAuditTool plugin in the **Solution Explorer** and open LevelAuditTool.uplugin.
- Replace the empty file contents with the following JSON:
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "Level Audit Tool",
"Description": "Scans the current level for common actor issues and displays results in a dockable Slate tab.",
"Category": "Editor",
"CreatedBy": "Your Name",
"CanContainContent": false,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "LevelAuditTool",
"Type": "Editor",
"LoadingPhase": "PostEngineInit"
}
]
}
What does this descriptor do?
FileVersion 3 is the current plugin descriptor format. Type set to Editor means this module only loads when the editor is running. It will never be included in a packaged game.
LoadingPhase set to PostEngineInit ensures the module loads after the engine subsystems are ready. This is required for editor extensions that depend on UToolMenus and FGlobalTabmanager.
- Save the file with **Ctrl+S**.
File not showing in Solution Explorer?
If you do not see LevelAuditTool.uplugin listed, you may need to regenerate your project files. Close the editor, right-click the .uproject file, select **Generate Visual Studio project files**, and reopen the .sln.
help me find my plugin files in Visual Studio Solution Explorer
✔️ Awesome, I've got everything!
Great. Make sure you have saved LevelAuditTool.uplugin before moving on.
ⓧ I'd like to double check the full code
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "Level Audit Tool",
"Description": "Scans the current level for common actor issues and displays results in a dockable Slate tab.",
"Category": "Editor",
"CreatedBy": "Your Name",
"CanContainContent": false,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "LevelAuditTool",
"Type": "Editor",
"LoadingPhase": "PostEngineInit"
}
]
}
Write the Build.cs module rules
The Build.cs file tells Unreal Build Tool which engine modules your plugin depends on. Without the correct dependencies listed here, the compiler will not find the headers you need.
- Open LevelAuditTool.Build.cs in Visual Studio (it is under Source/LevelAuditTool/).
- Replace the empty file contents with the following C# code:
using UnrealBuildTool;
public class LevelAuditTool : ModuleRules
{
public LevelAuditTool(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Slate",
"SlateCore",
"UnrealEd",
"ToolMenus",
"LevelEditor"
});
}
}
Why public vs private dependencies?
Public dependencies (Core, CoreUObject, Engine, InputCore) are foundational modules that any UE module needs.
Private dependencies are editor-specific: Slate and SlateCore give you the UI framework. UnrealEd gives access to GEditor and editor globals. ToolMenus provides the menu extensibility system. LevelEditor lets you hook into the main editor frame. Keeping them private means they are not exposed to other modules that depend on yours.
- Save LevelAuditTool.Build.cs.
Seeing red squiggles on UnrealBuildTool?
IntelliSense may not fully resolve Build.cs files because they are compiled by UBT, not by your project's normal build. The squiggles are cosmetic and will not prevent compilation.
help me fix IntelliSense errors in my UE5 Build.cs file
✔️ Awesome, I've got everything!
Great. Make sure you have saved LevelAuditTool.Build.cs before moving on.
ⓧ I'd like to double check the full code
using UnrealBuildTool;
public class LevelAuditTool : ModuleRules
{
public LevelAuditTool(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Slate",
"SlateCore",
"UnrealEd",
"ToolMenus",
"LevelEditor"
});
}
}
Create the module header and source files
Every UE5 module needs a C++ class that implements IModuleInterface. This class defines the lifecycle hooks the engine calls when your plugin loads and unloads.
- Right-click the Public folder under your plugin in **Solution Explorer** and select **Add > New Item**.
- Choose **Header File (.h)** and name it LevelAuditToolModule.h.
- Replace any default content with the following:
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
class FLevelAuditToolModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
void RegisterMenus();
TSharedRef<class SDockTab> OnSpawnTab(const class FSpawnTabArgs& SpawnTabArgs);
static const FName LevelAuditTabName;
};
What does this header declare?
FLevelAuditToolModule inherits from IModuleInterface, which gives you StartupModule() and ShutdownModule() lifecycle hooks.
RegisterMenus() and OnSpawnTab() are private helpers you will implement in later steps. LevelAuditTabName is a static FName that serves as the unique identifier for your dockable tab.
- Save LevelAuditToolModule.h.
Now create the implementation file.
- Right-click the Private folder and select **Add > New Item**.
- Choose **C++ File (.cpp)** and name it LevelAuditToolModule.cpp.
- Replace any default content with the following:
#include "LevelAuditToolModule.h"
#include "Modules/ModuleManager.h"
const FName FLevelAuditToolModule::LevelAuditTabName(TEXT("LevelAuditTab"));
void FLevelAuditToolModule::StartupModule()
{
}
void FLevelAuditToolModule::ShutdownModule()
{
}
IMPLEMENT_MODULE(FLevelAuditToolModule, LevelAuditTool)
What does IMPLEMENT_MODULE do?
The IMPLEMENT_MODULE macro registers FLevelAuditToolModule as the entry point for the LevelAuditTool module. Without it, the engine cannot find or instantiate your module class.
The StartupModule() and ShutdownModule() bodies are intentionally empty for now. You will fill them in the next step when you register the menu entry and tab spawner.
- Save LevelAuditToolModule.cpp.
- Build the project by going to **Build > Build Solution** in the Visual Studio menu bar (or press **Ctrl+B**).
The build should complete with 0 errors. You may see warnings about unused parameters in the empty functions. Those are safe to ignore.
Build failed with unresolved externals?
Check that your Build.cs lists all the required modules. A missing UnrealEd or ToolMenus dependency causes linker errors for editor symbols.
Also verify that LevelAuditToolModule.h is in the Public/ folder and LevelAuditToolModule.cpp is in the Private/ folder. UBT uses this folder structure to determine include paths.
help me fix build errors in my UE5 plugin module
✔️ Awesome, I've got everything!
Great. Double check you have saved all four files before moving on.
ⓧ I'd like to double check the full code
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
class FLevelAuditToolModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
void RegisterMenus();
TSharedRef<class SDockTab> OnSpawnTab(const class FSpawnTabArgs& SpawnTabArgs);
static const FName LevelAuditTabName;
};
#include "LevelAuditToolModule.h"
#include "Modules/ModuleManager.h"
const FName FLevelAuditToolModule::LevelAuditTabName(TEXT("LevelAuditTab"));
void FLevelAuditToolModule::StartupModule()
{
}
void FLevelAuditToolModule::ShutdownModule()
{
}
IMPLEMENT_MODULE(FLevelAuditToolModule, LevelAuditTool)
Your plugin module compiles cleanly. Next up, you will wire StartupModule() to register a menu entry under the Window menu and spawn a dockable tab.
Register the Menu Entry and Tab Spawner
Your module compiles, but it does nothing when the editor loads. The empty StartupModule and ShutdownModule functions need to register a dockable tab and a menu entry so your tool is actually reachable from the editor.
In this step, you will wire up the FGlobalTabmanager tab spawner and the UToolMenus system to make your plugin appear under the Window menu.
In this step, get ready to:
- Register a nomad tab spawner and clean up on shutdown.
- Add a Window menu entry that opens your tab.
- Build and launch the editor to verify the menu entry works.
Register the tab spawner and handle shutdown cleanup
FGlobalTabmanager is the singleton that manages every dockable tab in the Unreal Editor. You tell it how to create your tab by registering a "nomad tab spawner" with a unique ID and a callback function.
- In Visual Studio, open LevelAuditToolModule.cpp from the Private folder.
- Replace the entire contents of the file with the following code:
#include "LevelAuditToolModule.h"
#include "Modules/ModuleManager.h"
#include "ToolMenus.h"
#include "Widgets/Docking/SDockTab.h"
#include "Framework/Docking/TabManager.h"
#include "SLevelAuditWidget.h"
const FName FLevelAuditToolModule::LevelAuditTabName(TEXT("LevelAuditTab"));
void FLevelAuditToolModule::StartupModule()
{
// Register a nomad tab spawner so the editor knows how to create our tab
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
LevelAuditTabName,
FOnSpawnTab::CreateRaw(this, &FLevelAuditToolModule::OnSpawnTab))
.SetDisplayName(FText::FromString(TEXT("Level Audit Tool")))
.SetMenuType(ETabSpawnerMenuType::Hidden);
// Delay menu registration until UToolMenus is ready
UToolMenus::RegisterStartupCallback(
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FLevelAuditToolModule::RegisterMenus));
}
void FLevelAuditToolModule::ShutdownModule()
{
// Clean up in reverse order of registration
UToolMenus::UnRegisterStartupCallback(this);
UToolMenus::UnregisterOwner(this);
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(LevelAuditTabName);
}
IMPLEMENT_MODULE(FLevelAuditToolModule, LevelAuditTool)
What does this code do?
RegisterNomadTabSpawner tells the editor "when someone asks for a tab named LevelAuditTab, call my OnSpawnTab function to create it." A nomad tab can be docked anywhere in the editor layout.
SetMenuType(ETabSpawnerMenuType::Hidden) prevents the tab from auto-appearing in every Window menu. You will add your own explicit menu entry instead.
RegisterStartupCallback delays menu registration until UToolMenus is fully initialized. Calling ExtendMenu too early would silently fail.
ShutdownModule tears everything down in reverse order so the editor never holds stale references to your module.
Why NomadTab instead of DocumentTab?
A NomadTab is a standalone tool tab that can be docked anywhere. A DocumentTab is tied to a specific asset editor (like the Blueprint editor). Your audit tool is a standalone utility, so NomadTab is the correct choice.
Wire up the Window menu entry
The tab spawner is registered, but there is no way for users to open it yet. You need a menu entry under Window that calls TryInvokeTab to create or focus your tab.
- Add the RegisterMenus and OnSpawnTab function implementations below the IMPLEMENT_MODULE line. Place them directly above that macro (the final file will have IMPLEMENT_MODULE at the very bottom):
void FLevelAuditToolModule::RegisterMenus()
{
// Extend the existing Window menu in the level editor
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window");
FToolMenuSection& Section = Menu->FindOrAddSection("LevelAuditSection");
Section.AddMenuEntry(
"OpenLevelAudit",
FText::FromString(TEXT("Level Audit Tool")),
FText::FromString(TEXT("Scan the level for common actor issues")),
FSlateIcon(),
FUIAction(FExecuteAction::CreateLambda([]()
{
FGlobalTabmanager::Get()->TryInvokeTab(FName(TEXT("LevelAuditTab")));
}))
);
}
TSharedRef<SDockTab> FLevelAuditToolModule::OnSpawnTab(const FSpawnTabArgs& SpawnTabArgs)
{
return SNew(SDockTab)
.TabRole(NomadTab)
[
SNew(SLevelAuditWidget)
];
}
What does this code do?
ExtendMenu("LevelEditor.MainMenu.Window") gets a handle to the editor's Window dropdown. This is the same menu path that built-in editor tools use.
AddMenuEntry creates the visible entry with a label, tooltip, and an FUIAction that fires when clicked. The lambda calls TryInvokeTab which either creates the tab (first click) or focuses it (already open).
OnSpawnTab returns an SDockTab containing SLevelAuditWidget. That widget does not exist yet, so you will create a placeholder header next.
- Create a new file called SLevelAuditWidget.h in the Public folder (Plugins/LevelAuditTool/Source/LevelAuditTool/Public/).
- Add this placeholder content so the project compiles:
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
// Placeholder - full implementation in the next step
class SLevelAuditWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SLevelAuditWidget) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
// Empty for now - the next step builds the full UI
}
};
Why a placeholder widget?
OnSpawnTab references SLevelAuditWidget via the SNew macro. The compiler needs to see the class declaration and its SLATE_BEGIN_ARGS block to compile. You will replace this stub with the full SListView-based UI in the next step.
- Save both files.
Seeing a compile error about SLevelAuditWidget?
Make sure the file is named exactly SLevelAuditWidget.h and is located in the Public folder. The #include "SLevelAuditWidget.h" in LevelAuditToolModule.cpp looks in Public/ because the Build.cs adds it to the include path automatically.
help me fix my SLevelAuditWidget include error
Build and launch the editor
- Build the project in Visual Studio by pressing Ctrl+B (or Build > Build Solution).
- Wait for the build to succeed with zero errors in the Output window.
- Launch the Unreal Editor by pressing F5 (Start Debugging) or Ctrl+F5 (Start Without Debugging).
- Once the editor loads, click Window in the top menu bar.
- Scroll down and look for Level Audit Tool in the list.
- Click Level Audit Tool. An empty dockable tab opens.
You should see a new empty tab panel appear. You can drag it to dock anywhere in the editor layout.
Don't see Level Audit Tool in the Window menu?
Check that your RegisterMenus function uses the exact menu path LevelEditor.MainMenu.Window. A typo here silently fails because ExtendMenu does not throw an error for non-existent paths.
Also confirm the .uplugin has LoadingPhase set to PostEngineInit. If the module loads too early, UToolMenus is not ready and the callback never fires.
help me debug why my Level Audit Tool menu entry is not appearing
✔️ Awesome, I've got everything!
Great. Double check you have saved both LevelAuditToolModule.cpp and SLevelAuditWidget.h.
ⓧ I'd like to double check the full code
Here is the complete LevelAuditToolModule.cpp as it should look at the end of this step:
#include "LevelAuditToolModule.h"
#include "Modules/ModuleManager.h"
#include "ToolMenus.h"
#include "Widgets/Docking/SDockTab.h"
#include "Framework/Docking/TabManager.h"
#include "SLevelAuditWidget.h"
const FName FLevelAuditToolModule::LevelAuditTabName(TEXT("LevelAuditTab"));
void FLevelAuditToolModule::StartupModule()
{
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
LevelAuditTabName,
FOnSpawnTab::CreateRaw(this, &FLevelAuditToolModule::OnSpawnTab))
.SetDisplayName(FText::FromString(TEXT("Level Audit Tool")))
.SetMenuType(ETabSpawnerMenuType::Hidden);
UToolMenus::RegisterStartupCallback(
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FLevelAuditToolModule::RegisterMenus));
}
void FLevelAuditToolModule::ShutdownModule()
{
UToolMenus::UnRegisterStartupCallback(this);
UToolMenus::UnregisterOwner(this);
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(LevelAuditTabName);
}
void FLevelAuditToolModule::RegisterMenus()
{
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window");
FToolMenuSection& Section = Menu->FindOrAddSection("LevelAuditSection");
Section.AddMenuEntry(
"OpenLevelAudit",
FText::FromString(TEXT("Level Audit Tool")),
FText::FromString(TEXT("Scan the level for common actor issues")),
FSlateIcon(),
FUIAction(FExecuteAction::CreateLambda([]()
{
FGlobalTabmanager::Get()->TryInvokeTab(FName(TEXT("LevelAuditTab")));
}))
);
}
TSharedRef<SDockTab> FLevelAuditToolModule::OnSpawnTab(const FSpawnTabArgs& SpawnTabArgs)
{
return SNew(SDockTab)
.TabRole(NomadTab)
[
SNew(SLevelAuditWidget)
];
}
IMPLEMENT_MODULE(FLevelAuditToolModule, LevelAuditTool)
Your plugin now has a working entry point in the editor. Next up, you will replace that empty placeholder widget with a real Slate UI containing a button and a list view.
Build the Slate UI Widget
Your plugin now has a registered tab and a menu entry. Clicking Window > Level Audit Tool opens an empty dockable tab.
In this step, you will build the actual content that fills that tab. Slate is UE5's C++-only UI framework. Every panel, toolbar, and property inspector in the editor is built with it. You will create a custom SCompoundWidget that composes standard Slate primitives into an audit interface.
In this step, get ready to:
- Create an SLevelAuditWidget class with a data model for audit issues.
- Compose a Slate UI with a header, button, and list view inside the Construct method.
- Build the project and verify the tab displays your new widget.
Create the widget header file
Every custom Slate widget starts with a class that inherits from SCompoundWidget. This base class provides a ChildSlot you fill with your widget tree. You also need a data struct to represent each audit finding.
- In Visual Studio's Solution Explorer, right-click Public under Plugins > LevelAuditTool > Source > LevelAuditTool and select Add > New Item.
- Name the file SLevelAuditWidget.h and paste the following code:
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
#include "Widgets/Views/SListView.h"
// Represents a single issue found during the level audit
struct FAuditIssue
{
TWeakObjectPtr<AActor> Actor;
FString ActorName;
FString Description;
FString Category;
};
class SLevelAuditWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SLevelAuditWidget) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
private:
FReply OnRunAuditClicked();
TSharedRef<ITableRow> OnGenerateRow(TSharedPtr<FAuditIssue> Item, const TSharedRef<STableViewBase>& OwnerTable);
void OnRowClicked(TSharedPtr<FAuditIssue> Item);
TArray<TSharedPtr<FAuditIssue>> AuditResults;
TSharedPtr<SListView<TSharedPtr<FAuditIssue>>> AuditListView;
};
What does this header declare?
FAuditIssue is the data model for one finding. It holds a weak pointer to the actor (so it won't prevent garbage collection), plus the actor's name, issue description, and a category string for grouping.
The widget class uses SLATE_BEGIN_ARGS / SLATE_END_ARGS macros to define the argument struct that SNew uses when constructing the widget. AuditResults holds the list data, and AuditListView stores a reference to the list widget so you can refresh it later.
- Save SLevelAuditWidget.h.
You will not see a change yet. The implementation file comes next.
Implement the Construct method and row generation
The Construct method is where you compose Slate widgets into a visual hierarchy. Each slot in an SVerticalBox holds one child widget stacked top-to-bottom.
- Right-click Private in Solution Explorer (under Plugins > LevelAuditTool > Source > LevelAuditTool) and select Add > New Item.
- Name the file SLevelAuditWidget.cpp and paste the following code for the includes and Construct method:
#include "SLevelAuditWidget.h"
#include "Widgets/Layout/SVerticalBox.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Views/SListView.h"
#include "Editor.h"
#include "EngineUtils.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
void SLevelAuditWidget::Construct(const FArguments& InArgs)
{
ChildSlot
[
SNew(SVerticalBox)
// Header text at the top of the panel
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(8.0f)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Level Audit Tool")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 16))
]
// Button that triggers the audit scan
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(8.0f, 4.0f)
[
SNew(SButton)
.Text(FText::FromString(TEXT("Run Audit")))
.OnClicked(FOnClicked::CreateSP(this, &SLevelAuditWidget::OnRunAuditClicked))
]
// List view that displays audit results
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(8.0f)
[
SAssignNew(AuditListView, SListView<TSharedPtr<FAuditIssue>>)
.ListItemsSource(&AuditResults)
.OnGenerateRow(this, &SLevelAuditWidget::OnGenerateRow)
.OnMouseButtonClick(this, &SLevelAuditWidget::OnRowClicked)
]
];
}
// Stub - audit logic will be implemented in the next step
FReply SLevelAuditWidget::OnRunAuditClicked()
{
return FReply::Handled();
}
What does this Construct method do?
SNew(SVerticalBox) creates a vertical layout container. Each + SVerticalBox::Slot() adds a row. AutoHeight() makes a slot size to its content, while FillHeight(1.0f) tells the list view to fill all remaining space.
SAssignNew both creates the SListView and stores a pointer in AuditListView. You need this pointer later to call RequestListRefresh() after the audit populates results.
OnClicked binds the button to OnRunAuditClicked using a shared-pointer delegate (CreateSP). The function is stubbed for now and returns FReply::Handled() so the button is clickable without crashing.
- Now add the row generation and click handler below OnRunAuditClicked in the same file:
TSharedRef<ITableRow> SLevelAuditWidget::OnGenerateRow(TSharedPtr<FAuditIssue> Item, const TSharedRef<STableViewBase>& OwnerTable)
{
return SNew(STableRow<TSharedPtr<FAuditIssue>>, OwnerTable)
[
SNew(SVerticalBox)
// Actor name in bold
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(4.0f, 2.0f)
[
SNew(STextBlock)
.Text(FText::FromString(Item->ActorName))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
]
// Category and description in orange
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(4.0f, 0.0f, 4.0f, 4.0f)
[
SNew(STextBlock)
.Text(FText::FromString(FString::Printf(TEXT("[%s] %s"), *Item->Category, *Item->Description)))
.ColorAndOpacity(FSlateColor(FLinearColor(1.0f, 0.7f, 0.0f)))
]
];
}
// Stub - selection logic will be implemented in the next step
void SLevelAuditWidget::OnRowClicked(TSharedPtr<FAuditIssue> Item)
{
}
How does row generation work?
SListView does not create row widgets up front. Instead, it calls OnGenerateRow for each visible item as the user scrolls. This is the same virtualization pattern that makes large lists performant in the editor.
Each row is an STableRow wrapping a vertical box with two text blocks: the actor name in bold and the issue category plus description in orange (FLinearColor(1.0f, 0.7f, 0.0f)). The orange draws the eye to problem areas.
- Save SLevelAuditWidget.cpp.
Seeing red squiggles under SListView or FAuditIssue?
Make sure SLevelAuditWidget.h includes Widgets/Views/SListView.h. IntelliSense sometimes lags behind on new files. Try building the project. If it compiles, the squiggles will resolve.
Help me debug my SListView compile errors.
Build and verify the tab UI
Your module code from the previous step already references SLevelAuditWidget inside the tab spawner. Now that the source files exist, the project can compile.
- Build the project by pressing Ctrl+B in Visual Studio (or select Build > Build Solution).
- Wait for the Output window to show a successful build with zero errors.
- Switch to the Unreal Editor.
- If it prompts you to reload modules, click Reload.
- Click Window in the top menu bar, then click Level Audit Tool.
You should see a dockable tab containing the bold "Level Audit Tool" header text, a "Run Audit" button, and an empty list area below.
Build failed with unresolved externals?
Check that SLevelAuditWidget.cpp is in the Private/ folder and SLevelAuditWidget.h is in the Public/ folder. If Visual Studio doesn't pick them up, right-click your .uproject file and select Generate Visual Studio project files, then reopen the solution.
Help me debug my plugin linker errors.
✔️ Awesome, I've got everything!
Great. Double check you have saved both SLevelAuditWidget.h and SLevelAuditWidget.cpp.
ⓧ I'd like to double check the full code
Here is the complete SLevelAuditWidget.h:
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
#include "Widgets/Views/SListView.h"
struct FAuditIssue
{
TWeakObjectPtr<AActor> Actor;
FString ActorName;
FString Description;
FString Category;
};
class SLevelAuditWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SLevelAuditWidget) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
private:
FReply OnRunAuditClicked();
TSharedRef<ITableRow> OnGenerateRow(TSharedPtr<FAuditIssue> Item, const TSharedRef<STableViewBase>& OwnerTable);
void OnRowClicked(TSharedPtr<FAuditIssue> Item);
TArray<TSharedPtr<FAuditIssue>> AuditResults;
TSharedPtr<SListView<TSharedPtr<FAuditIssue>>> AuditListView;
};
And the complete SLevelAuditWidget.cpp:
#include "SLevelAuditWidget.h"
#include "Widgets/Layout/SVerticalBox.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Views/SListView.h"
#include "Editor.h"
#include "EngineUtils.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
void SLevelAuditWidget::Construct(const FArguments& InArgs)
{
ChildSlot
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(8.0f)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Level Audit Tool")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 16))
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(8.0f, 4.0f)
[
SNew(SButton)
.Text(FText::FromString(TEXT("Run Audit")))
.OnClicked(FOnClicked::CreateSP(this, &SLevelAuditWidget::OnRunAuditClicked))
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(8.0f)
[
SAssignNew(AuditListView, SListView<TSharedPtr<FAuditIssue>>)
.ListItemsSource(&AuditResults)
.OnGenerateRow(this, &SLevelAuditWidget::OnGenerateRow)
.OnMouseButtonClick(this, &SLevelAuditWidget::OnRowClicked)
]
];
}
FReply SLevelAuditWidget::OnRunAuditClicked()
{
return FReply::Handled();
}
TSharedRef<ITableRow> SLevelAuditWidget::OnGenerateRow(TSharedPtr<FAuditIssue> Item, const TSharedRef<STableViewBase>& OwnerTable)
{
return SNew(STableRow<TSharedPtr<FAuditIssue>>, OwnerTable)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(4.0f, 2.0f)
[
SNew(STextBlock)
.Text(FText::FromString(Item->ActorName))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(4.0f, 0.0f, 4.0f, 4.0f)
[
SNew(STextBlock)
.Text(FText::FromString(FString::Printf(TEXT("[%s] %s"), *Item->Category, *Item->Description)))
.ColorAndOpacity(FSlateColor(FLinearColor(1.0f, 0.7f, 0.0f)))
]
];
}
void SLevelAuditWidget::OnRowClicked(TSharedPtr<FAuditIssue> Item)
{
}
Your tab now has a real interface. Next up, you will wire the "Run Audit" button to scan every actor in the level and populate that list with real findings.
Implement the Level Audit Logic
Your Level Audit Tool has a working UI shell, but clicking the Run Audit button does nothing. The stub function just returns immediately without scanning anything.
In this step, you will implement the core audit engine. Your tool will query the editor world for every actor, run three diagnostic checks on each one, and display the results in your SListView. You will also wire up row clicks so that selecting an issue in the list highlights the offending actor directly in the viewport.
In this step, get ready to:
- Scan the editor world for all actors and detect three common level issues.
- Populate the results list so the SListView displays findings with category labels.
- Wire up row clicks to select the offending actor in the viewport.
Implement the actor scanning logic
The OnRunAuditClicked function needs to access the current editor world, walk through every actor in it, and flag three common problems: actors stranded at world origin, actors with non-uniform scale, and actors with auto-generated names.
- In Visual Studio, open SLevelAuditWidget.cpp from the Private folder.
- Find the stub OnRunAuditClicked function that currently just returns FReply::Handled().
- Replace the entire function with this implementation:
FReply SLevelAuditWidget::OnRunAuditClicked()
{
AuditResults.Empty();
UWorld* World = GEditor->GetEditorWorldContext().World();
if (!World)
{
return FReply::Handled();
}
for (TActorIterator<AActor> It(World); It; ++It)
{
AActor* Actor = *It;
if (!Actor || Actor->IsPendingKillPending())
{
continue;
}
FString ActorLabel = Actor->GetActorLabel();
FVector Location = Actor->GetActorLocation();
FVector Scale = Actor->GetActorScale3D();
// Check 1: Actor at world origin
if (Location.IsNearlyZero(1.0f))
{
TSharedPtr<FAuditIssue> Issue = MakeShared<FAuditIssue>();
Issue->Actor = Actor;
Issue->ActorName = ActorLabel;
Issue->Description = TEXT("Actor is at world origin (0,0,0)");
Issue->Category = TEXT("Position");
AuditResults.Add(Issue);
}
// Check 2: Non-uniform scale
if (!FMath::IsNearlyEqual(Scale.X, Scale.Y, 0.001f) ||
!FMath::IsNearlyEqual(Scale.Y, Scale.Z, 0.001f))
{
TSharedPtr<FAuditIssue> Issue = MakeShared<FAuditIssue>();
Issue->Actor = Actor;
Issue->ActorName = ActorLabel;
Issue->Description = FString::Printf(TEXT("Non-uniform scale: (%.2f, %.2f, %.2f)"), Scale.X, Scale.Y, Scale.Z);
Issue->Category = TEXT("Scale");
AuditResults.Add(Issue);
}
// Check 3: Auto-generated name (ends with underscore + digits)
FRegexPattern Pattern(TEXT("_\\d+$"));
FRegexMatcher Matcher(Pattern, ActorLabel);
if (Matcher.FindNext())
{
TSharedPtr<FAuditIssue> Issue = MakeShared<FAuditIssue>();
Issue->Actor = Actor;
Issue->ActorName = ActorLabel;
Issue->Description = TEXT("Actor has auto-generated name");
Issue->Category = TEXT("Auto-Generated Name");
AuditResults.Add(Issue);
}
}
if (AuditListView.IsValid())
{
AuditListView->RequestListRefresh();
}
return FReply::Handled();
}
What does this code do?
This function is the heart of your audit tool. It performs a full scan of the current editor level every time the learner clicks the button.
- AuditResults.Empty() clears any previous results so each scan starts fresh.
- GEditor->GetEditorWorldContext().World() retrieves the currently loaded editor level. This is an editor-only API available through the UnrealEd module dependency.
- TActorIterator<AActor> walks every actor in the world. For each actor, it runs three diagnostic checks and creates an FAuditIssue entry for every problem it finds.
- The regex pattern _\d+$ catches names like Cube_0 or BP_ThirdPersonCharacter_2 that the engine auto-generates when you duplicate or place actors.
- RequestListRefresh() tells the SListView that its data source changed and it should re-render all rows.
- Save SLevelAuditWidget.cpp.
- You will not see changes yet because the project needs to be rebuilt. The next substep adds the remaining logic before you build.
Seeing red squiggles under FRegexPattern?
Make sure #include "EngineUtils.h" is at the top of your file. The regex classes are part of the Core module and included transitively through CoreMinimal.h, but IntelliSense sometimes needs the file saved before it resolves them.
If IntelliSense still complains, the actual build will succeed. IntelliSense in large UE projects can lag behind the real compiler.
Help me fix IntelliSense errors in my UE5 plugin code.
Handle row clicks to select actors
When a learner spots an issue in the list, they need to find the actor in the viewport. You will implement OnRowClicked to select the clicked actor using the editor's selection API.
- In the same file, find the empty OnRowClicked function.
- Replace the entire function with this implementation:
void SLevelAuditWidget::OnRowClicked(TSharedPtr<FAuditIssue> Item)
{
if (!Item.IsValid() || !Item->Actor.IsValid())
{
return;
}
AActor* Actor = Item->Actor.Get();
GEditor->SelectNone(true, true, false);
GEditor->SelectActor(Actor, true, true, true);
}
What does this code do?
- Item.IsValid() checks that the shared pointer is not null. Item->Actor.IsValid() checks that the weak object pointer still references a live actor (it may have been deleted between the scan and the click).
- GEditor->SelectNone(true, true, false) deselects everything in the viewport first, giving a clean slate.
- GEditor->SelectActor(Actor, true, true, true) selects the specific actor. The editor highlights it in the viewport and shows its details in the Details panel.
- Save SLevelAuditWidget.cpp.
Build and test the audit tool
Both functions are implemented. Time to compile the project, set up some test actors in your level, and confirm the full scan-and-select workflow works end to end.
- Build the project in Visual Studio by pressing Ctrl+B.
- Wait for the build to succeed with zero errors in the Output window.
Build failed with linker or compile errors?
Double-check that your LevelAuditTool.Build.cs includes UnrealEd in PrivateDependencyModuleNames. Without it, GEditor and TActorIterator will not resolve.
If you see an error about FRegexPattern or FRegexMatcher, confirm that Core is listed in PublicDependencyModuleNames. These classes come from the Core module.
Help me fix build errors in my Level Audit Tool plugin.
Now set up some test actors in the level so the audit has something to find.
- Switch back to the Unreal Editor.
- In the viewport, select any actor (for example one of the existing cubes or the floor mesh).
- In the Details panel, set its Location to 0, 0, 0 so it sits at world origin.
- Select a different actor and set its Scale to something non-uniform, like 1.0, 2.0, 1.0.
- Open the Level Audit Tool tab from Window > Level Audit Tool.
- Click the Run Audit button.
You should see the list populate with audit results. Each row shows an actor name in bold, followed by its category and description in amber text.
- Click any row in the results list.
- Confirm the corresponding actor becomes selected in the viewport. You should see its selection outline appear and its properties show in the Details panel.
List stays empty after clicking Run Audit?
The audit checks actors in the currently loaded level. If you are on a blank or mostly empty map, there may be no actors that match the three conditions. Try moving an actor to 0, 0, 0 or scaling one non-uniformly.
Also verify you rebuilt the project after making changes. If the editor is running an older version of your plugin, your new code is not active yet. Close the editor, rebuild, then relaunch.
My Level Audit Tool list is empty even after placing test actors.
✔️ Awesome, I've got everything!
Great. Double check you have saved SLevelAuditWidget.cpp and that your build succeeded with zero errors.
ⓧ I'd like to double check the full code
Here is the complete SLevelAuditWidget.cpp at this point in the project:
#include "SLevelAuditWidget.h"
#include "Widgets/Layout/SVerticalBox.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Views/SListView.h"
#include "Editor.h"
#include "EngineUtils.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
void SLevelAuditWidget::Construct(const FArguments& InArgs)
{
ChildSlot
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(8.0f)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Level Audit Tool")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 16))
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(8.0f, 4.0f)
[
SNew(SButton)
.Text(FText::FromString(TEXT("Run Audit")))
.OnClicked(FOnClicked::CreateSP(this, &SLevelAuditWidget::OnRunAuditClicked))
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(8.0f)
[
SAssignNew(AuditListView, SListView<TSharedPtr<FAuditIssue>>)
.ListItemsSource(&AuditResults)
.OnGenerateRow(this, &SLevelAuditWidget::OnGenerateRow)
.OnMouseButtonClick(this, &SLevelAuditWidget::OnRowClicked)
]
];
}
FReply SLevelAuditWidget::OnRunAuditClicked()
{
AuditResults.Empty();
UWorld* World = GEditor->GetEditorWorldContext().World();
if (!World)
{
return FReply::Handled();
}
for (TActorIterator<AActor> It(World); It; ++It)
{
AActor* Actor = *It;
if (!Actor || Actor->IsPendingKillPending())
{
continue;
}
FString ActorLabel = Actor->GetActorLabel();
FVector Location = Actor->GetActorLocation();
FVector Scale = Actor->GetActorScale3D();
// Check 1: Actor at world origin
if (Location.IsNearlyZero(1.0f))
{
TSharedPtr<FAuditIssue> Issue = MakeShared<FAuditIssue>();
Issue->Actor = Actor;
Issue->ActorName = ActorLabel;
Issue->Description = TEXT("Actor is at world origin (0,0,0)");
Issue->Category = TEXT("Position");
AuditResults.Add(Issue);
}
// Check 2: Non-uniform scale
if (!FMath::IsNearlyEqual(Scale.X, Scale.Y, 0.001f) ||
!FMath::IsNearlyEqual(Scale.Y, Scale.Z, 0.001f))
{
TSharedPtr<FAuditIssue> Issue = MakeShared<FAuditIssue>();
Issue->Actor = Actor;
Issue->ActorName = ActorLabel;
Issue->Description = FString::Printf(TEXT("Non-uniform scale: (%.2f, %.2f, %.2f)"), Scale.X, Scale.Y, Scale.Z);
Issue->Category = TEXT("Scale");
AuditResults.Add(Issue);
}
// Check 3: Auto-generated name (ends with underscore + digits)
FRegexPattern Pattern(TEXT("_\\d+$"));
FRegexMatcher Matcher(Pattern, ActorLabel);
if (Matcher.FindNext())
{
TSharedPtr<FAuditIssue> Issue = MakeShared<FAuditIssue>();
Issue->Actor = Actor;
Issue->ActorName = ActorLabel;
Issue->Description = TEXT("Actor has auto-generated name");
Issue->Category = TEXT("Auto-Generated Name");
AuditResults.Add(Issue);
}
}
if (AuditListView.IsValid())
{
AuditListView->RequestListRefresh();
}
return FReply::Handled();
}
TSharedRef<ITableRow> SLevelAuditWidget::OnGenerateRow(TSharedPtr<FAuditIssue> Item, const TSharedRef<STableViewBase>& OwnerTable)
{
return SNew(STableRow<TSharedPtr<FAuditIssue>>, OwnerTable)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(4.0f, 2.0f)
[
SNew(STextBlock)
.Text(FText::FromString(Item->ActorName))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(4.0f, 0.0f, 4.0f, 4.0f)
[
SNew(STextBlock)
.Text(FText::FromString(FString::Printf(TEXT("[%s] %s"), *Item->Category, *Item->Description)))
.ColorAndOpacity(FSlateColor(FLinearColor(1.0f, 0.7f, 0.0f)))
]
];
}
void SLevelAuditWidget::OnRowClicked(TSharedPtr<FAuditIssue> Item)
{
if (!Item.IsValid() || !Item->Actor.IsValid())
{
return;
}
AActor* Actor = Item->Actor.Get();
GEditor->SelectNone(true, true, false);
GEditor->SelectActor(Actor, true, true, true);
}
Your audit tool is fully functional. It scans the world, reports issues, and lets you jump straight to the problem actor with a single click. In the Secret Mission, you will add a batch-fix button that auto-renames actors with generated names.
Secret mission
Add a "Fix All" Button
Your audit tool finds problems, but what if it could fix them too? In this mission, you'll add a batch-rename button that cleans up auto-generated actor names in one click, wrapped in a transaction so the entire operation is undoable with Ctrl+Z.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with no cloud services or paid APIs. The editor-only plugin has zero runtime cost and will not be included in packaged game builds.
Resources you used:
- LevelAuditProject folder (the Unreal Engine project you created).
- Plugins/LevelAuditTool directory inside your project (the editor plugin source code).
Keep everything running
No action needed. Choose this if you plan to reuse or extend the plugin.
- The plugin is editor-only, so it adds no cost to packaged builds.
- You can copy the Plugins/LevelAuditTool folder into any other UE5 project to reuse it.
Pause - I'll come back to this later
Close Visual Studio and the Unreal Editor to free up memory. Your project files stay on disk and you can reopen them at any time.
- Close Visual Studio.
- Close the Unreal Editor.
- Your LevelAuditProject folder remains on disk. Reopen the .uproject file whenever you want to continue.
Delete - I don't want to use this again
Remove the project files entirely. This cannot be undone.
- Close Visual Studio and the Unreal Editor.
- Delete just the plugin by removing the Plugins/LevelAuditTool directory inside your project folder.
- Delete the entire project by removing the LevelAuditProject folder.
On macOS, open Terminal and run:
rm -rf ~/path/to/LevelAuditProject
On Windows, open File Explorer, navigate to your LevelAuditProject folder, right-click it, and select Delete.
Nice Work!
Nice Work!
Nice work! You just built a professional-grade editor plugin for Unreal Engine 5 from scratch, complete with a custom Slate UI, actor scanning logic, and viewport selection integration.
You've learned how to:
- Build a complete editor-only plugin with a .uplugin descriptor, Build.cs dependencies, and IModuleInterface lifecycle hooks.
- Create a Slate UI from scratch using SCompoundWidget, SListView data binding, and register a dockable tab via FGlobalTabmanager with a Window menu entry through UToolMenus.
- Implement actor iteration and editor selection using TActorIterator and GEditor APIs so clicking audit results selects actors directly in the viewport.
- Secret Mission: Added a batch rename button wrapped in FScopedTransaction so the entire operation supports Ctrl+Z undo.
Ready to quiz yourself?