Writing a Custom Asset Editor for Unreal Engine 4 - Part 1


This guide will contain a step-by-step explanation on how to set up a Custom Asset Editor for Unreal Engine 4 using C++. We will recreate the default editor details window and provide you with a solid foundation from where you can start building your own custom editor.

Creating your own custom asset editor can be useful as some of the default functionality that Unreal its default editor offers might be lacking or not suffice for your specific custom asset. Creating your own asset editor with your custom tools can optimize your team its workflow when working with the asset and save you valuable time during production.

When extending the asset editor, you can create additional editor tabs, toolbar entries, menu entries. By utilizing Slate you can control how properties and fields within the editor are drawn and represented to the user, this can prove extremely powerful when you want to display specific information to the end user.


About you

This guide is for the more experienced Unreal Engine C++ programmers who are interested in extending Unreal its default editor functionality to create a custom editor for their assets.

At the end of this guide, you will have a better understanding of custom asset editors so that you can start increasing the productivity and workflow of your teams when working with custom assets.

You are expected to have some experience with programming C++ in Unreal Engine 4. You are familiar with the editor and have a basic understanding of how to create classes through Unreal its Editor interface and how to create classes through Visual Studio.


What will you learn

In this guide, you will learn how to use AssetTypeActions to modify your custom asset and spawn our custom editor. You will learn more about the AssetEditorToolkit which will be used to create our custom editor and you will refactor your editor module implementation to provide us with a clean way to access our custom editor.

Please note that this guide is part of a more comprehensive tutorial and it is essential that you either followed the previous parts or have already created something similar. Please check the prerequisites to make sure you have everything you need before starting to read this guide.

Prerequisites

Additional Notes

I have written this tutorial as I had to go through the same process of looking for resources on how to set up a custom asset editor for one of my projects. As I could not find a comprehensive guide anywhere and I mostly had to look through engine source, I thought I might be able to share my insights on the process.

Preparing the MyCustomEditor module

Setting up the Build Rules

CustomAssetEditor.Build.cs
PrivateDependencyModuleNames.AddRange(
	new string[]
	{
        "Core",
        "CoreUObject",
        "Json",
        "Slate",
        "SlateCore",
        "Engine",
        "InputCore",
        "UnrealEd", // for FAssetEditorManager
        "KismetWidgets",
        "Kismet",  // for FWorkflowCentricApplication
        "PropertyEditor",
        "RenderCore",
        "ContentBrowser",
        "WorkspaceMenuStructure",
        "EditorStyle",
        "EditorWidgets",
        "Projects",
        "AssetRegistry",
        "Tutorial"
        
        // ... add private dependencies that you statically link with here ...	
	}
);
Before we can begin building our Custom Asset Editor, we need access to some Unreal Editor Modules. We add these dependencies to our PrivateDependencyModuleNames to make sure we are able to use them within our module's codebase.

CustomAssetEditor.Build.cs
PrivateIncludePathModuleNames.AddRange(
	new string[] 
	{
        "Settings",
        "IntroTutorials",
        "AssetTools",
        "LevelEditor"
    }
);

DynamicallyLoadedModuleNames.AddRange(
	new string[] 
	{
		"AssetTools"
	}
);

We will also add some module names to our PrivateIncludePathModuleNames and DynamicallyLoadedModuleNamesas we want to be able to easily access them in our code.

Setting up our Source files

If you have followed the other recommended parts you will have set up your modules source files which are named CustomAssetEditorModule and contain a class that derives from IModuleInterface. In this step, we are going to start from scratch and refactor it to adhere to Epic's standards within their own editor modules.

An important side-note is that we will be using some classes that will be defined later in this guide. So do not worry when you cannot compile your code or visual studio is throwing you some errors. If you follow this guide we will be implementing all of the required classes.

Setting up our Module's header

CustomAssetEditorModule.h (Header file)
#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
#include "ModuleManager.h"
#include "Toolkits/AssetEditorToolkit.h"

