ラーメン好きプログラマーのメモ帳

技術系の記事やメモを書いていきます。

Blutilityでノード置換ツールを作ってみた~関数ノード編~

この記事はUnreal Engine 4 (UE4) #2 Advent Calendar 2019の14日目の記事です。
qiita.com

はじめまして、くぼっちです。
アドカレ、ブログ投稿共に初めてのため暖かい目で見ていただけると助かります。

目次

はじめに

BPの処理をC++に移行する作業を行っていた時に大量のノード置換作業が発生したので置換ツールを作ってみました。
今回作成する置換ツールで対応するノードは「関数ノード」、「変数ノード」です。
どちらも内容が長くなっためこの記事では「関数ノード」の置換方法について紹介します。

※変数ノードの置換処理は記事を書くのが間に合わなかったので後日書きます、、、すみません、、、

紹介するノード置換ツールですが、私が使用するときに必要とした最低限の機能しかないので、
ほしい機能がなかったり、処理の抜けがあったりするかもしれないのでメモ程度に見ていただけると幸いです。

注意事項

バージョンに関して

UE4のバージョンは4.22.3です。

4.23以降では今回使用している UGlobalEditorUtilityBase が削除されているので別途対応が必要です。

※4.23で削除された Blutility ですが キンアジ様 の以下の記事を参考にして4.23でも同じような感じで表示できました! いつもお世話になっております。

kinnaji.com

※置換処理自体を4.23で運用していないため今回のバージョンは4.22.3とさせて頂きます。

置換ツールに関して

Undoに関して

今回作成するノード置換ツールは Undo(Ctrl + Z) して置換したノードを元に戻してBPをコンパイルしたときにエラーが発生します。

したがって、変更を戻す場合はリバートするようにお願いします。

恐らくノードを削除していることが原因みたいなのですが、Transaction を使用して作業情報を保存してもUndoがうまくいかなかったので、対応方法を知っている方がいらっしゃいましたら是非教えていただきたいです。

Plugin作成

Editor系のモジュールが含まれているとパッケージ化に失敗するのでPluginを作成してその中に置換ツール用のクラスを作成します。

f:id:Kubotti0131:20191212212559p:plain

今回はブランクで作成しました。

Build.csの設定

必要なモジュールを設定していきます。

PrivateDependencyModuleNames.AddRange(
    new string[]
    {
        "CoreUObject",
        "Engine",
        "Slate",
        "SlateCore",
        "UnrealEd",          // KismetEditorUtilityies で使用する
        "Blutility",         // Blutility で使用する
        "BlueprintGraph",    // ノード系で使用する
        // ... add private dependencies that you statically link with here ... 
    }
    );

.uplugin の設定

モジュールのTypeをEditorにしておきます。

"Modules": [
    {
      "Name": "BlueprintNodeReplaceTool",
      "Type": "Editor",
      "LoadingPhase": "PreLoadingScreen"
    }
]

ここまででプラグインの設定が完了したので実装していきます。

処理の実装

BlueprintNodeReplacer.h

Blutilityとして作成するためにUGlobalEditorUtilityBaseを継承したクラスを作成します。
作成したヘッダーがこちら。

#include "CoreMinimal.h" 
#include "UObject/Object.h" 
#include "Blutility/Classes/GlobalEditorUtilityBase.h" 

#include "BlueprintNodeReplacer.generated.h" 

class UBlueprint;
class UEdGraphNode;
class UEdGraph;
class UK2Node_CallFunction;
class UK2Node_Variable;


UCLASS(Abstract, hideCategories = (Object), Blueprintable)
class UBlueprintNodeReplacer : public UGlobalEditorUtilityBase
{
    GENERATED_BODY()

protected:

    /**
    * @brief 指定したFunctionNodeを置換(Pure,Callable)
    * @param TargetBP 置換するノードが存在しているBP
    * @param TargetNodeName 置換対象のノード名
    * @param NewNodeOwnerClass 新規作成するノードを保持しているクラス
    * @param NewNodeName 新規作成するノードのノード名
    * @return true = 置換成功、 false = 置換失敗
    */
    UFUNCTION(BlueprintCallable, meta = (DisplayName = "ReplaceBlueprintFunctionNode", Keywords = "BlueprintNodeReplacer"), Category = "BlueprintNodeReplacer")
    bool ReplaceBlueprintFunctionNode(UBlueprint* TargetBP, FName const& TargetNodeName, UClass const* NewNodeOwnerClass, FName const& NewNodeName);

protected:

