[UE] Plugin Manager
by Nate Maxwell
I’ve been poking around the broader facility pipeline code and see this
interesting implementation. Each python module has a /module/src/code.py file,
but there is also a /module/ctx/context.py file in every repo. This
context.py file gets loaded by the sys path manager when the module is
imported at runtime, and it always contains a context class that describes
implementation and tracking details, namely in the form of relating environment
variables to module variables.
This got me thinking about our unreal setup in the Viz department. We work heavily with plugins as we can have anywhere from 2 to 4 projects running at one time, and they could last anywhere from 2 days to 18 months. Plugins make it extremely easy to get a new project up and running.
I’ve recently overhauled our unreal toolbar to create buttons in the play toolbar rather than using a widget the user must insert at the top of the window.
Each plugin now contains a context.json file that we’ve started using for
broader application understanding of the currently loaded toolset. The file is
located in the plugin root. A simplistic version would look like this:
{
"plugin_name": "Example Toolset",
"startup_cmd": "import example_toolset; example_toolset.main()"
}
Additionally, each plugin contains a simple python file that runs the primary entry point for the plugin, typically a widget, and would look something like this:
# example_toolset.py
from pathlib import Path
import unreal
_UTIL_SUBSYS = unreal.get_editor_subsystem(unreal.EditorUtilitySubsystem)
PLUGIN_NAME = 'Example_Toolset'
def launch_plugin_menu() -> None:
menu_path = Path(f'/{PLUGIN_NAME}/PluginMenu.PluginMenu')
widget = unreal.load_asset(menu_path.as_posix())
_UTIL_SUBSYS.spawn_and_register_tab(widget)
def main() -> None:
launch_plugin_menu()
if __name__ == '__main__':
main()
Since these are in the plugin’s Content/Plugins directory, there’s no need to
worry about managing them on the path.
From here we call the ue_plugins.py file from our init_unreal.py file to
loop through all plugins and check for a context.json file.
# ue_plugins.py
import json
from pathlib import Path
import editor
import unreal
PROJECT_DIR = Path(unreal.SystemLibrary.get_project_directory())
PLUGINS_DIR = Path(PROJECT_DIR, 'Plugins')
PLUGIN_NAME_K = 'plugin_name'
PLUGIN_CMD_K = 'startup_cmd'
SHELF_ICON = unreal.Name('EditorViewport.ShaderComplexityMode')
BUTTON_ICON = unreal.Name('WidgetDesigner.LayoutTransform')
def load_toolbar_plugins() -> None:
menu = editor.create_toolbar_submenu(section_name='Lucid',
dropdown_name='Plugins',
small_style_name=SHELF_ICON)
for p in PLUGINS_DIR.glob('*'):
ctx_file = Path(p, 'context.json')
if not ctx_file.exists():
continue # Not a pipeline friendly plugin
with open(ctx_file) as file:
ctx_data = json.load(file)
label = ctx_data[PLUGIN_NAME_K]
command = ctx_data[PLUGIN_CMD_K]
editor.add_dropdown_button(menu, label, command, BUTTON_ICON)
with the editor.create_toolbar_submenu and editor.add_dropdown_button
functions coming from editor.py and are as follows:
# editor.py
import unreal
_DEFAULT_ICON = unreal.Name('Log.TabIcon')
_SECTION = unreal.Name('actions')
_SECTION_LABEL = unreal.Text('Actions')
_OWNING_MENU = 'LevelEditor.LevelEditorToolBar.PlayToolBar'
def _get_play_toolbar(menu_name: str = _OWNING_MENU) -> unreal.ToolMenu:
tool_menus = unreal.ToolMenus.get()
return tool_menus.find_menu(
unreal.Name(menu_name)
)
def create_toolbar_submenu(section_name: str,
dropdown_name: str,
small_style_name: unreal.Name = _DEFAULT_ICON
) -> unreal.Name:
"""Add a dropdown to the Play toolbar.
Args:
section_name (str): The toolbar section to group under (created if
missing).
dropdown_name (str): The visible name of the dropdown.
small_style_name(unreal.Name): The name of the icon to use for the
button.
Returns:
unreal.Name: The submenu id.
"""
tool_menus = unreal.ToolMenus.get()
play_toolbar = _get_play_toolbar()
play_toolbar.add_section(
section_name=unreal.Name(section_name),
label=unreal.Text(section_name)
)
entry_name = unreal.Name(f'{dropdown_name.replace(" ", "")}')
combo = unreal.ToolMenuEntry(
name=entry_name,
type=unreal.MultiBlockType.TOOL_BAR_COMBO_BUTTON
)
combo.set_label(unreal.Text(dropdown_name))
combo.set_tool_tip(unreal.Text(dropdown_name))
combo.set_icon(unreal.Name('EditorStyle'), small_style_name)
play_toolbar.add_menu_entry(unreal.Name(section_name), combo)
popup_id = unreal.Name(f'{_OWNING_MENU}.{entry_name}')
popup = tool_menus.find_menu(popup_id) or tool_menus.register_menu(popup_id)
popup.add_section(_SECTION, _SECTION_LABEL)
tool_menus.refresh_all_widgets()
return popup_id
def add_dropdown_button(menu_id: unreal.Name,
label: str,
command: str,
small_style_name: unreal.Name = _DEFAULT_ICON) -> None:
"""Add a menu item to an existing drop-down menu.
Args:
menu_id (unreal.Name): The submenu id to add to.
label (str): The entry label.
command (str): The string python command for the button to exec.
small_style_name(unreal.Name): The name of the icon to use for the
button.
"""
tool_menus = unreal.ToolMenus.get()
popup = tool_menus.find_menu(menu_id) or tool_menus.register_menu(menu_id)
popup.add_section(_SECTION, _SECTION_LABEL)
str_id = unreal.StringLibrary.conv_name_to_string(menu_id)
entry = unreal.ToolMenuEntry(
name=unreal.Name(
# Ensure unique name
f'Item_{hash((str_id, label)) & 0xffffffff:x}'),
type=unreal.MultiBlockType.MENU_ENTRY
)
entry.set_label(unreal.Text(label))
entry.set_tool_tip(unreal.Text(label))
entry.set_string_command(
type=unreal.ToolMenuStringCommandType.PYTHON,
custom_type=unreal.Name(''),
string=command
)
entry.set_icon(unreal.Name('EditorStyle'), small_style_name)
popup.add_menu_entry(unreal.Name('actions'), entry)
tool_menus.refresh_menu_widget(menu_id)
So we start up the engine, search all the plugins for a context.json file,
and then run the string python command to launch and register a plugin widget.
Our core plugin that manages the unreal toolbar is loaded into every project,
and from here we optionally load each other plugin and add a button to the
toolbar for them.
This is a simplistic version of our implementation, as the context.json file
contains more data about what kind of tool is being inspected. Some plugins are
considered ‘core’ plugins and get treated special, while others are project
dependent and get added to the plugins drop down.
Maybe this isn’t anything interesting to you, or maybe it inspires you to think about your plugin management in Unreal. Either way I thought it was worth sharing.
tags: Unreal - Plugins - Python - UMG