Nine Slicer Plugin
Project Type: Unreal Engine Plugin
Software used: Unreal Engine 5.3
Languages: C++
Team Size: Solo Project
About

The Nine Slicer plugin was created purely out of spite. Unreal Engine has a notoriously bad nine-slicing system that involves a lot of guess work. As someone who desired a nine-slicing tool similar to Unity’s own, I went to the market place to search for a solution. Unfortunately, there were few alternatives and all currently available nine-slicing plugins required payment. I firmly believe in making tools available to everyone because if we all have access to the same tools, we all have the potential to create amazing products. With this belief in mind, I set out to create a functional nine-slicer with robust features. This project page describes my complete development process along with the trials and setbacks I faced along the way. It also serves as a reference for anyone who also wants to create their own plugin modifying an existing asset editor.
Initial Development
The hardest part of creating the plugin was learning where and how to spawn the editor tab. Due to my previous work in creating Editor Utility Widgets in Clean Sweep, I knew Unreal had a system for spawning and integrating editor tabs. My initial main concerns were:
- How do I spawn these tabs?
- How do I link said tab to the widget editor?
- How do I even make a plugin?
Starting at question 3, I had to learn how to create a plugin. Luckily, creating a blank plugin was the easy part. I went to the plugin tab and created an empty plugin. I learned about how modules worked and both where and how my plugin initializes. This included registering a function on the startup callback to initialize the nine slicer widget menu, unregistering the menu so I could adequately load and unload the module, and spawning the utility through the module itself. My initial code was as follows:
#define LOCTEXT_NAMESPACE "FNP_NineSlicerModule"
void FNP_NineSlicerModule::StartupModule()
{
FGlobalTabmanager::Get()->RegisterNomadTabSpawner("NineSlicerUtilityTab",
FOnSpawnTab::CreateRaw(this, &FNP_NineSlicerModule::SpawnNineSlicerTab))
.SetDisplayName(NSLOCTEXT("OpenNineSlicer", "TabTitle", "OpenNineSlicer"))
.SetGroup(WorkspaceMenu::GetMenuStructure().GetToolsCategory());
UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FNP_NineSlicerModule::ExtendUMGMenu));
}
void FNP_NineSlicerModule::ShutdownModule()
{
UToolMenus::UnRegisterStartupCallback(this);
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner("NineSlicerUtilityTab");
}
void FNP_NineSlicerModule::ExtendUMGMenu()
{
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("AssetEditor.WidgetBlueprintEditor.MainMenu");
FToolMenuSection& Section = Menu->FindOrAddSection("NineSlicer");
Section.AddMenuEntry(
"OpenNineSlicer",
NSLOCTEXT("NineSlicer", "OpenNineSlicer", "NineSlicer"),
NSLOCTEXT("NineSlicer", "OpenNineSlicerTooltip", "Open the Nine Slicer widget to slice textures."),
FSlateIcon(FName("EditorStyle"), "WidgetDesigner.LocationGridSnap"),
FUIAction(FExecuteAction::CreateStatic( &FNP_NineSlicerModule::TriggerNineSlicerTab))
);
}
void FNP_NineSlicerModule::TriggerNineSlicerTab()
{
FGlobalTabmanager::Get()->TryInvokeTab(FName("OpenNineSlicer"));
}
TSharedRef<SDockTab> FNP_NineSlicerModule::SpawnNineSlicerTab(const FSpawnTabArgs&)
{
TWeakObjectPtr<UWidgetBlueprint> CurrentBlueprint =
GEditor ? Cast<UWidgetBlueprint>(GEditor->GetSelectedObjects()->GetTop<UWidgetBlueprint>()) : nullptr;
return SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[
SNew(SNineSlicerUtility)
.WidgetBlueprint(CurrentBlueprint)
];
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FNP_NineSlicerModule, NP_NineSlicer)
This initial code spawned the “NineSlicerUtilityTab” tab via the Global Tab Manager. This worked and allowed me to begin testing my prototype Nine Slicer. The big issue that presented itself was the way in which I was spawning the tab. The global tab manager was working, but it had no way of directly linking itself with the blueprint editor that I was attempting access. There was also the issue of only a single tab editor being available to spawn at a time. If I tried to open the tool in a different blueprint, it would stay locked into the initial editor it was opened in. The final big issue was the way I was linking the nine-slicer to the widget blueprint editor. As shown, when spawning the tab, I would get the GEditor singleton and get the selected object, a.k.a. the currently selected asset in the content browser. This would be sent into the spawned SNineSlicerUtility widget as an argument. This failed often as opening the editor caused the asset in the content browser to sometimes be unselected or reference incorrect widget editors.
My Nine Slicer utility code had very basic functionality. It would bind to the editor that was passed in when spawned, listen for images being selected, and display the image on the screen. Here was the code responsible for that:
void BindToWidgetEditor()
{
UE_LOG(LogTemp, Log, TEXT("Starting BindToWidgetEditor"));
if (!WidgetBP.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("Current Widget is not valid!"));
return;
}
WidgetEditor = StaticCastSharedPtr<FWidgetBlueprintEditor>(FToolkitManager::Get().FindEditorForAsset(WidgetBP.Get()));
if (WidgetEditor.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("Found Asset Editor for Current Widget!"));
WidgetEditor->OnSelectedWidgetsChanged.AddRaw(
this, &SNineSlicerUtility::UpdateSelectedImage);
bBoundToWidgetEditor = true;
UpdateSelectedImage();
}
else
{
UE_LOG(LogTemp, Error, TEXT("BindToWidgetEditor: Failed to find editor for asset"));
}
}
void UpdateSelectedImage()
{
if (!WidgetEditor.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("WidgetEditor is not valid!"));
return;
}
// Get the selected widgets
const TSet<FWidgetReference>& SelectedWidgets = WidgetEditor->GetSelectedWidgets();
bool bFoundImage = false;
// Look for an Image widget in the selection
for (const FWidgetReference& WidgetRef : SelectedWidgets)
{
if (UWidget* Widget = WidgetRef.GetPreview())
{
if (UImage* WidgetImage = Cast<UImage>(Widget))
{
UE_LOG(LogTemp, Log, TEXT("Found Image!"));
SelectedImage = WidgetImage;
bFoundImage = true;
return;
}
}
}
if (!bFoundImage)
{
UE_LOG(LogTemp, Error, TEXT("No image found"));
// No image widget was found in selection
SelectedImage = SelectedImage.IsValid() ? SelectedImage.Get() : nullptr;
}
if (ImageWidget.IsValid() && bBoundToWidgetEditor)
{
ImageWidget->SetImage(TAttribute<const FSlateBrush*>(this, &SNineSlicerUtility::GetSelectedImageBrush));
}
}
Figuring out how to link the widget editor and solve question 2 (How do I link said tab to the widget editor?) was very difficult. Question 1 was “answered”, but I knew that the global tab manager was not going to be a valid approach for linking the spawned tab to the editor. I was initially at a road block as I could not find any good documentation or resources for linking specific editors to an editor tab when spawning in said tab.
With my efforts blocked on the tab spawning front, I focused on developing the Nine Slicer Widget itself. I expanded the widget and added sliders on each side of the image corresponding to a different margin to edit. I also added drawn bars that scrolled alongside the image so it was easy to visualize where the slicing was occurring on the image itself. Finally, I added transactions that would mark the blueprint as dirty once modified, allowing for the ability to undo, redo, and save changes to the widget. With all of these in place, I spent a majority of my time experimenting with and updating the visuals of the nine slicer in order to slowly refine it.
The initial, working version of the nine slicer utility widget.
Tab Spawning Refactoring
When attempting to update my widget spawning code, I had discovered and used the Asset Editor Subsystem. Specifically, I was binding to the OnAssetOpenedInEditor callback function. Using this, I was finally able to have unique nine-slicer tabs register to each widget editor. This was exhilarating to finally get working. While it was working well, it still caused some major issues for me. Tabs wouldn’t be immediately registered until after opening an asset and would leave a lot of “unrecognized tab” tabs when the editor tried to restore previously opened tabs. I would have to close every single unrecognized tab, which would increase every time a widget editor is opened, and finally open my nine-slicer tab.
I was also doubling up registration, using both the Asset Editor Subsystem to register the tab if not already done as well as using the FToolMenuContext to assist with both registering and spawning the nine-slicer tab from the Window tab. Reading through my code more, I saw had a lot of unnecessary functions. I cleaned up my module code and reduced it down to only a few lines:
void FNP_NineSlicerModule::StartupModule()
{
// Add the Sub Menu to the UMG Editor's Window Menu
UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FNP_NineSlicerModule::RegisterMenus));
FToolMenuOwnerScoped OwnerScoped(this); // should help with unregistering everything
}
void FNP_NineSlicerModule::ShutdownModule()
{
// Unregister the Nine Slicer tab spawner
UToolMenus::UnRegisterStartupCallback(this);
UToolMenus::UnregisterOwner(this);
}
void FNP_NineSlicerModule::RegisterMenus() const
{
// Get Window Menu
UToolMenu* WindowMenu = UToolMenus::Get()->ExtendMenu("AssetEditor.WidgetBlueprintEditor.MainMenu.Window");
// Get Designer Section
FToolMenuSection& WidgetDesigner = WindowMenu->FindOrAddSection("WidgetDesigner");
// Add Nine Slicer to WidgetDesigner
WidgetDesigner.AddEntry(FToolMenuEntry::InitMenuEntry(
"NineSlicer",
INVTEXT("Nine Slicer"),
INVTEXT("Opens the Nine Slicer utility tab which allows you to edit the margins in an image when trying to nine slice it"),
FSlateIcon(FName("EditorStyle"), "WidgetDesigner.LocationGridSnap"),
FToolUIAction(FToolMenuExecuteAction::CreateLambda([this](const FToolMenuContext& Context)
{
UE_LOG(LogTemp, Log, TEXT("Nine Slicer Menu Entry Clicked"));
if (UAssetEditorToolkitMenuContext* DataContext = Context.FindContext<UAssetEditorToolkitMenuContext>())
{
RegisterAndSpawnNineSlicerTab(Cast<UWidgetBlueprint>(DataContext->GetEditingObjects()[0]));
}
else
{
UE_LOG(LogTemp, Error, TEXT("No DataContext found for NineSlicer"));
UE_LOG(LogTemp, Error, TEXT("Log Info: %s"), *Context.FindContext<UObject>()->GetName());
}
}))
));
}
void FNP_NineSlicerModule::RegisterAndSpawnNineSlicerTab(UWidgetBlueprint* DataContext) const
{
if (!DataContext) // this technically should never be called but just in case
{
UE_LOG(LogTemp, Error, TEXT("DataContext is null. Canceling Nine Slicer Spawn"));
return;
}
// Get the current Widget's editor
TSharedPtr<FWidgetBlueprintEditor> WidgetEditor = StaticCastSharedPtr<FWidgetBlueprintEditor>(FToolkitManager::Get().FindEditorForAsset(DataContext));
// Get the editor's local Tab Manager
TSharedPtr<FTabManager> TabManager = WidgetEditor.Get()->GetAssociatedTabManager();
// If the Tab is not registered, register it
if (!TabManager->HasTabSpawner(FName("NineSlicerUtility")))
{
UE_LOG(LogTemp, Log, TEXT("Registering Nine Slicer tab spawner"));
TabManager->RegisterTabSpawner(
FName("NineSlicerUtility"),
// using lamba so no extra function
FOnSpawnTab::CreateLambda([this, DataContext](const FSpawnTabArgs& Args)
{
return SNew(SDockTab)
.TabRole(NomadTab)
[
SNew(SNineSlicerUtility)
.WidgetBlueprint(DataContext)
];
})
);
}
// Try to Spawn the NineSlicerUtility tab
TabManager->TryInvokeTab(FName("NineSlicerUtility"));
}
This code was working... mostly. When opening a tab through the window menu, it worked perfectly fine. The main issue was the issue previously described; tabs were not being registered before opening the widget editor and thus when the editor attempted to restore previously opened tabs, it would make multiple unrecognized tabs. At a loss, I researched more into the editor workflow to try and solve this block. One of my searches led me to this website: UE4 Custom Resource Editor Development - Editor class definitions.
This website broke down the structure of Blueprint editors, even including what an FWorkflowCentricApplication was. This helped me understand that I needed to create a Workflow Tab Factory for the Widget Editor’s Workflow Centric Application. I got to researching existing tab factories and created my own Nine-Slicer Factory.
TSharedRef<SWidget> FNineSlicerUtilityFactory::CreateTabBody(const FWorkflowTabSpawnInfo& Info) const
{
TSharedPtr<FWidgetBlueprintEditor> BlueprintEditorPtr = StaticCastSharedPtr<FWidgetBlueprintEditor>(BlueprintEditor.Pin());
return SNew(SNineSlicerUtility)
.WidgetBlueprint(BlueprintEditorPtr->GetWidgetBlueprintObj());
}
I also updated and cleaned up my module to use the FNineSlicerUtilityFactory instead.
#define LOCTEXT_NAMESPACE "FNP_NineSlicerModule"
void FNP_NineSlicerModule::StartupModule()
{
// Add the Sub Menu to the UMG Editor's Window Menu
UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FNP_NineSlicerModule::RegisterMenus));
FToolMenuOwnerScoped OwnerScoped(this); // should help with unregistering everything
}
void FNP_NineSlicerModule::ShutdownModule()
{
// Unregister the Nine Slicer tab spawner
UToolMenus::UnRegisterStartupCallback(this);
UToolMenus::UnregisterOwner(this);
}
void FNP_NineSlicerModule::RegisterMenus() const
{
IUMGEditorModule& UMGEditorModule = FModuleManager::LoadModuleChecked<IUMGEditorModule>("UMGEditor");
UMGEditorModule.OnRegisterTabsForEditor().AddLambda([this](const FWidgetBlueprintApplicationMode& ApplicationMode, FWorkflowAllowedTabSet& AllowedTabSet)
{
TSharedPtr<FWorkflowTabFactory> Factory = MakeShareable(new FNineSlicerUtilityFactory(ApplicationMode.GetBlueprintEditor()));
if (AllowedTabSet.GetFactory(Factory->GetIdentifier()))
return; // Already registered
AllowedTabSet.RegisterFactory(Factory);
});
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FNP_NineSlicerModule, NP_NineSlicer)
Now my code was very concise, clean, and efficient. The Workflow Tab Factory registered the new tab directly to the Window menu automatically which worked perfectly for me. Now, I had completely finished the registering, spawning, and linking of the nine-slicer tab. I’d finally answered all of my questions with a solid answer, as well as paved a starting point for any new custom tabs I would want to create in the future. Now all that’s left was to update the Nine Slicer itself and improve its functionality. The current state of the utility was as follows:

The Nine-Slicer had sliders on each side to edit the margins along with buttons to change the color of the margins. I also had a reset button that set the margins back to 0 as well as text in the tab to show the current value of the margin. This was mainly for my own debugging to make sure that the margins aligned with the image.
Nine-Slicer Tab Updates
Now there was very few changes I wanted to make in order to call this plugin-in fully finished:
- Swap out the sliders for draggable margins,
- Add plugin settings,
- and make images scale and their aspect ratio.
I started making these changes in the given order, beginning with adding draggable margins. For ease, I made the draggable margins a separate widget that layered on top of the image in the original tab, occupying the same overlay slot position. I knew it needed margins associated with each bar along with delagates to have the main Nine-Slicer Utility Tab to listen to changes in the margins. I created the following class:
class SDraggableMargins : public SLeafWidget
{
SLATE_DECLARE_WIDGET(SDraggableMargins, SLeafWidget)
public:
SLATE_BEGIN_ARGS(SDraggableMargins)
: _MarginColor(FLinearColor::Red)
, _LeftMargin(0.f)
, _RightMargin(1.f)
, _TopMargin(0.f)
, _BottomMargin(1.f)
, _OnLeftMarginChanged()
, _OnRightMarginChanged()
, _OnTopMarginChanged()
, _OnBottomMarginChanged()
, _OnMouseCaptureEnd()
{}
SLATE_ATTRIBUTE(FLinearColor, MarginColor)
SLATE_ATTRIBUTE(float, LeftMargin)
SLATE_ATTRIBUTE(float, RightMargin)
SLATE_ATTRIBUTE(float, TopMargin)
SLATE_ATTRIBUTE(float, BottomMargin)
SLATE_EVENT(FOnFloatValueChanged, OnLeftMarginChanged)
SLATE_EVENT(FOnFloatValueChanged, OnRightMarginChanged)
SLATE_EVENT(FOnFloatValueChanged, OnTopMarginChanged)
SLATE_EVENT(FOnFloatValueChanged, OnBottomMarginChanged)
SLATE_EVENT(FSimpleDelegate, OnMouseCaptureEnd)
SLATE_END_ARGS()
SDraggableMargins();
void Construct( const FArguments& InArgs );
// SWidget overrides
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
virtual FVector2D ComputeDesiredSize(float) const override { return FVector2D(1, 1); }
virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
TSlateAttribute<FLinearColor> MarginColor;
TSlateAttribute<float> LeftMargin;
TSlateAttribute<float> RightMargin;
TSlateAttribute<float> TopMargin;
TSlateAttribute<float> BottomMargin;
void SetLeftMargin(float InLeftMargin)
{
LeftMargin.Set(*this, InLeftMargin);
OnLeftMarginChanged.ExecuteIfBound(InLeftMargin);
}
void SetRightMargin(float InRightMargin)
{
RightMargin.Set(*this, InRightMargin);
OnRightMarginChanged.ExecuteIfBound(InRightMargin);
}
void SetTopMargin(float InTopMargin)
{
TopMargin.Set(*this, InTopMargin);
OnTopMarginChanged.ExecuteIfBound(InTopMargin);
}
void SetBottomMargin(float InBottomMargin)
{
BottomMargin.Set(*this, InBottomMargin);
OnBottomMarginChanged.ExecuteIfBound(InBottomMargin);
}
void SetMarginsWithoutDelegate(FMargin InMargins)
{
LeftMargin.Set(*this, InMargins.Left);
RightMargin.Set(*this, InMargins.Right);
TopMargin.Set(*this, InMargins.Top);
BottomMargin.Set(*this, InMargins.Bottom);
}
void SetHandleLetterVisibility(ECheckBoxState InState);
private:
FOnFloatValueChanged OnLeftMarginChanged;
FOnFloatValueChanged OnRightMarginChanged;
FOnFloatValueChanged OnTopMarginChanged;
FOnFloatValueChanged OnBottomMarginChanged;
FSimpleDelegate OnMouseCaptureEnd;
enum class EBarType { None, Vertical, Horizontal };
EBarType DraggingBarType = EBarType::None;
int32 DraggingBarIndex = INDEX_NONE; // 0 = Left/Bottom, 1 = Right/Top
bool HandleLetterVisibility = false;
// Helper to check if mouse is near a bar
bool HitTestBar(const FVector2D& LocalPos, const FVector2D& Size, EBarType& OutType, int32& OutIndex) const;
};
This class featured the properties and delegates needed for margins as well as a method for grabbing and dragging the drawn margins. The class also had a lot of customizable elements within it including handles, bar grab distance, and color. I linked this class within SNineSlicerUtility using the SAssignNew function to spawn the draggable margins class. There I linked the margin colors as well as the delegates to already existing functions in the previous versions of the plugin.
SAssignNew(DraggableMargins, SDraggableMargins)
.MarginColor_Lambda([this]()
{
return MarginColor;
})
.OnLeftMarginChanged(this, &SNineSlicerUtility::OnLeftMarginSliderChanged)
.OnRightMarginChanged(this, &SNineSlicerUtility::OnRightMarginSliderChanged)
.OnTopMarginChanged(this, &SNineSlicerUtility::OnTopMarginSliderChanged)
.OnBottomMarginChanged(this, &SNineSlicerUtility::OnBottomMarginSliderChanged)
.OnMouseCaptureEnd(this, &SNineSlicerUtility::MarkImageMarginsAsDirty)
Now with this class set up, I just wanted to add a few more features to the main nine slicer as well as add settings to customize those features. First, I added a button to toggle Handle Letter Visibility, a mode where the handles had the letters L, R, T, and B (Left, Right, Top, and Bottom respectively) to better display which bar corresponds to what margin. Next, I added a fourth color button which corresponded to a custom color. Initially, I wanted this to be a modifiable property in settings, but I found it worked better to have the tab directly support changing colors in case the red, blue, and green didn’t provide enough visual difference in the image you were modifying.
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(0, 0, 5, 0))
[
SNew(SButton) // Custom Color Button
.OnClicked_Lambda([this]()
{
IsCustomColorModeActive = true;
MarginColor = CustomColor;
return FReply::Handled();
})
.ButtonColorAndOpacity(FColor(56, 56, 56))
[
SNew(SImage)
.Image(FAppStyle::Get().GetBrush("AdvanceWidgets.Default"))
.ColorAndOpacity(FLinearColor::White)
]
.ToolTipText(TAttribute<FText>(FText::FromString("Sets the margin color to the custom color next to this button")))
]
+ SHorizontalBox::Slot()
.MaxWidth(100.f)
.Padding(FMargin(0, 0, 5, 0))
[
SNew(SColorBlock) // Color Picker
.OnMouseButtonDown(this, &SNineSlicerUtility::OnColorBlockMouseDown)
.Color_Lambda([this]()
{
return CustomColor;
})
.Cursor_Lambda([this]()
{
return EMouseCursor::Hand;
})
.ToolTipText(TAttribute<FText>(FText::FromString("A user specified custom color")))
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(0, 0, 5, 0))
[
SNew(SCheckBox) // Toggle Handle Letters
.OnCheckStateChanged(this, &SNineSlicerUtility::OnHandleLetterVisibilityChanged)
.ToolTipText(TAttribute<FText>(FText::FromString("Toggles handle letter visibility")))
]
The final hurdles I had to jump through was creating the settings and making the images scale via their aspect ratio. I created my own UDeveloperSettings and added 4 settings: Precision controlling how many decimal places the bar should round to, Margin Grab Tolerance controlling how far the margins can be grabbed, Margin Thickness controlling how thick the margins are drawn, and Margin Handle Size controlling how big the circular handles are drawn in the center of each margin. I set up basic functionality to load these settings and default values that were currently being hard-coded into the widget. The user settings created a .ini file called UserEditorSettings and stored the settings within that config file. The values were loaded by the nine-slicer tab and the draggable margins as so:
const UNineSlicerUserSettings* UserSettings = GetDefault<UNineSlicerUserSettings>();
int SettingsPrecision = UserSettings->Precision;
After linking the settings to every part of the plugin needing it, cleaning up the code, fixing various bugs, and placing the image in a scale box to finish the last feature, I was officially done with version 1.0 of the plugin!
What’s Next?
My current plans for the near future are to release the plugin for free on Fab, make the plugin’s code publicly available, make an in-depth video discussing and documenting the creation of the plugin, and continue updating the plugin to support more than just images. I’d eventually want to set it up to work with any widget components that include an image brush. I’d also like to get the plugin compiled for different Unreal Engine versions. As of now, the code is entirely in Unreal Engine 5.3, but I hope to expand it to more versions. I think it will be an exciting challenge to see how my code may need to change based off of engine changes.
What I Learned
This plugin gave me a fantastic deep dive into the Unreal Engine architecture. It allowed me to explore various parts of the Unreal Engine source code needed to understand what parts of the engine I wished to extend. It was a fully C++ project which forced me to momentarily abandon my Blueprint knowledge. It taught me all about how to make a plugin, how to interface and extend the editor, and how to debug any issues I faced along the way. Most importantly, this project forced me to deep dive into Unreal’s documentation which. I wasn’t reading given tutorials from Epic Games, but rather analyzing the C++ class structure and hierarchy in order to expand the necessary systems. Overall, this plugin has given me a lot of insight, and I hope to continue developing more plugins to make both my and the greater community’s productive workflow easier.