    /**
    * @brief 関数ノードの生成
    * @param TargetGraph 置換対象のグラフ(EventGraph,FunctionGraph,MacroGraph...etc)
    * @param SetFunction 新規追加するノードに設定する関数
    * @param NodePosX ノードのX座標
    * @param NodePosY ノードのY座標
    */
    UK2Node_CallFunction* CreateFunctionNode(UEdGraph* TargetGraph, UFunction const * SetFunction, int32 NodePosX, int32 NodePosY);

    /**
    * @brief BPノードのピンの置換
    * @note 古いノードにあるピンが新しいノードにもある場合接続します
    * @param OldNode 置換対象の古いノード
    * @param NewNode 置換後の新しいノード
    */
    void ReplaceNodePinLinks(UEdGraphNode const* OldNode, UEdGraphNode const* NewNode);

    /**
    * @brief 関数ノードの置換
    * @param TargetGraph 置換対象のグラフ(EventGraph,FunctionGraph,MacroGraph...etc)
    * @param OldNode 置換対象の古いノード
    * @param SetFunction 新規追加するノードに設定する関数
    * @return true = 置換成功、 false = 置換失敗
    */
    bool ReplaceFunctionNode(UEdGraph* TargetGraph, UK2Node_CallFunction* OldNode, UFunction const * SetFunction);

protected:

    // 置換を実行するBlueprint 
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "BlueprintNodeReplacer")
    UBlueprint* TargetBlueprint;

    // 置換する対象のノード名 
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "BlueprintNodeReplacer")
    FName TargetNodeName;

    // 新規作成するノードを保持しているクラス
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "BlueprintNodeReplacer")
    UClass* CreateNodeClass;

    // 新規作成するノードのノード名
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "BlueprintNodeReplacer")
    FName CreateNodeName;
};

BlueprintNodeReplacer.cpp

ここから各関数を解説していきます。

CreateFunctionNode(関数ノードの生成)

UK2Node_CallFunction* UBlueprintNodeReplacer::CreateFunctionNode(UEdGraph* TargetGraph, UFunction const * SetFunction, int32 NodePosX, int32 NodePosY)
{
    UK2Node_CallFunction* new_node = nullptr;
    if (TargetGraph == nullptr || SetFunction == nullptr)
    {
        return new_node;
    }

    // ノード生成 
    FGraphNodeCreator<UK2Node_CallFunction> FunctionNodeCreator(*TargetGraph);
    new_node = FunctionNodeCreator.CreateNode(false);
    if (new_node != nullptr)
    {
        // 関数を設定
        new_node->SetFromFunction(SetFunction);

        // 設定した関数に対応しピンを生成
        new_node->CreatePinsForFunctionCall(SetFunction);

        // 座標設定 
        new_node->NodePosX = NodePosX;
        new_node->NodePosY = NodePosY;

        // ノード生成完了 
        FunctionNodeCreator.Finalize();
    }

    return new_node;
}

ノードの生成に使用しているFGraphNodeCreatorEdGraph.hに定義されているノード生成のヘルパー構造体で、ノード生成はこの構造体を使用しています。
ここで作成しているUK2Node_CallFunctionは関数ノードのクラスです。
生成した時点では何も設定されていないので必要な情報を設定していきます。
SetFromFunctionで関数を設定して、CreatePinsForFunctionCallで設定した関数に応じたピンを作成します。
FGraphNodeCreatorのコメントにノードの生成が完了したらFinalizeを呼ぶように書いてあるので設定が完了したら呼んでいます。

ReplaceNodePinLinks(ピンの置換)

void UBlueprintNodeReplacer::ReplaceNodePinLinks(UEdGraphNode const* OldNode, UEdGraphNode const* NewNode)
{
    for (auto& new_node_pin : NewNode->Pins)
    {
        // 置き換え対象のピンがあったら 
        auto old_node_pin = OldNode->FindPin(new_node_pin->PinName);
        if (old_node_pin != nullptr)
        {
            // ピンの置き換え 
            old_node_pin->GetSchema()->MovePinLinks(*old_node_pin, *new_node_pin);
        }
    }
}

