Why Build Custom Editor Tools?
Every UE5 project has repetitive tasks that eat development time: renaming assets to match conventions, batch-updating material properties, validating level setups, generating reports. These tasks are too specific for marketplace plugins but too tedious for manual execution.
Custom editor tools solve this. UE5 provides two approaches:
- Python scripting: Fast to write, great for automation and batch operations
- Editor Utility Widgets (EUW): Blueprint-based UI tools that live in the editor
Both run only in the editor (zero runtime overhead), and together they cover most custom tooling needs.
Python Scripting in UE5
Enabling Python
- Enable the Python Editor Script Plugin in Edit → Plugins
- Restart the editor
- Open the Python console: Window → Developer Tools → Output Log → switch to Python
Running Scripts
Three ways to execute Python:
Output Log Console:
import unreal
unreal.log("Hello from Python!")
File Execution:
# Save as Content/Python/my_script.py
# Execute via console: py "Content/Python/my_script.py"
Startup Scripts:
Place scripts in Content/Python/init_unreal.py to run on editor startup.
Common Python Operations
Asset Operations
import unreal
# Get the asset registry
asset_registry = unreal.AssetRegistryHelpers.get_asset_registry()
# Find all static meshes in a folder
assets = asset_registry.get_assets_by_path("/Game/Environment/Rocks", recursive=True)
for asset in assets:
if asset.asset_class_path.asset_name == "StaticMesh":
unreal.log(f"Found mesh: {asset.asset_name}")
Batch Rename Assets
import unreal
editor_util = unreal.EditorUtilityLibrary()
selected_assets = editor_util.get_selected_assets()
for asset in selected_assets:
old_name = asset.get_name()
# Add SM_ prefix to static meshes
if isinstance(asset, unreal.StaticMesh) and not old_name.startswith("SM_"):
new_name = f"SM_{old_name}"
unreal.EditorAssetLibrary.rename_asset(
asset.get_path_name(),
f"{asset.get_path_name().rsplit('/', 1)[0]}/{new_name}"
)
unreal.log(f"Renamed: {old_name} → {new_name}")
Material Property Batch Update
import unreal
# Find all material instances in a folder
assets = unreal.EditorAssetLibrary.list_assets("/Game/Materials/Terrain", recursive=True)
for asset_path in assets:
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
if isinstance(asset, unreal.MaterialInstanceConstant):
# Set roughness scalar parameter
unreal.MaterialEditingLibrary.set_material_instance_scalar_parameter_value(
asset, "Roughness", 0.7
)
unreal.log(f"Updated roughness on: {asset.get_name()}")
# Save all modified assets
unreal.EditorAssetLibrary.save_directory("/Game/Materials/Terrain")
Level Validation
import unreal
def validate_level():
"""Check for common level issues."""
issues = []
editor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor_subsystem.get_all_level_actors()
for actor in actors:
# Check for actors at world origin (likely misplaced)
location = actor.get_actor_location()
if location.x == 0 and location.y == 0 and location.z == 0:
issues.append(f"Actor at origin: {actor.get_name()} ({actor.get_class().get_name()})")
# Check for missing mesh references
if isinstance(actor, unreal.StaticMeshActor):
comp = actor.static_mesh_component
if comp and not comp.static_mesh:
issues.append(f"Missing mesh: {actor.get_name()}")
# Check for negative scale (flipped normals)
scale = actor.get_actor_scale3d()
if scale.x < 0 or scale.y < 0 or scale.z < 0:
issues.append(f"Negative scale: {actor.get_name()} ({scale})")
if issues:
for issue in issues:
unreal.log_warning(issue)
unreal.log_warning(f"Found {len(issues)} issues!")
else:
unreal.log("Level validation passed!")
validate_level()
Texture Audit
import unreal
def audit_textures(path="/Game"):
"""Find textures that are too large or have wrong settings."""
assets = unreal.EditorAssetLibrary.list_assets(path, recursive=True)
oversized = []
non_power_of_two = []
for asset_path in assets:
asset_data = asset_registry.get_asset_by_object_path(asset_path)
if asset_data.asset_class_path.asset_name != "Texture2D":
continue
texture = unreal.EditorAssetLibrary.load_asset(asset_path)
if not texture:
continue
width = texture.blueprint_get_size_x()
height = texture.blueprint_get_size_y()
# Flag textures larger than 4K
if width > 4096 or height > 4096:
oversized.append(f"{asset_path}: {width}x{height}")
# Flag non-power-of-two textures
if (width & (width - 1)) != 0 or (height & (height - 1)) != 0:
non_power_of_two.append(f"{asset_path}: {width}x{height}")
unreal.log(f"Oversized textures (>4K): {len(oversized)}")
for t in oversized:
unreal.log_warning(t)
unreal.log(f"Non-power-of-two textures: {len(non_power_of_two)}")
for t in non_power_of_two:
unreal.log_warning(t)
audit_textures()
Editor Utility Widgets
For tools that need a visual interface, Editor Utility Widgets provide Blueprint-based UI that runs in the editor.
Creating an Editor Utility Widget
- Right-click Content Browser → Editor Utilities → Editor Utility Widget
- Name it (e.g.,
EUW_AssetManager) - Double-click to open the Widget Blueprint editor
- Design your UI using UMG widgets
- Run it via right-click → Run Editor Utility Widget
Example: Batch Asset Processor
Create an EUW with:
- A text field for the source folder path
- Checkboxes for operations (rename, move, update properties)
- A "Process" button
- A scrollbox for output log
The Button click event calls your Python scripts or Blueprint logic:
// In the EUW Blueprint Graph:
On Button Clicked →
Get Selected Assets →
For Each Loop →
Process Asset (your custom logic) →
Add to Output Log scrollbox
Example: Level Setup Tool
A tool that sets up standard level elements:
- Lighting preset buttons: Interior, Exterior Day, Exterior Night, Underground
- Post-processing presets: Cinematic, Stylized, Realistic, Horror
- Standard actor spawning: Player start, nav mesh bounds, kill volume, blocking volumes
- Validation button: Run the validation script and display results
Registering as a Menu Item
Make your EUW accessible from the editor toolbar:
import unreal
# Register a menu entry that opens the widget
menus = unreal.ToolMenus.get()
menu = menus.find_menu("LevelEditor.MainMenu.Tools")
section = menu.add_section("CustomTools", "Custom Tools")
entry = unreal.ToolMenuEntry(
name="OpenAssetManager",
type=unreal.MultiBlockType.MENU_ENTRY
)
entry.set_label("Asset Manager")
entry.set_tool_tip("Open the custom asset management tool")
# Set command to open EUW
menu.add_menu_entry("CustomTools", entry)
menus.refresh_all_widgets()
Advanced: Combining Python and C++
For tools that need both UI (EUW) and performance (C++ batch operations):
- Write the heavy processing in a C++ Editor Module
- Expose functions with
UFUNCTION(BlueprintCallable, Category = "EditorTools") - Call those functions from your EUW Blueprint
- Use Python for quick scripting and prototyping before moving to C++
Editor Module Setup
// MyEditorTools.Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "UnrealEd",
"EditorScriptingUtilities", "Blutility"
});
// MyEditorToolsBPLibrary.h
UCLASS()
class UMyEditorToolsBPLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
UFUNCTION(BlueprintCallable, Category = "Editor Tools")
static int32 BatchUpdateMaterials(const FString& FolderPath, float NewRoughness);
UFUNCTION(BlueprintCallable, Category = "Editor Tools")
static TArray<FString> ValidateLevel();
};
Practical Tool Ideas
Asset Naming Convention Enforcer
Scan the project for assets that don't match naming conventions:
- SM_ prefix for Static Meshes
- MI_ prefix for Material Instances
- T_ prefix for Textures
- BP_ prefix for Blueprints
- Offer automatic rename
Build Report Generator
Generate a report showing:
- Total asset count by type
- Texture memory usage
- Mesh polygon counts
- Blueprint complexity scores
- Unused assets (no references)
- Missing references
Data Table Editor
A custom editor for gameplay data that's friendlier than UE5's default data table editor:
- Spreadsheet-like grid editing
- Validation rules per column
- Import/Export to CSV
- Diff view for changes
Level Audit Dashboard
Real-time metrics for the open level:
- Actor count by class
- Light count and overlap visualization
- Draw call estimation
- Navigation mesh coverage
- Collision complexity warnings
Building custom editor tools is an investment that pays dividends throughout your project. Start with the task that wastes the most time, automate it, and build from there. Your future self will thank you.
For editor customization without writing code, check out DetailForge — it lets you reorganize UE5's Details Panel with metadata attributes instead of Slate C++.