class ICustomAssetEditor;
class UMyCustomAsset;

extern const FName CustomAssetEditorAppIdentifier;

/**
 * Custom Asset editor module interface
 */
class ICustomAssetEditorModule : public IModuleInterface, public IHasMenuExtensibility, public IHasToolBarExtensibility
{
public:
	/**
	 * Creates a new custom asset editor.
	 */
	virtual TSharedRef<ICustomAssetEditor> CreateCustomAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr< IToolkitHost >& InitToolkitHost, UMyCustomAsset* CustomAsset) = 0;
};

We add some includes and start by forward declaring two of the classes that we are going to be referencing in our function. We start by creating a new class that implements the following interfaces.

IModuleInterface is the interface which contains the startup and shutdown module methods, every module needs to inherit from this base class.

IHasMenuExtensiblity indicates that our class has a default menu that is extensible.

IHasToolBarExtensibility indicates that our class has a default toolbar that is extensible.

We also create the CreateCustomAssetEditor function which we will mark as pure virtual. This function will be implemented in a derived class and will spawn our custom editor.

Setting up our module's implementation file

CustomAssetEditorModule.cpp (Implementation file)
#include "CustomAssetEditorModule.h"
#include "MyCustomAsset.h"
#include "Modules/ModuleManager.h"
#include "IToolkit.h"
#include "CustomAssetEditor.h"

const FName CustomAssetEditorAppIdentifier = FName(TEXT("CustomAssetEditorApp"));

/**
 * Custom Asset editor module
 */
class FCustomAssetEditorModule : public ICustomAssetEditorModule
{
public:
	/** Constructor */
	FCustomAssetEditorModule() { }

	/** Gets the extensibility managers for outside entities to extend custom asset editor's menus and toolbars */
	virtual TSharedPtr<FExtensibilityManager> GetMenuExtensibilityManager() override { return MenuExtensibilityManager; }
	virtual TSharedPtr<FExtensibilityManager> GetToolBarExtensibilityManager() override { return ToolBarExtensibilityManager; }

private:
	TSharedPtr<FExtensibilityManager> MenuExtensibilityManager;
	TSharedPtr<FExtensibilityManager> ToolBarExtensibilityManager;
};

IMPLEMENT_GAME_MODULE(FCustomAssetEditorModule, CustomAssetEditor);

In our implementation file, we create a new class which inherits from our previously created ICustomAssetEditor. We start by implementing the functions provided by the IHasMenuExtensiblity and IHasToolbarExtensiblity classes and also declare the required properties.

With these properties declared, we can start implementing our module functions.

CustomAssetEditorModule.h (Implementation file)

/**
* Called right after the module DLL has been loaded and the module object has been created
*/
virtual void StartupModule() override
{
    // Create new extensibility managers for our menu and toolbar
    MenuExtensibilityManager = MakeShareable(new FExtensibilityManager);
    ToolBarExtensibilityManager = MakeShareable(new FExtensibilityManager);
}

/**
* Called before the module is unloaded, right before the module object is destroyed.
*/
virtual void ShutdownModule() override
{
    // Reset our existing extensibility managers
    MenuExtensibilityManager.Reset();
    ToolBarExtensibilityManager.Reset();
}

/**
* Creates a new custom asset editor for a MyCustomAsset
*/
virtual TSharedRef<ICustomAssetEditor> CreateCustomAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr< IToolkitHost >& InitToolkitHost, UMyCustomAsset* CustomAsset) override
{
    // Initialize and spawn a new custom asset editor with the provided parameters
    TSharedRef<FCustomAssetEditor> NewCustomAssetEditor(new FCustomAssetEditor());
    NewCustomAssetEditor->InitCustomAssetEditorEditor(Mode, InitToolkitHost, CustomAsset);
    return NewCustomAssetEditor;
}

We start by implementing the StartupModuleand ShutdownModulefunctions. At startup, we want to make sure that we create two new extensibility managers for our menu and toolbar. We can now implement the CreateCustomAssetEditor function, we will initialize a shared reference with our custom asset editor and initialize our asset editor.