古いノードと新しく生成したノードを比べて同じもの(実行ピン等々)があればピンを接続します。
古いノードにある変数ピンが新しいノードにもあれば、そこから伸びているピンも接続してくれました。

f:id:Kubotti0131:20191212230427p:plain

※上記画像のように、実行ピンがあるノードから実行ピンのないノードに置換する場合その間は接続されないので注意してください。
この間を接続したい場合は置換対象ノードの実行ピンの情報を取得して、両サイドをつなげると実現できそうです。

ReplaceFunctionNode(関数ノードの置換)

bool UBlueprintNodeReplacer::ReplaceFunctionNode(UEdGraph* TargetGraph, UK2Node_CallFunction* OldNode, UFunction const * SetFunction)
{
    if (TargetGraph == nullptr || OldNode == nullptr || SetFunction == nullptr)
    {
        UE_LOG(LogTemp, Error, TEXT("Argument is null"));
        return false;
    }

    // ノード生成 
    UK2Node_CallFunction* new_node = CreateFunctionNode(TargetGraph, SetFunction, OldNode->NodePosX, OldNode->NodePosY);

    if (new_node == nullptr)
    {
        UE_LOG(LogTemp, Error, TEXT("Create failed is ReplaceNode!"));
        return false;
    }

    // 置換対象の古いノードのピンを新しく生成したノードのピンと置き換え 
    ReplaceNodePinLinks(OldNode, new_node);


    // 古いノードのピンのリンクを全て切る 
    OldNode->BreakAllNodeLinks();
    return true;
}

ここまで紹介した関数を使用して置換関数を作ります。

  1. 関数ノード生成
  2. 生成したノードのピンを接続
  3. 古いノードのピンのリンクをすべて切る

これで置換処理は完了です。
後はこれをBPに公開する関数内で対象のBPのすべてのGraphのノードに対して行います。

ReplaceBlueprintFunctionNode(BPで使用する置換処理)

bool UBlueprintNodeReplacer::ReplaceBlueprintFunctionNode(UBlueprint* TargetBP, FName const& TargetNodeName, UClass const* NewNodeOwnerClass, FName const& NewNodeName)
{
    if (TargetBP == nullptr || NewNodeOwnerClass == nullptr)
    {
        return false;
    }

    // 新しく作成するノードに設定する関数を取得
    UFunction* replace_function = NewNodeOwnerClass->FindFunctionByName(NewNodeName);
    if (replace_function == nullptr)
    {
        UE_LOG(LogTemp, Error, TEXT("%s is Unknown Node"), *NewNodeName.ToString());
        return false;
    }

    // 対象のBPの全グラフ取得 
    TArray<UEdGraph*> all_graphs;
    TargetBP->GetAllGraphs(all_graphs);
    if (all_graphs.Num() <= 0)
    {
        return false;
    }

    // 置換する関数ノード一覧 
    TArray<UK2Node_CallFunction*> replace_function_node_list;

    // 全てのグラフを参照して置換 
    bool replace_result = true;
    for (auto& graph : all_graphs)
    {
        // 変換対象のノードを探して置換 
        // ノード参照のために for を回している最中にノードを置換すると配列の内容が変ってエラーが出るので検索と置換を分けています 
        for (auto& target_node : graph->Nodes)
        {
            // キャストが成功するかで関数ノードかどうかを判定する
            UK2Node_CallFunction* target_function_node = Cast<UK2Node_CallFunction>(target_node);
            if (target_function_node == nullptr)
            {
                continue;
            }

            // 関数名を取得 
            FName target_function_node_name = target_function_node->FunctionReference.GetMemberName();
            if (target_function_node_name == TargetNodeName)
            {
                // 置換対象リストに追加 
                replace_function_node_list.Add(target_function_node);
            }
        }

        if (replace_function_node_list.Num() <= 0)
        {
            continue;
        }

        // 関数ノードの置換 
        for (auto& replace_function_node : replace_function_node_list)
        {
            replace_result = ReplaceFunctionNode(graph, replace_function_node, replace_function);
            if (replace_result == false)
            {
                break;
            }
        }

        // 古いノードを削除
        for (auto& delete_function_node : replace_function_node_list)
        {
            if (graph->RemoveNode(delete_function_node) == false)
            {
                UE_LOG(LogTemp, Error, TEXT("%s is Remove Failed"), *delete_function_node->GetFName().ToString());
                replace_result = false;
            }
        }



        // 置換配列を空にする 
        replace_function_node_list.Empty();
        if (replace_result == false)
        {
            break;
        }
    }

    // 置換でエラーがないか確認するためにコンパイル 
    FKismetEditorUtilities::CompileBlueprint(TargetBP);

    // 確認用にBPを開く 
    FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(TargetBP->FunctionGraphs[0]);

    return replace_result;
}

