Plugin Development in Unreal: Part 2
This blog post is part of a series of posts about Unreal Engine plugin development. Click here to see all posts or click here to see the first post in this series.
Adding A Context Menu Item for UWorld Objects
The next feature that we will be working on is adding an item in a menu, so that when you right click on a Map, you can add an item to the menu that appears. In our case, we would like to have that item appear only when we right click on a map. Clicking that menu item will call some code that will add the selected sublevel to the base level. I chose this example because I consider it illustrates reasonably well the process of modifying assets through user interaction, as well as being useful for many use cases.
Adding an item to the menu is another of those features that are not thoroughly documented yet achievable. There are a couple of examples here and there, but it can be difficult to know exactly what changes are required to your codebase to achieve this. There are a couple of books that seem to have some good content regarding this, as well as some publicly available tutorials. I also saw the official UE plugin development tutorial on the UE4 learn page, which does cover this.
It boils down to two things. Firstly, you need to create a class that overrides the FAssetTypeActions_Base class. Secondly, you need to register that class. I find with C++ code it is hard to share a simple snippet of code that contains the whole story, because normally you need to include the right files on the top of your cpp file, as well as perform the right modifications in the header file. For this reason, I won’t attempt to copy the whole codebase here, and will instead document what I did and link to a working version of the code on GitHub, from which you can copy the code to your project -or which you can use as a base for your plugin. Here is the diff of what is required to add right click functionality to your project, and a tag with it implemented here.
Inheriting from FAssetTypeActions_Base is relatively simple, and only involves overriding the following methods:
List of functions to override
Of those, the most important is GetActions. For each entry we want to add to the Menu Entry, we need to call “AddMenuEntry” on the MenuBuilder object. It takes four parameters: the text, the tooltip text, the icon slate object and a “FUIAction” object. The FUIAction object itself takes two lambda parameters, one of which is the code that gets called when the item is clicked, and another which indicates if the menu item should be usable for the selected objects. Below is an example add entry call:
MenuBuilder.AddMenuEntry( LOCTEXT("TextAsset_ReverseText", "Add sublevel"), LOCTEXT("TextAsset_ReverseTextToolTip", "Adds a sublevel to the base level."), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([=] { UE_LOG(LogTemp, Warning, TEXT("Right click OK.")); }), FCanExecuteAction::CreateLambda([=] { UE_LOG(LogTemp, Warning, TEXT("Checking if should show in menu.")); return true; }) ) );
Then you can register the class you created in your main module class:
void FTutorialExampleModule::RegisterAssetTools() { IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get(); auto Action = MakeShareable(new FExampleMenuActions()); AssetTools.RegisterAssetTypeActions(Action); RegisteredAssetTypeActions.Add(Action); } void FTutorialExampleModule::UnregisterAssetTools() { FAssetToolsModule* AssetToolsModule = FModuleManager::GetModulePtr<FAssetToolsModule>("AssetTools"); if (AssetToolsModule != nullptr) { IAssetTools& AssetTools = AssetToolsModule->Get(); for (auto Action : RegisteredAssetTypeActions) { AssetTools.UnregisterAssetTypeActions(Action); } } }
Below is a screenshot of this basic boilerplate at work:
Log output on the left, and new right-click menu option on the right.
We can see that the basic code structure is working. Now all we need to do is implement the logic. At a high level we can assume the code will involve getting the base level, after checking if it has been created, and adding a sublevel. Some of this has overlap with the code that we had created, and ideally there would be a place where we would avoid duplication of string variables. In my case I will create a class with static variables as its simplest, but creating custom settings for the module would also be an option.
With regards to the lambda that is in charge of checking whether the menu item should be grayed out, we need it to be grayed out if the user attempts to add “BaseLevel” to itself. Here is the code that does that:
FCanExecuteAction::CreateLambda([=] { for (auto& World : WorldAssets) { if (World.IsValid()) { // Cannot add BaseLevel to itself. if (World->GetMapName() == FString(TEXT("BaseLevel"))) { return false; } } } return true; }) )
With regards to adding the map as a sublevel, I had a look around and found the UWorld object has an AddLevel method. Initially I was hopeful this would be helpful, but I feel that method is meant to be used at runtime by game developers, because even though I am able to invoke the method it does not seem to have any effects on the saved level on disk.
I tried other methods, such as AddPartitionSublevel, but that resulted in a crash and honestly, I am not really certain that function is related to what I want to do. I attempted to see if anybody was doing anything similar online, but the search results were full of people creating dynamically generated worlds.
This could be the end of the blog post, but I thought I would take you through a process you can follow whenever you get stuck on plugin development. Overall, Unreal Engine is a very well-engineered piece of software. If something is done by it, generally speaking the code that performs that task will be modular enough that you can call it from your plugins. The only real difficulty lies in identifying how to do that. In our case, we know that users can add levels manually by opening the “Levels” view and clicking “Add Existing”, as below:
Inspecting native UE4 functionality
What I did was download the UE4 source code as described here, and grep for the “Add Existing…” string within the code base. It is important to choose a string that is unique enough that you won’t be swamped with results. You can do this using “grep” from Linux, or WSL in Windows. The results of the grep will point you to a file that contains the code for generating the UI, as below:
Engine/Source/Editor/WorldBrowser/Private/LevelCollectionCommands.h:50: UI_COMMAND( World_AddExistingLevel, "Add Existing...", "Adds an existing level", EUserInterfaceActionType::Button, FInputChord() );
World_AddExistingLevel is another identifier that we can then grep for. Following through the rabbit hole, you need to keep digging until you find the code that is relevant. In this case, I found a function called “HandleAddExistingLevelSelected”, which contained a reference to a class called EditorLevelUtils, as well as a function within it called “AddLevelsToWorld”, documented here.
I created some code that uses that function, and it works. The only issue is that if this code is called when the map isn’t opened, this results in a crash. This is probably because the code path was never considered to be callable unless the map is currently opened in editor. A workaround would be to open the map prior to calling this function, or alternatively check which map is currently opened and refuse to call the function if it is not BaseLevel. I’ll opt for the latter because it is simpler in this case. The full code on Github at this point in time can be found here. The relevant part is highlighted here:
FExecuteAction::CreateLambda([=] { UE_LOG(LogTemp, Warning, TEXT("Add sublevel clicked.")); // Check that the "BaseLevel" has been created at some point. UWorld* BaseLevel = CastChecked<UWorld>(StaticLoadObject(UWorld::StaticClass(), NULL, *TutorialExampleSettings::BaseLevel)); if (!IsValid(BaseLevel)) { FText DialogText = FText::FromString("Base level doesn't exist. Please create using 'Create Base Level' button."); FMessageDialog::Open(EAppMsgType::Ok, DialogText); return; } // Check that the currently open level is "BaseLevel". FEditorModeTools& EditorTools = GLevelEditorModeTools(); UWorld* CurrentlyOpenWorld = EditorTools.GetWorld(); if (CurrentlyOpenWorld->GetMapName() != FString(TEXT("BaseLevel"))) { FText DialogText = FText::FromString("This functionality can only be invoked if BaseLevel is currently open. Open the 'BaseLevel' level and try again."); FMessageDialog::Open(EAppMsgType::Ok, DialogText); return; } // Add all levels. for (auto& World : WorldAssets) { if (World.IsValid()) { FString PathName = World->GetPathName(); UE_LOG(LogTemp, Warning, TEXT("Processing '%s'."), *PathName); ULevelStreaming* AddedLevel = UEditorLevelUtils::AddLevelToWorld(BaseLevel, *PathName, ULevelStreamingDynamic::StaticClass()); if (!IsValid(AddedLevel)) { UE_LOG(LogTemp, Warning, TEXT("Could not add level!")); } else { UE_LOG(LogTemp, Warning, TEXT("Level added successfully.")); } } } }),
In Conclusion
In this blog post series so far we explored certain abstract features that we can now implement in our own plugins. For example, we investigated and implemented how to duplicate assets within the editor, a primitive that we can use in many ways. Additionally, we implemented a right click action that loads assets from disk and performs modifications on them, using functionality that we discovered by inspecting the unreal engine source code.
This gives users of our plugin a more streamlined workflow and doesn’t require them to know what the levels view is, nor how to enable it. It also highlights certain features of the Unreal Engine architecture that we can take advantage of in order to tackle unknown problems we may face whenever programming something that may not be thoroughly documented.
Stay tuned for part three of this tutorial series, where we will cover modifying project settings.