Note: We have not yet created our custom asset editor so Visual Studio will be throwing some errors, we will now be creating our Custom Asset Editor.

Creating our Custom Asset Editor

We will now be creating our Custom Asset Editor. This is the editor that is spawned when you double-click a custom asset. Every class that we are adding from this point is located inside our Editor module. We will not be working in runtime game module.

The Custom Asset Interface

ICustomAssetEditor.h (Header file)
#pragma once

#include "CoreMinimal.h"
#include "Toolkits/AssetEditorToolkit.h"

class UMyCustomAsset;

/**
 * Public interface to Custom Asset Editor
 */
class ICustomAssetEditor : public FAssetEditorToolkit
{
public:
	/** Retrieves the current custom asset. */
	virtual UMyCustomAsset* GetCustomAsset() = 0;

	/** Set the current custom asset. */
	virtual void SetCustomAsset(const UMyCustomAsset* InCustomAsset) const = 0;
};

We start by creating the ICustomAssetEditorclass (in a new file) which inherits from FAssetEditorToolkit. Please note that this class cannot be created via the Unreal Editor Interface so you will have to create it through Visual Studio.

We inherit from FAssetEditorToolkit as this is the abstract base class for toolkits that are used for asset editing. This also means that some of the functions provided by this toolkit are marked as pure-virtual which we will have to implement in our Asset Editor. For now, we will just create our interface which will contain some pure-virtual functions to retrieve and set the Custom Asset that we are going to be editing.

The reason I am creating an interface is so that we can easily hold a pointer or reference to the interface instead of the entire class. This also means that we can limit the number of accessible functions. Other than that it is personal preference to first create an interface for the custom editor.

Creating the Custom Asset Editor Header

Setting up the Header file

CustomAssetEditor.h (Header file)
#pragma once

#include "CoreMinimal.h"
#include "Toolkits/IToolkitHost.h"
#include "Toolkits/AssetEditorToolkit.h"
#include "Editor/PropertyEditor/Public/PropertyEditorDelegates.h"
#include "ICustomAssetEditor.h"

class IDetailsView;
class SDockableTab;
class UMyCustomAsset;

/**
 * 
 */
class CUSTOMASSETEDITOR_API FCustomAssetEditor : public ICustomAssetEditor
{
};

The next step is to create our Custom Asset Editor. We create a class called FCustomAssetEditor which inherits from our ICustomAssetEditor interface class. We add the necessary includes and forward declare some classes that we will be adding as properties at a later time.

Adding our Initialization and Tab Spawners

CustomAssetEditor.h (Header file)
class CUSTOMASSETEDITOR_API FCustomAssetEditor : public ICustomAssetEditor
{
public:
    // This function creates tab spawners on editor initialization
    virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager) override;

    // This function unregisters tab spawners on editor initialization
    virtual void UnregisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager) override;

    // This method decides how the custom asset editor will be initialized
    void InitCustomAssetEditorEditor(const EToolkitMode::Type Mode, const TSharedPtr<class IToolkitHost>& InitToolkitHost, UMyCustomAsset* InCustomAsset);

    /** Destructor */
    virtual ~FCustomAssetEditor();
};

We set up tab spawners. These functions will provide us with an implementation to spawn editor tabs, for example, the details panel. We also declare the InitCustomAssetEditor function which will be used to initialize our custom asset editor. Additionally, we also provide our class with a virtual destructor to do some cleanup.

Implementing the Toolkit interface

CustomAssetEditor.h (Header file)
class CUSTOMASSETEDITOR_API FCustomAssetEditor : public ICustomAssetEditor
{
public:

	virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager) override;
	virtual void UnregisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager) override;

	/**
	 * Edits the specified asset object
	 *
	 * @param	Mode					Asset editing mode for this editor (standalone or world-centric)
	 * @param	InitToolkitHost			When Mode is WorldCentric, this is the level editor instance to spawn this editor within
	 * @param	InCustomAsset			The Custom Asset to Edit
	 */
	void InitCustomAssetEditorEditor(const EToolkitMode::Type Mode, const TSharedPtr<class IToolkitHost>& InitToolkitHost, UMyCustomAsset* InCustomAsset);

	/** Destructor */
	virtual ~FCustomAssetEditor();

	/** Begin IToolkit interface */
	virtual FName GetToolkitFName() const override;
	virtual FText GetBaseToolkitName() const override;
	virtual FText GetToolkitName() const override;
	virtual FText GetToolkitToolTipText() const override;
	virtual FString GetWorldCentricTabPrefix() const override;
	virtual FLinearColor GetWorldCentricTabColorScale() const override;
	virtual bool IsPrimaryEditor() const override { return true; }
	/** End IToolkit interface */
};

We can now start implementing the abstract functions provided by the toolkit. These toolkit functions provide the editor with some information about our Custom Asset Editor. For example, the GetToolkitToolTipText function returns the localized tooltip text of this toolkit. For more information about the toolkit, you can take a look at the IToolkit documentation page.

Implementing Interface methods and Properties

CustomAssetEditor.h (Header file)
class CUSTOMASSETEDITOR_API FCustomAssetEditor : public ICustomAssetEditor
{
public:

	virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager) override;
	virtual void UnregisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager) override;

	/**
	 * Edits the specified asset object
	 *
	 * @param	Mode					Asset editing mode for this editor (standalone or world-centric)
	 * @param	InitToolkitHost			When Mode is WorldCentric, this is the level editor instance to spawn this editor within
	 * @param	InCustomAsset			The Custom Asset to Edit
	 */
	void InitCustomAssetEditorEditor(const EToolkitMode::Type Mode, const TSharedPtr<class IToolkitHost>& InitToolkitHost, UMyCustomAsset* InCustomAsset);

	/** Destructor */
	virtual ~FCustomAssetEditor();

	/** Begin IToolkit interface */
	virtual FName GetToolkitFName() const override;
	virtual FText GetBaseToolkitName() const override;
	virtual FText GetToolkitName() const override;
	virtual FText GetToolkitToolTipText() const override;
	virtual FString GetWorldCentricTabPrefix() const override;
	virtual FLinearColor GetWorldCentricTabColorScale() const override;
	virtual bool IsPrimaryEditor() const override { return true; }
	/** End IToolkit interface */

	/** Begin ICustomAssetEditor initerface */
	virtual UMyCustomAsset* GetCustomAsset();
	virtual void SetCustomAsset(const UMyCustomAsset* InCustomAsset) const;
	/** End ICustomAssetEditor initerface */

private:
	/** Create the properties tab and its content */
	TSharedRef<SDockTab> SpawnPropertiesTab(const FSpawnTabArgs& Args);

	/** Dockable tab for properties */
	TSharedPtr< SDockableTab > PropertiesTab;

	/** Details view */
	TSharedPtr<class IDetailsView> DetailsView;

	/**	The tab ids for all the tabs used */
	static const FName PropertiesTabId;

	/** The Custom Asset open within this editor */
	UMyCustomAsset* CustomAsset;
};

Finally, we implement the abstract methods provided by our ICustomAssetEditor class. We also add some properties that we will be using in our Editor. I will briefly go over the properties and what they are used for but their usage will become more clear when looking at the implementation file.

PropertiesTab is the dockable tab that can be docked anywhere within our custom editor.

DetailsView is a shared pointer to our details view panel that we will be spawning in the properties tab.

PropertiesTabId is the identifier for the properties tab.

CustomAsset is the custom asset that we will be editing in this editor.

Setting up our implementation file

Setting up the Tab Spawners

