One of the most powerful things I've done so far is adding some custom classes I made for my personal project, Orbital: Space Rogues, into the editor's context menu. While it's not a difficult process, it can be a bit tedious. I wanted to share the exact method for doing this so that someone else would be able to serve their project's needs.
TABLE OF CONTENTS
Definitions
Development
But first, let's talk about two things; modules and factories. (If you already know about these, you can skip ahead).
MODULES
A module is a way of collecting code and allowing it to initialize and cook differently from what's known as the "primary game module" as well as the engine modules. So things like GameplayAbilities, Slate, and Niagara are all in their own "modules". You can just think of them as plugins, if that makes it easier for you.
Each module has a loading phase that determines....well, when it gets loaded. For our purposes, we want to ensure that the engine modules have initialized so that we have access to the functions and features inside of those modules when our module starts up.
So we will need to create our own module for the editor in order to get this to work.
FACTORY
The Factory, typed as UFactory in the engine, is the class responsible for actually creating binary files, like the .uasset that every asset for the editor is saved as. This Factory can be subclassed in order to change behavior for particular types of classes, which is what we'll do in order to have a factory make our asset types. This is the process that we trigger when we create a new class from the context menu.
There are a few other things we need to get involved with, but we'll look at all of it together.
Let's get to it!
CREATING THE MODULE
The first thing we need to do is create the module the module for the editor. My project is called "OSR", so keep that in mind as you look through these.
My "primary game module" is OSR, so I had to create an editor module alongside it called OSREditor. So my source folder looks like this:
Inside of that OSREditor folder, we need our .h and .cpp for the module, as well as a Build.cs file. Without these, we won't even be able to compile the module.
For now, we just need minimal code inside the module to get it to compile; we need to also go ahead and include other modules from the engine, as well as our primary game module, where the classes we want to use are actually stored.
using UnrealBuildTool;
public class OSREditor : ModuleRules
{
public OSREditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"OSR"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Core",
"Engine",
"CoreUObject",
"UnrealEd",
"SlateCore",
"Slate"
});
}
}
This is what the Build.cs file looks like in our editor module. Note the dependency module names. We publically depend on our primary game module (in this case, OSR) and add private dependencies to other classes we'll need. You'll notice Slate and SlateCore there, and we'll talk about that at the end when we add custom icons for those classes.
Also add this into your Target.cs file for the project:
if (bBuildEditor)
{
ExtraModuleNames.Add("OSREditor");
}
Next, let's look at the code we need for the .h and .cpp of the module.
// module's .h file
#pragma once
#include "Engine.h"
#include "Modules/ModuleInterface.h"
#include "UnrealEd.h"
DECLARE_LOG_CATEGORY_EXTERN(OSREditor, All, All)
class FOSREditorModule: public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
This is our module class, so we need to declare it, include the editor, engine, and interface from which it inherits. We also need to override the startup and shutdown functions.
These two functions show why it's necessary to make an editor module as a matter of course (and not just optimization). The primary game module doesn't have startup and shutdown functions.
Now, for our .cpp
// module's .cpp file
#include "OSREditor.h"
#include "Modules/ModuleManager.h"
#include "Modules/ModuleInterface.h"
IMPLEMENT_GAME_MODULE(FOSREditorModule, OSREditor);
DEFINE_LOG_CATEGORY(OSREditor)
#define LOCTEXT_NAMESPACE "OSREditor"
void FOSREditorModule::StartupModule()
{
UE_LOG(OSREditor, Warning, TEXT("OSREditor: Log Started"));
}
void FOSREditorModule::ShutdownModule()
{
UE_LOG(OSREditor, Warning, TEXT("OSREditor: Log Ended"));
}
#undef LOCTEXT_NAMESPACE
It's a pretty simple setup, to be frank, and if you look at other plugin modules or gameplay modules in the editor, you'd see something similar. The only thing left to do is to tell our project that the module exists. Open your .uproject file and note the "Modules" section:
"FileVersion": 3,
"EngineAssociation": "5.0",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "OSR",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine",
"GASCompanion",
"DeveloperSettings"
]
}
],
Here, you'll see your primary game module. You can add another module entry enclosed in curly braces by just copying and pasting this one, being sure to separate them by a comma.
,
{
"Name": "OSREditor",
"Type": "Editor",
"LoadingPhase": "PostEngineInit",
"AdditionalDependencies": [
"UnrealEd"
]
}
Now our project knows about our new editor module. If you compile at this point, it should be green across the board.
So, now the module is built. It's time to add the factory!
CREATING THE FACTORY
As I mentioned before, the factory is what's responsible for creating .uasset binary files, and then registering them with the editor. If we can't do this, we can't create an "action" for the menu to do that will create our classes.
If you look again at my OSREditor folder structure, you'll notice I've got three folders, one for each set of classes we'll need in order to achieve this. Let's open up the factory.
In my .h, I actually have four classes I want to make factories for, so I simply make all four factory declarations in the same header, and define them in the same .cpp. This just keeps the project cleaner.
Let's look at one of them.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "SolidBodyDefinitionFactory.generated.h"
/**
*
*/
UCLASS()
class OSREDITOR_API USolidBodyDefinitionFactory : public UFactory
{
GENERATED_BODY()
public:
USolidBodyDefinitionFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
};
We subclass the UFactory. Because the UFactory is actually on its own a subclass of UObject, it's registered with the UObject system on module startup, so we don't need to do anything else here.
You need to make sure that this class is also exported to your module API. For me, that's OSREDITOR_API, but if your module is called MyGameEditor, it needs to be MYGAMEEDITOR_API.
Now, let's look at the source implementation for this class.
USolidBodyDefinitionFactory::USolidBodyDefinitionFactory()
{
SupportedClass = USolidBodyDefinition::StaticClass();
bCreateNew = true;
}
UObject* USolidBodyDefinitionFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName,
EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<USolidBodyDefinition>(InParent, InClass, InName, Flags, Context);
}
That's really all it takes to create a factory for your custom classes. We need to identify the supported class in the constructor, which means you'll need to include the header file for that class. That's probably in your primary game module, and that's why we have to add a dependency to it in our editor's Build.cs file. You get the static class so that the factory knows exactly what classes or subclasses are supported.
Then you override the FactoryCreateNew() function so that you can pass your class into the NewObject<>() template.
Boom. That's it; your factory is done. Now we need to make AssetTypeActions, which are a little messier but not all that complicated.
CREATING ASSET TYPE ACTIONS
In your AssetTypeActions folder (presuming you made one like I told you to) you'll want to create a header and implementation file. You can call them whatever you'd like, but you'll need to include them in your module header and source file, soon.
Now, it's worth noting that my example is going to go a bit off the rails, but I'll explain why. Firstly, I knew all four of these classes would need to end up in the same place on the context menu. Because of that, I did this with inheritance in mind. Keep that in your brain when you see this code for the asset actions header file.
#pragma once
#include "CoreMinimal.h"
#include "AssetTypeActions_Base.h"
class OSREDITOR_API FCommonBodyActions : public FAssetTypeActions_Base
{
public:
uint32 GetCategories() override {return FAssetToolsModule::GetModule().Get().RegisterAdvancedAssetCategory(FName("Adventure Map"), FText::FromString("Adventure Map"));}
virtual const TArray<FText>& GetSubMenus() const override;
};
class OSREDITOR_API FSolidBodyDefinitionActions : public FCommonBodyActions
{
public:
virtual UClass* GetSupportedClass() const override;
virtual FText GetName() const override {return INVTEXT("Solid Body Definition");}
virtual FColor GetTypeColor() const override {return FColor::Emerald;}
};
Whew, looks like a bit of spaghetti, right? Let's go through it.
So firstly, we need to create a class that inherits from the FAssetTypeActions_Base type. We can then override a couple of functions in the subclass in order to change how the editor displays our classes in the content browser and context menus.
In FCommonBodyActions, which is the superclass I made for all of my other asset type actions, I overrode the GetCategories() and GetSubmenus() functions. I'll explain that in a moment, but just know that the values provided in those overrides apply to all of the subclasses I made.
Further down, in FSolidBodyDefinitionActions, which inherits from my superclass, we override GetSupportedClass(), which returns the class we want this button to make. We override GetName() so we can show the display name. Lastly, GetTypeColor() is a nice little cherry on top; you can change the color of the bar on the bottom of the asset icon in the editor to whatever you'd like. I opted to use some of FColor's default color structs like Emerald and Orange, but you could conceivable use the FColor() constructor to create whatever color you want.
Now, let's look at that FCommonBodyActions again and see what it's actually doing.
class OSREDITOR_API FCommonBodyActions : public FAssetTypeActions_Base
{
public:
uint32 GetCategories() override {return FAssetToolsModule::GetModule().Get().RegisterAdvancedAssetCategory(FName("Adventure Map"), FText::FromString("Adventure Map"));}
virtual const TArray<FText>& GetSubMenus() const override;
};
The GetCategories() function is actually registering a new category for us. Typically, GetCategories() wants a uint32 return type. You could put any old integer in there, but the AssetToolsModule wants an enum, which will be cast to uint32, declared as EAssetTypeCategories. If we actually inspect that enumerator, we find this:
namespace EAssetTypeCategories
{
enum Type
{
None = 0,
Basic = 1 << 0,
Animation = 1 << 1,
Materials = 1 << 2,
Sounds = 1 << 3,
Physics = 1 << 4,
UI = 1 << 5,
Misc = 1 << 6,
Gameplay = 1 << 7,
Blueprint = 1 << 8,
Media = 1 << 9,
Textures = 1 << 10,
// Items below this will be allocated at runtime via RegisterAdvancedAssetCategory
FirstUser = 1 << 11,
LastUser = 1 << 31,
// Last allowed value is 1 << 31
};
}
This is how I discovered how to add custom categories. The comment inside the enum tells us that there are a certain number of bits inside that 11 -> 31 limit that we can use to register our own categories. We'll talk about subcategories in a moment.
That gives us up to 20 additional context menu categories. So if we look back at the GetCategories() function...
uint32 GetCategories() override {return FAssetToolsModule::GetModule().Get().RegisterAdvancedAssetCategory(FName("Adventure Map"), FText::FromString("Adventure Map"));}
That's what we're doing. We're registering our custom category. The first FName is the 'key' used to ensure a unique registration (so we don't needlessly take up more bits than just 1). The second FText::FromString() is the category's name as displayed in the context menu.
"bUt wHaT aBoUt sUbMeNuS" yes yes, so let's look at that override for GetSubMenus().
// override in .h
virtual const TArray<FText>& GetSubMenus() const override;
// implementation in .cpp
#define LOCTEXT_NAMESPACE "AssetTypeActions"
const TArray<FText>& FCommonBodyActions::GetSubMenus() const
{
static const TArray<FText> AssetTypeActionSubMenu
{
LOCTEXT("AssetAdventureMapSubMenu", "Data Classes"),
};
return AssetTypeActionSubMenu;
}
So here, we actually create and return the array that contains one item for each submenu. More items means more depth, so if you did this:
static const TArray<FText> AssetTypeActionSubMenu
{
LOCTEXT("AssetAdventureMapSubMenu", "Data Classes"),
LOCTEXT("AssetAdventureMapSubMenu", "Collections"),
};
Then the path in the context menu would look like this
You can override this individually for other AssetTypeActions and create some more unique pathing like this:
So, it's been a long, complicated road to get here. But, you're still not done. None of this will show up if we don't declare these actions to the editor.
So, in your module's .h, you need to declare a shared pointer to your action class (and obviously include it in the header. )
#pragma once
#include "Engine.h"
#include "Modules/ModuleInterface.h"
#include "UnrealEd.h"
#include "AssetTypeActions/SolidBodyDefinitionActions.h"
DECLARE_LOG_CATEGORY_EXTERN(OSREditor, All, All)
class FOSREditorModule: public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
TSharedPtr<FSolidBodyDefinitionActions> SolidBodyDefinitionActions;
TSharedPtr<FSolidBodyCollectionActions> SolidBodyCollectionActions;
TSharedPtr<FStarDefinitionActions> StarDefinitionActions;
TSharedPtr<FStarSystemCollectionActions> StarSystemCollectionActions;
};
Now, we need to go to the .cpp and get ahold of the AssetTools module.
void FOSREditorModule::StartupModule()
{
UE_LOG(OSREditor, Warning, TEXT("OSREditor: Log Started"));
IAssetTools& Tools = FAssetToolsModule::GetModule().Get();
SolidBodyDefinitionActions = MakeShared<FSolidBodyDefinitionActions>();
Tools.RegisterAssetTypeActions(SolidBodyDefinitionActions.ToSharedRef());
SolidBodyCollectionActions = MakeShared<FSolidBodyCollectionActions>();
Tools.RegisterAssetTypeActions(SolidBodyCollectionActions.ToSharedRef());
StarDefinitionActions = MakeShared<FStarDefinitionActions>();
Tools.RegisterAssetTypeActions(StarDefinitionActions.ToSharedRef());
StarSystemCollectionActions = MakeShared<FStarSystemCollectionActions>();
Tools.RegisterAssetTypeActions(StarSystemCollectionActions.ToSharedRef());
}
From there, we register our asset type actions with the AssetTools module. If you build this and go to the editor and right-click in the content browser, you should see your new category, your submenus, and the buttons with icons (and custom colors) that will create the class. Hurrah! It's been an adventure! But we're still. not. done.
MAKING A CUSTOM CLASS THUMBNAIL
You'll recollect that we included Slate in our module, and now it's type to make use of it. In the Styles folder, create an AssetStyles.h and AssetStyles.cpp.
In the .h, we need this:
#pragma once
#include "Styling/SlateStyle.h"
class FOSRAssetStyles : public FSlateStyleSet
{
public:
FOSRAssetStyles();
};
and in the .cpp, for now
FOSRAssetStyles::FOSRAssetStyles() : FSlateStyleSet("OSRStyleSet")
{
const FString contentDir = FPaths::ProjectContentDir();
const FVector2D size(64.f,64.f);
}
You need to put your styleset name in the quotes in the super call in the constructor; that name is how we're going to shut it down when we're done with it.
Now, we get the content direction because we're going to add the icon path to it. Slate wants the path to the icon from disk; it won't do relative pathing via "/Game/" like you might be used to; however, because you may be working with other people on the project, it's best to call it this way so the icons work for everyone.
Now, let's add the magic into our cpp:
FOSRAssetStyles::FOSRAssetStyles() : FSlateStyleSet("OSRStyleSet")
{
const FString contentDir = FPaths::ProjectContentDir();
const FString starAsset = contentDir +
TEXT("Resources/StarAssetThumbnail.png");
const FVector2D size(64.f,64.f);
FSlateImageBrush* starBrush = new FSlateImageBrush(starAsset, size);
Set("ClassThumbnail.StarDefinition", starBrush);
}
Now, I've placed my thumbnail in "Content/Resources/", so I add that relative path to the end of contentDir so it gets it from root. This should work for everyone else in your project as well, so long as they all have the icon in the same part of the content folder.
We create a new image brush, passing in our asset path and the size we want for the image.
Then, we use Set() to actually set the class thumbnail, removing the class prefix when we do so. If your class is declared as AMyActor, you need to drop the A, so your input here would read "ClassThumbnail.MyActor", followed then by the image brush we just created.
There's still one more step to make this functional.
In your module .cpp, you need to include the path to your style header, and then place this in your StartupModule() function:
FSlateStyleRegistry::RegisterSlateStyle(*new FOSRAssetStyles);
Of course, instead of FOSRAssetStyles, you need to put the class of your declared asset style.
Then you additionally want to add this to your ShutdownModule() function:
FSlateStyleRegistry::UnRegisterSlateStyle(FName("OSRStyleSet"));
Replacing the FName() with the name you implemented in the StyleSet class on that constructor.
Now if you compile, you should also see your custom icons in that menu, rather than any default icons inherited from superclasses.
Comments