こんにちは、株式会社Leon Gameworksの遠藤です。

今回は、Editor Utilityを用いた開発において頻繁に利用されるメニュー追加などのエディタ拡張について解説します。内容はやや上級者向けとなっており、実装は全てC++から行います。

記事の目次

    0:動作環境

    本記事はUE5.6.1を基に執筆しており、エディターの言語は英語でスクリーンショットを撮影しております。

    本記事で作成するツールのプロジェクト一式のデータは、以下からダウンロードできます。

    今回のプロジェクトデータ

    1:基本的なメニューの追加方法

    メニューバーやツールバーに独自メニューを追加し、Editor Utilityを実行したいケースは多々あると思います。過去の記事ではブループリントから追加する方法を解説しましたが、今回はC++からの様々な追加方法について解説します。

    まず、準備として拡張ポイントとなる FExtender クラスを定義します。

    class FEUTools17EditorModule : public IModuleInterface
    {
    public:
    	//~ Begin IModuleInterface Interface.
    	virtual void StartupModule() override;
    	virtual void ShutdownModule() override;
    	//~ End IModuleInterface Interface.
    
    private:
    	TSharedPtr<FExtender> Extender;
    };

    次に、以下のように AddMenuBarExtension などを用いて、追加する箇所や内容を指定します。実際の開発では冗長にならないよう、階層ごとに関数へまとめることを推奨しますが、今回はわかりやすさを優先し、1箇所にまとめて記述しています。

    Extender = MakeShared<FExtender>();
    if (Extender.IsValid())
    {
    	Extender->AddMenuBarExtension(
    		"Help",					// 追加ポイントとなるフック
    		EExtensionHook::After,	// フックの前後どこに追加するか
    		nullptr,				// コマンドリスト
    		FMenuBarExtensionDelegate::CreateLambda([this](FMenuBarBuilder& MenuBarBuilder)
    		{
    			// プルダウンメニュー
    			MenuBarBuilder.AddPullDownMenu(
    				LOCTEXT("MenuBar", "My Menu Bar"),
    				LOCTEXT("MenuBarTooltip", ""),
    				FNewMenuDelegate::CreateLambda([this](FMenuBuilder& MenuBuilder)
    				{
    					// シンプルメニュー
    					MenuBuilder.AddMenuEntry(
    						LOCTEXT("MenuBarSimple", "Simple Menu"),
    						LOCTEXT("MenuBarSimple_Tooltip", ""),
    						FSlateIcon(),
    						FUIAction(FExecuteAction::CreateLambda([this]()
    						{
    							// 実行内容
    							UE_LOG(LogTemp, Display, TEXT("My MenuBar"));
    						}))
    					);
    				})
    			);
    		})
    	);
    }

    最後に、メニューの登録処理と除外処理を記述して完了です。

    void FEUTools17EditorModule::StartupModule()
    {
    	// メニューの登録
    	FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
    	TSharedPtr<FExtensibilityManager> menuManager = LevelEditorModule.GetMenuExtensibilityManager();
    	if (menuManager.IsValid())
    	{
    		menuManager->AddExtender(Extender);
    	}
    }
    
    void FEUTools17EditorModule::ShutdownModule()
    {
    	if (Extender.IsValid() && FModuleManager::Get().IsModuleLoaded("LevelEditor"))
    	{
    		FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
    		TSharedPtr<FExtensibilityManager> menuManager = LevelEditorModule.GetMenuExtensibilityManager();
    		if (menuManager.IsValid())
    		{
    			menuManager->RemoveExtender(Extender);
    		}
    	}
    
    	Extender.Reset();
    }

    下図のように、メニューバーにシンプルな項目を追加できました。

    ▲ メニューバーに項目を追加

    メニューバー以外の箇所に追加する場合は、AddMenuExtensionAddToolBarExtension を使用します。

    特に AddToolBarExtension の場合は、型や関数名が一部異なりますので注意が必要です。

    Extender->AddToolBarExtension(
    	"Play",
    	EExtensionHook::After,
    	nullptr,
    	FToolBarExtensionDelegate::CreateLambda([this](FToolBarBuilder& ToolBarBuilder)
    	{
    		// シンプルメニュー
    		ToolBarBuilder.AddToolBarButton(
    			FUIAction(FExecuteAction::CreateLambda([this]()
    			{
    				// 実行内容
    				UE_LOG(LogTemp, Display, TEXT("My ToolBarButton"));
    			})),
    			NAME_None,
    			FText::FromString(TEXT("My Button")),
    			FText::FromString(TEXT("")),
    			FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Levels")
    		);
    	})
    );
    
    // メニューの登録
    FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
    TSharedPtr<FExtensibilityManager> toolManager = LevelEditorModule.GetToolBarExtensibilityManager();
    if (toolManager.IsValid())
    {
    	toolManager->AddExtender(Extender);
    }
    ▲ ツールバーに項目を追加

    2:サブメニュー

    サブメニューを作成する場合は、AddSubMenu を使用します。

    MenuBuilder.AddSubMenu(
    	LOCTEXT("SubMenu", "My SubMenu"),
    	LOCTEXT("SubMenu_ToolTip", ""),
    	FNewMenuDelegate::CreateLambda([this](FMenuBuilder& MenuBuilder)
    	{
    		MenuBuilder.AddMenuEntry(
    			LOCTEXT("SubMenu1", "Menu 1"),
    			LOCTEXT("SubMenu1_Tooltip", ""),
    			FSlateIcon(),
    			FUIAction(FExecuteAction::CreateLambda([this]()
    			{
    				// 実行内容
    				UE_LOG(LogTemp, Display, TEXT("Submenu 1"));
    			}))
    		);
    		MenuBuilder.AddMenuEntry(
    			LOCTEXT("SubMenu2", "Menu 2"),
    			LOCTEXT("SubMenu2_Tooltip", ""),
    			FSlateIcon(),
    			FUIAction(FExecuteAction::CreateLambda([this]()
    			{
    				// 実行内容
    				UE_LOG(LogTemp, Display, TEXT("Submenu 2"));
    			}))
    		);
    	})
    );
    ▲ サブメニューの追加

    3:ラジオボタン

    ラジオボタンを追加したい場合は、引数の EUserInterfaceActionTypeRadioButton に設定します。ラジオボタンを複数追加し、あるボタンを押した際に他のラジオボタンのチェックを外したい場合は、処理の実行箇所を CreateLambda ではなく CreateRaw で記述し、関数に対応するラジオボタンのインデックスを渡しています。

    // ラジオボタン
    MenuBuilder.AddSubMenu(
    	LOCTEXT("RadioSubMenu", "Radio Button"),
    	LOCTEXT("RadioSubMenu_ToolTip", ""),
    	FNewMenuDelegate::CreateLambda([this](FMenuBuilder& MenuBuilder)
    	{
    		MenuBuilder.AddMenuEntry(
    			LOCTEXT("RadioButton1", "Mode 1"),
    			LOCTEXT("RadioButton1_Tooltip", ""),
    			FSlateIcon(),
    			FUIAction(
    				FExecuteAction::CreateRaw(this, &FEUTools17EditorModule::OnSelectMode, 0),
    				FCanExecuteAction(),
    				FIsActionChecked::CreateRaw(this, &FEUTools17EditorModule::IsModeSelected, 0)
    			),
    			NAME_None,
    			EUserInterfaceActionType::RadioButton
    		);
    
    		// ---以下省略---
    	})
    );
    
    ------
    
    void FEUTools17EditorModule::OnSelectMode(int32 Mode)
    {
    	CurrentModeIndex = Mode;
    	UE_LOG(LogTemp, Log, TEXT("Mode changed to: %d"), Mode);
    }
    
    bool FEUTools17EditorModule::IsModeSelected(int32 Mode) const
    {
    	return CurrentModeIndex == Mode;
    }

    また、ラジオボタンのように同様の MenuEntry を複数記述する必要があるケースでは、コマンドリストを活用するとコードをすっきりさせることができます。

    具体的には、以下のようにコマンドリストを定義し、AddMenuBarExtension などの引数に渡すことで、AddMenuEntry にコマンドを指定するだけで追加処理を記述できます。

    class FMyCommands : public TCommands<FMyCommands>
    {
    public:
    	FMyCommands()
    		: TCommands<FMyCommands>(
    			TEXT("MyCommands"),
    			LOCTEXT("MyCommands", ""),
    			NAME_None,
    			FAppStyle::GetAppStyleSetName())
    	{}
    
    	TSharedPtr<FUICommandInfo> LowCmd;
    	TSharedPtr<FUICommandInfo> MediumCmd;
    	TSharedPtr<FUICommandInfo> HighCmd;
    	TSharedPtr<FUICommandInfo> EpicCmd;
    	TSharedPtr<FUICommandInfo> CinematicCmd;
    
    	virtual void RegisterCommands() override
    	{
    		UI_COMMAND(LowCmd,       "Low",       "", EUserInterfaceActionType::RadioButton, FInputChord());
    		UI_COMMAND(MediumCmd,    "Medium",    "", EUserInterfaceActionType::RadioButton, FInputChord());
    		UI_COMMAND(HighCmd,      "High",      "", EUserInterfaceActionType::RadioButton, FInputChord());
    		UI_COMMAND(EpicCmd,      "Epic",      "", EUserInterfaceActionType::RadioButton, FInputChord());
    		UI_COMMAND(CinematicCmd, "Cinematic", "", EUserInterfaceActionType::RadioButton, FInputChord());
    	}
    };
    
    ------
    
    auto MakeMyAction = [this](uint8 Value)
    {
        return FUIAction(
            FExecuteAction::CreateLambda([this, Value]
            {
                // 実行内容
            }),
            FCanExecuteAction(),
            FIsActionChecked::CreateLambda([Value]
            {
                // チェック判定箇所
                return true;
            })
        );
    };
    
    if (!FMyCommands::IsRegistered())
    {
        FMyCommands::Register();
    }
    const FMyCommands& MyCommands = FMyCommands::Get();
    CommandList->MapAction(MyCommands.LowCmd,       MakeMyAction(0));
    CommandList->MapAction(MyCommands.MediumCmd,    MakeMyAction(1));
    CommandList->MapAction(MyCommands.HighCmd,      MakeMyAction(2));
    CommandList->MapAction(MyCommands.EpicCmd,      MakeMyAction(3));
    CommandList->MapAction(MyCommands.CinematicCmd, MakeMyAction(4));

    4:トグルボタン

    ラジオボタンと同じように、引数から EUserInterfaceActionTypeToggleButton にします。

    // トグルボタン
    MenuBuilder.AddMenuEntry(
    	LOCTEXT("ToggleButton", "Toggle Button"),
    	LOCTEXT("ToggleButton_Tooltip", ""),
    	FSlateIcon(),
    	FUIAction(
    		FExecuteAction::CreateRaw(this, &FEUTools17EditorModule::OnToggleButton),
    		FCanExecuteAction(),
    		FIsActionChecked::CreateRaw(this, &FEUTools17EditorModule::IsToggleEnabled)
    	),
    	NAME_None,
    	EUserInterfaceActionType::ToggleButton
    );
    
    ------
    
    void FEUTools17EditorModule::OnToggleButton()
    {
    	bToggle = !bToggle;
    	UE_LOG(LogTemp, Log, TEXT("Toggle %s"), bToggle ? TEXT("ON") : TEXT("OFF"));
    }
    bool FEUTools17EditorModule::IsToggleEnabled() const
    {
    	return bToggle;
    }

    5:アイコンの設定

    項目にアイコンを設定したい場合は、AddMenuEntry などの引数で指定します。

    ▲アイコンの指定箇所
    ▲項目にアイコンを設定

    6:メニューを整理する

    AddSeparator や BeginSection を使うことで、項目を線で区切って整理できます。

    MenuBuilder.AddSeparator();
    MenuBuilder.BeginSection("SectionHook", LOCTEXT("MySection", "My Section"));
    MenuBuilder.EndSection();
    ▲ メニューを整理

    7:まとめ

    本連載も第17回となり、回数を重ねてきましたので、今回は上級者向けの内容を解説しました。今回取り上げた内容は、ツール開発において頻繁に利用される要素だと思いますので、ぜひ参考にしていただければ幸いです。

    本記事で作成するツールのプロジェクト一式のデータは、以下からダウンロードできます。

    今回のプロジェクトデータ

    株式会社Leon Gameworks

    ●公式サイト
    www.leon-game.co.jp

    ●X(Twitter)
    @Leon_Gameworks

    トンコツ(遠藤俊太)

    ●トンコツ開発ブログ
    shuntaendo.hatenablog.com

    ●X(Twitter)
    @tonkotsu3656

    TEXT_トンコツ(Leon Gameworks)
    EDIT_小村仁美 / Hitomi Komura(CGWORLD)、オムライス駆