CustomAssetEditor.cpp (Implementation file)
#include "CustomAssetEditor.h"
#include "Modules/ModuleManager.h"
#include "EditorStyleSet.h"
#include "Widgets/Docking/SDockTab.h"
#include "PropertyEditorModule.h"
#include "IDetailsView.h"
#include "Editor.h"
#include "AssetEditorToolkit.h"
#include "CustomAssetEditorModule.h"

#define LOCTEXT_NAMESPACE "CustomAssetEditor"

const FName FCustomAssetEditor::ToolkitFName(TEXT("CustomAssetEditor"));
const FName FCustomAssetEditor::PropertiesTabId(TEXT("CustomAssetEditor_Properties"));

void FCustomAssetEditor::RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
    // Add a new workspace menu category to the tab manager
	WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_CustomAssetEditor", "Custom Asset Editor"));

    // We register the tab manager to the asset editor toolkit so we can use it in this editor
	FAssetEditorToolkit::RegisterTabSpawners(InTabManager);

    // Register the properties tab spawner within our tab manager
    // We provide the function with the identiefier for this tab and a shared pointer to the
    // SpawnPropertiesTab function within this editor class
    // Additionaly, we provide a name to be displayed, a category and the tab icon
	InTabManager->RegisterTabSpawner(PropertiesTabId, FOnSpawnTab::CreateSP(this, &FCustomAssetEditor::SpawnPropertiesTab))
		.SetDisplayName(LOCTEXT("PropertiesTab", "Details"))
		.SetGroup(WorkspaceMenuCategory.ToSharedRef())
		.SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tabs.Details"));
}

void FCustomAssetEditor::UnregisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
    // Unregister the tab manager from the asset editor toolkit
	FAssetEditorToolkit::UnregisterTabSpawners(InTabManager);

    // Unregister our custom tab from the tab manager, making sure it is cleaned up when the editor gets destroyed
	InTabManager->UnregisterTabSpawner(PropertiesTabId);
}

TSharedRef<SDockTab> FCustomAssetEditor::SpawnPropertiesTab(const FSpawnTabArgs& Args)
{
    // Make sure we have the correct tab id
	check(Args.GetTabId() == PropertiesTabId);

    // Return a new slate dockable tab that contains our details view
	return SNew(SDockTab)
		.Icon(FEditorStyle::GetBrush("GenericEditor.Tabs.Properties"))
		.Label(LOCTEXT("GenericDetailsTitle", "Details"))
		.TabColorScale(GetTabColorScale())
		[
                // Provide the details view as this tab its content
		        DetailsView.ToSharedRef()
		];
}

#undef LOCTEXT_NAMESPACE

We start by implementing the RegisterTabSpawners function. This function will add a local workspace menu category and register the TabManager. We will register a tab spawner that is linked to our PropertiesTabId and set it up with a shared pointer to the SpawnPropertiesTab function.

SpawnPropertiesTab will take create a new dockable tab that is initialized with some default properties and it will contain the slate provided by the DetailsViewwidget.

The UnregisterTabSpawners function will take care of cleaning up and unregistering the tab spawners that we have created when shutting down the editor.

Implementing the Interface methods

CustomAssetEditor.cpp (Implementation file)
FCustomAssetEditor::~FCustomAssetEditor()
{
    // On destruction we reset our tab and details view 
	DetailsView.Reset();
	PropertiesTab.Reset();
}

FName FCustomAssetEditor::GetToolkitFName() const
{
	return ToolkitFName;
}

FText FCustomAssetEditor::GetBaseToolkitName() const
{
	return LOCTEXT("AppLabel", "Custom Asset Editor");
}

FText FCustomAssetEditor::GetToolkitToolTipText() const
{
	return LOCTEXT("ToolTip", "Custom Asset Editor");
}

FString FCustomAssetEditor::GetWorldCentricTabPrefix() const
{
	return LOCTEXT("WorldCentricTabPrefix", "AnimationDatabase ").ToString();
}

FLinearColor FCustomAssetEditor::GetWorldCentricTabColorScale() const
{
	return FColor::Red;
}

UMyCustomAsset* FCustomAssetEditor::GetCustomAsset()
{
	return CustomAsset;
}