新規生成するノード名(関数名)からノードに設定する関数を取得しています。
関数が取得できない場合はノードの生成ができないためエラーログを出して終了します。

最初に作成したときはGraphから置換対象のノード検索しながら置換と削除を行っていたのですが、検索中に配列の内容が変わりエラーがでるのでfor文を分けています。

これで置換処理の準備ができたので作成したクラスを使用してBlutilityを作ります。

Blutilityを作る

参考

Blutilityに関してご存じない方はalwei様の以下の記事が参考になります。
いつもお世話になっております。

unrealengine.hatenablog.com

C++で作成したクラスからBlutilityを作る

C++で作成したクラスからBlutilityを初めに作成したプラグインのコンテンツフォルダに生成します。

f:id:Kubotti0131:20191212233936p:plain

EditorUtilityWidgetを選択。

f:id:Kubotti0131:20191212234014p:plain

C++ で作成したクラスを選択して作成する。

作成したBlutilityにイベントを追加

f:id:Kubotti0131:20191212235023p:plain

カスタムイベントを作成して、エディタで呼び出すにチェックを入れます。
作成したBP公開用の関数にメンバ変数ノードを設定。

これでBlutility側の実装は完了です。

完成したBlutiltiy

完成したものがこちら。

f:id:Kubotti0131:20191212235428p:plain

使い方の説明

完成したノード置換ツールの使い方を説明します。

置換に必要なデータを設定

f:id:Kubotti0131:20191213001005p:plain

  • TargetBlueprint:置換対象のノードが存在するBPを設定する。
  • TargetNodeName:置換対象のノード名を設定
  • CreateNodeClass:生成するノード(関数)を保持しているクラスを設定
    • BPのノードに置き換える場合はBPのクラスを設定 * C++のノードに置き換える場合はC++のクラス設定
  • CreateNodeName:生成するノード(関数)名

データの設定が終わったらReplaceFunctionNodeボタンをクリックして実行します。

実行してみる

今回テストとして置換するBPの中身がこちら。 f:id:Kubotti0131:20191213002404p:plain

Hoge1ノードPiyoに置換した結果がこちら。
ちゃんと実行ピンが繋がっていますね!
f:id:Kubotti0131:20191213002853p:plain

Hoge1ノードFuga3に置換した結果がこちら。
実行ピンと戻り値のピンが繋がっていますね!
f:id:Kubotti0131:20191213004146p:plain ※ピンの名前が同じでないと繋がらないので、引数や戻り値の変数名を変えたノードのピンも接続したい場合はピンの置換処理に別途対応が必要でした、、、

まとめ

とても長くなってしまいましたがこれでノードの置換ツールは完成です。
作成するときは何も情報がなかったためノード生成のフローをブレークポイントをつけて追っかけました。
エンジンのソースコードを見ることでBPのノードの仕組みを少し理解することができたのでとてもいい勉強になりました!

また、EditorUtilityWidget や Blutility のおかげでエディタ拡張がお手軽にできるようになり、
さらにC++を使うことでUE4が行っていることはある程度できることが解ったのでツール作成の可能性が広がりました!
もし、EditorUtilityWidget や Blutility に少しでも興味を持った方はエンジンのソースを追いながら便利ツールを作ってみてはいかがでしょうか?

今後も便利なツールの情報が共有されてよりよい開発環境が構築できることを楽しみにしております!
以上、拙い文章だったと思いますがここまで読んでいただいてありがとうございます。

明日は goolee07 さんのAI周りの何かについてです!
とても楽しみですね!!!