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

今回は、前回紹介した「ブループリントに含まれるグラフを取得する機能」を応用し、ブループリントエディタのツールバーに未使用ノードを一覧表示するメニューを追加してみます。本稿ではEditor Utility Widgetは使用せず、機能の実装は全てC++で行います。

記事の目次

    0:動作環境

    本記事UE5.8.1を基に執筆しており、画面のスクリーンショットはエディタの言語設定を「英語」として撮影しています。

    なお、本記事で作成するプロジェクト一式は、以下よりダウンロード可能です。

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

    1:エディタ専用モジュールの追加

    まずは .uproject にエディタ専用モジュールを追加します。

    {
    	"FileVersion": 3,
    	"EngineAssociation": "5.8",
    	"Category": "",
    	"Description": "",
    	"Modules": [
    		{
    			"Name": "EUTools23",
    			"Type": "Runtime",
    			"LoadingPhase": "Default",
    			"AdditionalDependencies": [
    				"Engine"
    			]
    		},
    		{
    			"Name": "EUTools23Editor",
    			"Type": "Editor",
    			"LoadingPhase": "PostEngineInit",
    			"AdditionalDependencies": [
    				"Engine",
    				"CoreUObject",
    				"UnrealEd",
    				"Kismet",
    				"BlueprintGraph",
    				"BlueprintEditorLibrary",
    				"ToolMenus"
    			]
    		}
    	]
    }
    ▲ エディタ専用モジュールを追加

    次に、Build.csを設定します。ブループリント内のグラフを取得するためにBlueprintEditorLibrary、ブループリントノードを扱うためにBlueprintGraphを依存モジュールとして追加します。

    using UnrealBuildTool;
    
    public class EUTools23Editor : ModuleRules
    {
    	public EUTools23Editor(ReadOnlyTargetRules Target)
    		: base(Target)
    	{
    		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
    
    		PublicDependencyModuleNames.AddRange(
    			new string[]
    			{
    				"Core"
    			});
    
    		PrivateDependencyModuleNames.AddRange(
    			new string[]
    			{
    				"CoreUObject",
    				"Engine",
    				"Slate",
    				"SlateCore",
    				"UnrealEd",
    				"ToolMenus",
    				"Kismet",
    				"BlueprintGraph",
    				"BlueprintEditorLibrary"
    			});
    	}
    }
    ▲ Build.csを追加

    2:ブループリントエディタのツールバーを拡張

    続いて、ブループリントエディタのツールバーを拡張し、独自メニューを追加します。まずは、その処理を定義するEUTools23Editor.hを作成します。

    #pragma once
    
    #include "CoreMinimal.h"
    #include "Modules/ModuleManager.h"
    
    class SWidget;
    class UEdGraphNode;
    struct FToolMenuContext;
    
    class FEUTools23EditorModule : public IModuleInterface
    {
    public:
    	//~ Begin IModuleInterface Interface
    	virtual void StartupModule() override;
    	virtual void ShutdownModule() override;
    	//~ End IModuleInterface Interface
    
    private:
    	// ブループリントエディタへメニューを登録
    	void RegisterMenus();
    
    	// [Unused Nodes]を開いた際のメニューを生成
    	static TSharedRef<SWidget> GenerateUnusedNodesMenu(const FToolMenuContext& MenuContext);
    
    	// ノードが未使用か判定
    	static bool IsUnusedNode(UEdGraphNode* Node);
    
    	// 指定したノードへ移動
    	static void JumpToNode(TWeakObjectPtr<UEdGraphNode> Node);
    };
    ▲ EUTools23Editor.h を追加

    主に、以下の3つの処理を実装します。

    RegisterMenus:ブループリントエディタのツールバーに[Unused Nodes]メニューを追加します。
    GenerateUnusedNodesMenu:現在開いているブループリントから未使用ノードを検索し、その結果を基にメニュー項目を動的に生成します。
    JumpToNode:メニューで選択したノードへ移動します。

    続いて、EUTools23Editor.cppを作成します。まずは、ツールバーへのメニュー登録処理から実装します。

    #include "EUTools23Editor.h"
    
    #include "BlueprintEditorContext.h"
    #include "BlueprintEditorLibrary.h"
    #include "EdGraph/EdGraph.h"
    #include "EdGraph/EdGraphNode.h"
    #include "EdGraph/EdGraphPin.h"
    #include "Engine/Blueprint.h"
    #include "Framework/MultiBox/MultiBoxBuilder.h"
    #include "K2Node.h"
    #include "Kismet2/KismetEditorUtilities.h"
    #include "ToolMenu.h"
    #include "ToolMenuContext.h"
    #include "ToolMenuDelegates.h"
    #include "ToolMenuEntry.h"
    #include "ToolMenus.h"
    
    #define LOCTEXT_NAMESPACE "FEUTools23EditorModule"
    
    void FEUTools23EditorModule::StartupModule()
    {
    	UToolMenus::RegisterStartupCallback(
    		FSimpleMulticastDelegate::FDelegate::CreateRaw(
    			this,
    			&FEUTools23EditorModule::RegisterMenus));
    }
    
    void FEUTools23EditorModule::ShutdownModule()
    {
    	UToolMenus::UnRegisterStartupCallback(this);
    	UToolMenus::UnregisterOwner(this);
    }
    
    void FEUTools23EditorModule::RegisterMenus()
    {
    	FToolMenuOwnerScoped OwnerScoped(this);
    
    	UToolMenu* ToolbarMenu =
    		UToolMenus::Get()->ExtendMenu(
    			"AssetEditor.BlueprintEditor.ToolBar");
    
    	FToolMenuSection& Section =
    		ToolbarMenu->FindOrAddSection(
    			"EUTools23");
    
    	FToolMenuEntry Entry =
    		FToolMenuEntry::InitComboButton(
    			"UnusedNodes",
    			FToolUIActionChoice(),
    			FNewToolMenuChoice(
    				FNewToolMenuWidget::CreateStatic(
    					&FEUTools23EditorModule::
    						GenerateUnusedNodesMenu)),
    			LOCTEXT(
    				"UnusedNodes_Label",
    				"Unused Nodes"),
    			LOCTEXT(
    				"UnusedNodes_Tooltip",
    				"Lists nodes with no connections "
    				"in the current Blueprint."),
    			FSlateIcon(),
    			false,
    			NAME_None);
    
    	Section.AddEntry(Entry);
    }
    
    TSharedRef<SWidget>
    FEUTools23EditorModule::GenerateUnusedNodesMenu(
    	const FToolMenuContext& MenuContext)
    {
    	FMenuBuilder MenuBuilder(
    		true,
    		nullptr);
    
    	const UBlueprintEditorToolMenuContext*
    		BlueprintContext =
    			MenuContext.FindContext<
    				UBlueprintEditorToolMenuContext>();
    
    	UBlueprint* Blueprint =
    		BlueprintContext
    			? BlueprintContext->GetBlueprintObj()
    			: nullptr;
    
    	if (!IsValid(Blueprint))
    	{
    		MenuBuilder.AddMenuEntry(
    			LOCTEXT(
    				"BlueprintNotFound",
    				"Blueprint could not be found."),
    			FText::GetEmpty(),
    			FSlateIcon(),
    			FUIAction(
    				FExecuteAction(),
    				FCanExecuteAction::CreateLambda(
    					[]
    					{
    						return false;
    					})));
    
    		return MenuBuilder.MakeWidget();
    	}
    
    	// この後にメニュー生成処理を追加
    
    	return MenuBuilder.MakeWidget();
    }
    ▲ EUTools23Editor.cpp を追加

    ここまで実装したら、一度ビルドしてエディタを起動してみましょう。ブループリントエディタを開くと、ツールバーに[Unused Nodes]メニューが追加されていることを確認できます。

    ▲メニューの追加を確認

    3:未使用ノードの取得

    GenerateUnusedNodesMenuでブループリントを取得したら、続いて、そのブループリントに含まれるグラフを取得する処理を追加します。

    int32 UnusedNodeCount = 0;
    
    for (UEdGraph* Graph :
    	UBlueprintEditorLibrary::ListGraphs(Blueprint))
    {
    	if (!IsValid(Graph))
    	{
    		continue;
    	}
    
    	TArray<UEdGraphNode*> UnusedNodes;
    
    	for (UEdGraphNode* Node : Graph->Nodes)
    	{
    		if (IsUnusedNode(Node))
    		{
    			UnusedNodes.Add(Node);
    		}
    	}
    
    	if (UnusedNodes.IsEmpty())
    	{
    		continue;
    	}
    
    	UnusedNodeCount += UnusedNodes.Num();
    
    	// この後にグラフごとのメニュー生成処理を追加
    }
    ▲ブループリントに含まれるグラフを取得

    続いて、IsUnusedNodeを実装します。

    bool FEUTools23EditorModule::IsUnusedNode(
    	UEdGraphNode* Node)
    {
    	if (!IsValid(Node))
    	{
    		return false;
    	}
    
    	// コメントノードなど、K2ノード以外は対象外
    	if (!Node->IsA<UK2Node>())
    	{
    		return false;
    	}
    
    	// Function Entryなど、
    	// ユーザーが削除できないノードは対象外
    	if (!Node->CanUserDeleteNode())
    	{
    		return false;
    	}
    
    	for (const UEdGraphPin* Pin :
    		Node->GetAllPins())
    	{
    		if (Pin &&
    			Pin->HasAnyConnections())
    		{
    			return false;
    		}
    	}
    
    	return true;
    }
    ▲IsUnusedNodeの実装

    ノードの収集対象は、UK2Nodeを継承するノードに限定します。各ノードについてピンの接続状態を確認し、未使用と判定されたノードを抽出します。

    未使用ノードが見つかった場合は、グラフごとにメニュー項目を生成します。先ほどのGenerateUnusedNodesMenu内で、未使用ノードを取得する処理に続けて、以下の処理を追加します。

    MenuBuilder.BeginSection(
    	Graph->GetFName(),
    	FText::FromName(Graph->GetFName()));
    
    for (UEdGraphNode* Node : UnusedNodes)
    {
    	const TWeakObjectPtr<UEdGraphNode>
    		WeakNode(Node);
    
    	FText NodeTitle =
    		Node->GetNodeTitle(
    			ENodeTitleType::ListView);
    
    	if (NodeTitle.IsEmpty())
    	{
    		NodeTitle =
    			Node->GetClass()->
    				GetDisplayNameText();
    	}
    
    	MenuBuilder.AddMenuEntry(
    		NodeTitle,
    		FText::Format(
    			LOCTEXT(
    				"JumpToNode_Tooltip",
    				"Jump to {0} in {1}."),
    			NodeTitle,
    			FText::FromName(
    				Graph->GetFName())),
    		FSlateIcon(),
    		FUIAction(
    			FExecuteAction::CreateStatic(
    				&FEUTools23EditorModule::
    					JumpToNode,
    				WeakNode)));
    }
    
    MenuBuilder.EndSection();
    ▲メニュー項目の生成処理

    ここまで実装したら、ビルドして動作を確認してみましょう。ブループリント内に未使用ノードが存在する場合は、メニューに対応する項目が生成されます。

    ▲未使用ノード一覧の確認

    未使用ノードがひとつも見つからなかった場合は、その旨を示すメニュー項目を表示するようにします。グラフの列挙処理が完了した後に、以下の処理を追加します。

    if (UnusedNodeCount == 0)
    {
    	MenuBuilder.AddMenuEntry(
    		LOCTEXT(
    			"UnusedNodeNotFound",
    			"No unused nodes were found."),
    		FText::GetEmpty(),
    		FSlateIcon(),
    		FUIAction(
    			FExecuteAction(),
    			FCanExecuteAction::CreateLambda(
    				[]
    				{
    					return false;
    				})));
    }
    ▲未使用ノードがひとつもなかった場合の処理
    ▲未使用ノードがひとつもなかった場合のメニュー

    4:対象ノードへジャンプ

    最後に、メニューから選択しノードへ移動するJumpToNodeを実装します。

    void FEUTools23EditorModule::JumpToNode(
    	TWeakObjectPtr<UEdGraphNode> Node)
    {
    	if (!Node.IsValid())
    	{
    		return;
    	}
    
    	FKismetEditorUtilities::
    		BringKismetToFocusAttentionOnObject(
    			Node.Get(),
    			false);
    }
    ▲JumpToNodeの実装

    FKismetEditorUtilities::BringKismetToFocusAttentionOnObjectに対象ノードを渡すことで、そのノードを含むブループリントエディタとグラフを開き、対象ノードへフォーカスできます。

    ここまで実装すれば完成です。

    5:まとめ

    今回は、未使用ノードを検出し、対象ノードへ移動できるメニューを実装しました。検出結果を一覧で確認できるため、各ノードが本当に不要かどうかを確認した上で、ユーザー自身が対応を判断できます。

    さらに発展させるなら、次または前の未使用ノードへ順番に移動する機能や、ツールバー上への検出数の表示、選択した未使用ノードを削除する機能などを追加するのも有効です。

    本記事で作成したプロジェクト一式は、以下よりダウンロード可能です。

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

    株式会社Leon Gameworks

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

    ●X(Twitter)
    @Leon_Gameworks

    トンコツ(遠藤俊太)

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

    ●X(Twitter)
    @tonkotsu3656

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