void FCustomAssetEditor::SetCustomAsset(UMyCustomAsset* InCustomAsset)
{
	CustomAsset = InCustomAsset;
}

We will now be implementing our getters and setters as well as our destructor. This should be fairly straight forward but if you are interested in learning more about the functions, I encourage you to take a look at the documentation.

Implementing the Initialization method

CustomAssetEditor.cpp (Implementation file)
void FCustomAssetEditor::InitCustomAssetEditorEditor(const EToolkitMode::Type Mode, const TSharedPtr<class IToolkitHost>& InitToolkitHost, UMyCustomAsset* InCustomAsset)
{
    // Cache some values that will be used for our details view arguments
	const bool bIsUpdatable = false;
	const bool bAllowFavorites = true;
	const bool bIsLockable = false;

    // Set this InCustomAsset as our editing asset
	SetCustomAsset(InCustomAsset);

    // Retrieve the property editor module and assign properties to DetailsView
	FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
	const FDetailsViewArgs DetailsViewArgs(bIsUpdatable, bIsLockable, true, FDetailsViewArgs::ObjectsUseNameArea, false);
	DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);

    // Create the layout of our custom asset editor
	const TSharedRef<FTabManager::FLayout> StandaloneDefaultLayout = FTabManager::NewLayout("Standalone_CustomAssetEditor_Layout_v1")
	->AddArea
	(
        // Create a vertical area and spawn the toolbar
		FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical)
		->Split
		(
			FTabManager::NewStack()
			->SetSizeCoefficient(0.1f)
			->SetHideTabWell(true)
			->AddTab(GetToolbarTabId(), ETabState::OpenedTab)
		)
		->Split
		(
            // Split the tab and pass the tab id to the tab spawner
			FTabManager::NewSplitter()
			->Split
			(
				FTabManager::NewStack()
				->AddTab(PropertiesTabId, ETabState::OpenedTab)
			)
		)
	);

	const bool bCreateDefaultStandaloneMenu = true;
	const bool bCreateDefaultToolbar = true;

    // Initialize our custom asset editor
	FAssetEditorToolkit::InitAssetEditor(
		Mode,
		InitToolkitHost,
		CustomAssetEditorAppIdentifier,
		StandaloneDefaultLayout,
		bCreateDefaultStandaloneMenu,
		bCreateDefaultToolbar,
		(UObject*)InCustomAsset);
    
    // Set the asset we are editing in the details view
	if (DetailsView.IsValid())
	{
		DetailsView->SetObject((UObject*)CustomAsset);
	}
}

We can now start implementing our initialization function. This is the first function that is executed when the editor is spawned and serves as the entry point to create our custom editor layout.

We start by assigning the asset that we will be editing within this editor via the SetCustomAsset function, this will make sure that we can access functions and variables inside our custom asset via the editor.

We will now retrieve the PropertyEditorModule and provide our DetailsView with the properties defined at the top of the function.

The next step is to set up the layout of the editor. This should be fairly straightforward and I have commented the code above to provide some insight into what is happening at specific parts of the code. If you are interested in learning more I would encourage you to take a look at the FLayout documentation.

Conclusion

We have set up all of the logic for our custom asset editor and refactored our module implementation to be more clear and straightforward. We have learned how to set up a custom asset editor using the functions provided by the FAssetEditorToolkit and how to set up custom tab spawners for our editor. Finally, we have implemented our initialization function in which we take care of how the editor is spawned and what tabs are displayed within our custom editor.

If you have any questions or comments on this guide, feel free to email me at cairan.steverink@gmail.com and I will be happy to answer your questions.

Continue to Part 2

We can now continue to the second part of this guide. In the next part, we will be creating AssetTypeActions for our custom asset and spawn our custom asset editor.

Continue to part 2

Source Code

The complete source code can be found on the Github page below:
https://github.com/caiRanN/Custom-Asset-Editor-Source

Further Reading and References


Share this Post: