アンリアる! C++入門編 ~対話形式で学ぶUnreal Engine~
BOOTHでUnreal Engine C++入門書を販売していますUnreal Engine上でC++を使って開発するために必要となる基礎知識を一冊の本にまとめた本です。 対話形式によるわかりやすい説明を目指しました。無料の試し読みも可能ですので、ぜひ読んでみてください!
[UE5] K2Nodeを継承してブループリントノードを自作する(2) 実行ピン編

本記事では、実行ピン付きのブループリントノードを作る方法について説明します。
基本編の内容を理解していることを前提に説明するため、必要に応じて 基本編の記事 を確認してください。

ソースコード一式

本記事で説明するソースコード一式は、GitHubで公開しています。

https://github.com/colory-games/techblog-sample-projects/tree/main/nutti/20220626

ソースコードを利用して動作確認する場合は、次の手順に従ってください。

  1. ディレクトリ MyCompareInt を、Unreal Engineのプロジェクトディレクトリにある Plugins に配置する
  2. .uproject ファイルを右クリックし、Visual Studio向けのプロジェクトファイルを作成する
  3. プロジェクトファイルを開き、プロジェクトをビルドする
  4. .uproject ファイルからUnreal Engineを起動し、プラグイン「MyCompareInt」を有効化する

作成するブループリントノードの仕様

本記事で作成するブループリントノード「MyCompareInt」の仕様を次に示します。

  • 入力として2つの整数「A」「B」を受け取る
  • 入力「A」「B」の大小関係によって実行する出力側の実行ピンを変更する
    • 実行ピン「Greater」: A>Bのときに実行する
    • 実行ピン「Equal」: A=Bのときに実行する
    • 実行ピン「Less」: A<Bのときに実行する

この仕様は、ブループリントで提供されているマクロ「Compare Int」と同様です。
必要に応じてマクロ「Compare Int」の実装も確認してみてください。

モジュールの作成

基本編で説明した内容に従ってモジュールを作成します。
具体的な作成方法については、基本編の記事 を参考にしてください。

ブループリントノードの作成

ブループリントノードは、次の手順で作成します。

  1. クラス UK2Node の継承
  2. 基本情報の設定
  3. ピンの追加
  4. 内部ピンの追加
  5. 実行ピンの処理定義

なお、手順1と手順2については 基本編の記事 とほぼ同じであるため、ここでは手順3から説明します。

3. ピンの追加

作成するノードの仕様に従って、次のピンを追加します。

ピン名 入出力方向 意味
入力 ノードの実行を起動
A 入力 整数の入力
B 入力 整数の入力
Greater 出力 A>Bのときにピンの先につながったノードを実行
Equal 出力 A=Bのときにピンの先につながったノードを実行
Less 出力 A<Bのときにピンの先につながったノードを実行

ピンの追加処理は、メンバ関数 AllocateDefaultPins に実装します。

CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Int, APinName);
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Int, BPinName);
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, GreaterPinName);
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, EqualPinName);
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, LessPinName);

実行ピンを追加する場合、ピンの型を示す第2引数には UEdGraphSchema_K2::PC_Exec を指定します。
ノード実行のトリガとなる入力側の実行ピンを追加する場合、第3引数には UEdGraphSchema_K2::PN_Execute を指定するのが一般的です。

出力側にも3つの実行ピンを追加します。
ここでは、名前が「Greater」「Equal」「Less」の実行ピンを追加します。

4. 内部ピンの追加

今回作成するノード「MyCompareInt」は、入力に指定された整数「A」と「B」の値に応じて実行する出力側のピンを決める必要があります。
したがって、次の処理を実現する必要があります。

  • 「A>B」のときに、実行ピン「Greater」の先につながるノードを実行する
  • 「A=B」のときに、実行ピン「Equal」の先につながるノードを実行する
  • 「A<B」のときに、実行ピン「Less」の先につながるノードを実行する

これをブループリントで実現すると、次のようになります。

ブループリントでの実装1

しかしここでは、実装上の制約を考慮して次のような形で実現します。

  1. 入力「A」と「B」をライブラリ関数 UKismetMathLibrary::GreaterEqual_IntInt に渡し、結果が False の場合は実行ピン「Less」の先につながるノードを実行する
  2. 1の結果が True の場合、入力「A」と「B」をライブラリ関数 UKismetMathLibrary::LessEqual_IntInt に渡し、結果が False の場合は実行ピン「Greater」の先につながるノードを実行する
  3. 2の結果が True の場合、実行ピン「Equal」の先につながるノードを実行する

ブループリントで実現すると次のようになります。

ブループリントでの実装2

これをノードで実現するためには、次の2つを実現する方法を理解する必要があります。

  • ライブラリ関数の呼び出しに必要な、内部ピンを追加する
  • 追加したライブラリ関数の出力結果を利用し、実行する実行ピンを決定する

最初に、ライブラリ関数の呼び出しに必要な内部ピンを追加する処理について説明します。
他のピンを追加する場合と同様、メンバ関数 CreatePin を利用して内部ピンを追加します。

UEdGraphPin* FunctionPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UKismetMathLibrary::StaticClass(), TEXT("GreaterEqual_IntInt"));
FunctionPin->bDefaultValueIsReadOnly = true;
FunctionPin->bNotConnectable = true;
FunctionPin->bHidden = true;

第2引数には、UEdGraphSchema_K2::PC_Object を指定します。
第3引数にはブループリント関数ライブラリ、第4引数にはライブラリ関数の名前を指定します。

ライブラリ関数 CreatePin の戻り値を利用し、読み取り専用の内部ピンであることを示すために各種設定を適用します。
ライブラリ関数を実行するピンの作成が完了しましたが、この状態では実行するライブラリ関数が設定されていません。

実行するライブラリ関数を内部ピンに設定する処理を次に示します。

UFunction* Function = FindUField(UKismetMathLibrary::StaticClass(), TEXT("GreaterEqual_IntInt"));
if (Function != nullptr && Function->HasAllFunctionFlags(FUNC_Static))
{
    UBlueprint* Blueprint = GetBlueprint();
    if (Blueprint != nullptr)
    {
        UClass* FunctionOwnerClass = Function->GetOuterUClass();
        if (!Blueprint->SkeletonGeneratedClass->IsChildOf(FunctionOwnerClass))
        {
            FunctionPin->DefaultObject = FunctionOwnerClass->GetDefaultObject();
        }
    }
}

関数 FindUField を呼び出してライブラリ関数 UKismetMathLibrary::GreaterEqual_IntIntUFunction を取得します。
そして、作成した内部ピン FunctionPin のメンバ変数 DefaultObject に取得した UFunction を設定します。
これによって、内部ピンはライブラリ関数 UKismetMathLibrary::GreaterEqual_IntInt を実行できるようになります。

ここでは説明を省きますが、同様にしてライブラリ関数 UKismetMathLibrary::LessEqual_IntInt を実行するための内部ピンを作成します。

5. 実行ピンの処理定義

ライブラリ関数の出力結果を利用し、実行する実行ピンを決定する処理を実装します。
基本編では計算グラフをメンバ関数 ExpandNode を用いて定義しましたが、メンバ関数 ExpandNode は特定の条件に基づいて実行先のピンを選択するという処理を実現できません。
これを実現するためには、クラス FNodeHandlingFunctor を継承したクラスを定義する必要があります。
今回はクラス FKCHandler_MyCompareInt として定義しています。

class FKCHandler_MyCompareInt : public FNodeHandlingFunctor
{
    TMap BoolTermMap;
public:
    FKCHandler_MyCompareInt(FKismetCompilerContext& InCompilerContext) : FNodeHandlingFunctor(InCompilerContext)
    {
    }

    virtual void RegisterNets(FKismetFunctionContext& Context, UEdGraphNode* Node) override
    {
        // (省略)
    }

    virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node) override
    {
        // (省略)
    }
};

ここからは、クラス FKCHandler_MyCompareInt の定義について詳しく説明します。

変数の作成

メンバ関数 RegisterNets には、実行したライブラリ関数の結果を格納する一時変数を作成する処理を定義します。
ここで作成した一時変数は、のちほど利用します。

virtual void RegisterNets(FKismetFunctionContext& Context, UEdGraphNode* Node) override
{
    FNodeHandlingFunctor::RegisterNets(Context, Node);

    {
        // ライブラリ関数「UKismetMathLibrary::GreaterEqual_IntInt」の実行結果を格納する
        // Boolean型の一時変数を作成する
        FBPTerminal* BoolTerm = Context.CreateLocalTerminal();
        BoolTerm->Type.PinCategory = UEdGraphSchema_K2::PC_Boolean;
        BoolTerm->Source = Node;
        BoolTerm->Name = Context.NetNameMap->MakeValidName(Node, TEXT("IsGreaterEqual"));
        BoolTermMap.Add(TEXT("IsGreaterEqual"), BoolTerm);
    }
}

メンバ関数 RegisterNets はオーバーライドして定義したクラスであり、最初に親クラスの RegisterNets を呼び出す必要があります。
親クラスの RegisterNets を呼び出したあとに、引数 Context のメンバ関数 CreateLocalTerminal を呼び出してBoolean型の変数を作成します。
これを2回繰り返し、ライブラリ関数 UKismetMathLibrary::GreaterEqual_IntIntUKismetMathLibrary::LessEqual_IntInt の実行結果を格納する変数 IsGreaterEqualIsLessEqual を作成します。

作成した変数はブループリントのコンパイル処理で利用するため、メンバ変数 BoolTermMap に登録しておきます。

ブループリントのコンパイル処理

メンバ関数 Compile にはブループリントのコンパイル時に呼ばれる処理を定義し、ノードで実現する処理に対応したブループリントのステートメント FKismetCompiledStatement を追加します。

ここで定義したメンバ関数 Compile は、Compile Functions のフェーズで呼ばれます。
ブループリントのコンパイルに関する具体的な処理については、公式のドキュメント Blueprint Compiler Overview が参考になります。

ブループリントのステートメントタイプは、EKismetCompiledStatementType から確認できます。
今回利用するステートメントタイプは次の3つです。

ステートメントタイプ 意味
KCST_CallFunction 関数(UFunction)を呼び出す
KCST_GotoIfNot 指定した条件が False の場合に特定の実行ピンに実行制御を移す
KCST_UnconditionalGoto 無条件で特定の実行ピンに実行制御を移す

ステートメントは、引数 Context のメンバ関数 AppendStatementForNode を呼び出すことによって作成できます。
戻り値は構造体 FBlueprintCompiledStatement です。
メンバ変数 Type にステートメントタイプを指定し、LHSRHS にはそれぞれステートメントに渡す変数を指定します。
変数の意味はステートメントタイプによって異なるため、具体的な意味については EKismetCompiledStatementType を参照してください。

ここからは、メンバ関数 Compile に定義する処理について説明します。

コンパイルに必要な変数の取得

メンバ関数 Compile は、最初にコンパイル時に必要な情報を取得します。

virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node) override
{
    UK2Node_MyCompareInt* MyCompareIntNode = CastChecked(Node);

    // 出力側の実行ピンを取得する
    UEdGraphPin* GreaterPin = MyCompareIntNode->FindPin(GreaterPinName);
    UEdGraphPin* EqualPin = MyCompareIntNode->FindPin(EqualPinName);
    UEdGraphPin* LessPin = MyCompareIntNode->FindPin(LessPinName);

    // 一時変数を取得する
    FBPTerminal* IsGreaterEqualTerm = BoolTermMap.FindRef(TEXT("IsGreaterEqual"));
    FBPTerminal* IsLessEqualTerm = BoolTermMap.FindRef(TEXT("IsLessEqual"));

    // 入力ピン「A」の変数を取得する
    UEdGraphPin* APin = MyCompareIntNode->FindPin(APinName);
    UEdGraphPin* ANet = FEdGraphUtilities::GetNetFromPin(APin);
    FBPTerminal* ATerm = Context.NetMap.FindRef(ANet);

    // 入力ピン「B」の変数を取得する
    UEdGraphPin* BPin = MyCompareIntNode->FindPin(BPinName);
    UEdGraphPin* BNet = FEdGraphUtilities::GetNetFromPin(BPin);
    FBPTerminal* BTerm = Context.NetMap.FindRef(BNet);
}

ステートメントを作成するにあたって、変数に相当するのが構造体 FBPTerminal です。
実行ピンを表すクラス UEdGraphPin ではないことに注意しましょう。
ここでは、次に示す変数を取得しています。

変数名 意味
IsGreaterEqualTerm ライブラリ関数 UKismetMathLibrary::GreaterEqual_IntInt の結果を保存
IsLessEqualTerm ライブラリ関数 UKismetMathLibrary::LessEqual_IntInt の結果を保存
ATerm 入力ピン「A」
BTerm 入力ピン「B」

ブループリントのステートメント作成

ブループリントのステートメントを作成する処理について説明します。
先ほど紹介したように、指定した条件が True の場合に特定の実行ピンに実行制御を移すというステートメントタイプがありません。
ここで、手順4で実装上の制約を考慮し実現方法を変更したことが役立ちます。

改めて実現する処理を示します。

  1. 入力「A」と「B」をライブラリ関数 UKismetMathLibrary::GreaterEqual_IntInt に渡し、結果が False の場合は実行ピン「Less」の先につながるノードを実行する
  2. 1の結果が True の場合、入力「A」と「B」をライブラリ関数 UKismetMathLibrary::LessEqual_IntInt に渡し、結果が False の場合は実行ピン「Greater」の先につながるノードを実行する
  3. 2の結果が True の場合、実行ピン「Equal」の先につながるノードを実行する

参考のため、C++で実現する場合の疑似コードを示します。

IsGreaterEqualTerm = UKismetMathLibrary::GreaterEqual_IntInt(ATerm, BTerm);
if (!IsGreaterEqualTerm) {
    goto LessPin;
}
IsLessEqualTerm = UKismetMathLibrary::LessEqual_IntInt(ATerm, BTerm);
if (!IsLessEqualTerm) {
    goto GreaterPin;
}
goto EqualPin;

これをブループリントのステートメントに書き換えると次のようになります。

KCST_CallFunction IsGreaterEqualTerm [ATerm, BTerm]
KCST_GotoIfNot IsGreaterEqualTerm -> LessPin
KCST_CallFunction IsLessEqualTerm [ATerm, BTerm]
KCST_GotoIfNot IsLessEqualTerm -> GreaterPin
KCST_UnconditionalGoto -> EqualPin

この一連のステートメントを作成する処理をメンバ関数 Compile に記述していきます。

virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node) override
{
    // (省略)

    {
        // ライブラリ関数「UKismetMathLibrary::GreaterEqual_IntInt」のUFunctionを取得する
        UEdGraphPin* FunctionPin = MyCompareIntNode->FindPin(TEXT("GreaterEqual_IntInt"));
        FBPTerminal* FunctionContext = Context.NetMap.FindRef(FunctionPin);
        UClass* FunctionClass = Cast(FunctionPin->PinType.PinSubCategoryObject.Get());
        UFunction* FunctionPtr = FindUField(FunctionClass, FunctionPin->PinName);

        // ライブラリ関数を呼び出すステートメントを作成する
        FBlueprintCompiledStatement& CallFuncStatement = Context.AppendStatementForNode(MyCompareIntNode);
        CallFuncStatement.Type = KCST_CallFunction;
        CallFuncStatement.FunctionToCall = FunctionPtr;
        CallFuncStatement.FunctionContext = FunctionContext;
        CallFuncStatement.bIsParentContext = false;
        CallFuncStatement.LHS = IsGreaterEqualTerm;
        CallFuncStatement.RHS.Add(ATerm);
        CallFuncStatement.RHS.Add(BTerm);

        // ライブラリ関数の呼び出し結果ががFalseのときに、実行ピン「LessPin」に実行制御が移るステートメントを作成する
        FBlueprintCompiledStatement& GotoStatement = Context.AppendStatementForNode(MyCompareIntNode);
        GotoStatement.Type = KCST_GotoIfNot;
        GotoStatement.LHS = IsGreaterEqualTerm;
        Context.GotoFixupRequestMap.Add(&GotoStatement, LessPin);
    }

    {
        // (省略)

        // ライブラリ関数の呼び出し結果がFalseのときに、実行ピン「GreaterPin」に実行制御が移るステートメントを作成する
        FBlueprintCompiledStatement& GotoStatement = Context.AppendStatementForNode(MyCompareIntNode);
        GotoStatement.Type = KCST_GotoIfNot;
        GotoStatement.LHS = IsLessEqualTerm;
        Context.GotoFixupRequestMap.Add(&GotoStatement, GreaterPin);
    }

    // いずれの条件も満たさない場合は、無条件で実行ピン「EqualPin」に実行制御が移るステートメントを作成する
    GenerateSimpleThenGoto(Context, *MyCompareIntNode, EqualPin);
}

最初に、ブループリントのステートメント作成時に使用するライブラリ関数 UKismetMathLibrary::GreaterEqual_IntIntUFunction を取得します。
これは、手順4で作成した内部ピンに設定した情報から取得できます。

次に Context のメンバ関数 AppendStatementForNode を呼び出し、ライブラリ関数を呼び出すステートメント KCST_CallFunction を作成します。
構造体 FBlueprintCompiledStatement のメンバ変数 Type には、ライブラリ関数を呼び出すステートメントであることを明示するために KCST_CallFunction を設定します。
メンバ変数 FunctionToCall には、このステートメントで呼び出すライブラリ関数の UFunction を設定します。
ここでは、事前に取得しておいた UFunction を指定すればよいです。
メンバ変数 LHS には、ライブラリ関数の戻り値を保存する変数 IsGreaterEqualTerm を設定します。
メンバ変数 RHS には、ライブラリ関数の引数に渡す値を引数順に設定します。
今回の場合は、ATermBTerm を引数として設定します。

続いて作成するのは、指定した条件が False の場合に特定の実行ピンに実行制御を移すステートメント KCST_GotoIfNot です。
メンバ変数 LHSIsGreaterEqualTerm を指定し、IsGreaterEqualTermFalse のとき(「A<B」のとき)に実行制御が移るようにします。
実行制御を移す先は実行ピン LessPin であるため、Context.GotoFixupRequestMap.Add を呼び出して実行制御を移す先を登録します。

同様にして、実行ピン GreaterPin に実行制御を移す処理を作成します。

最後に関数 GenerateSimpleThenGoto を呼び出し、いずれの条件にも当てはまらなかったとき(「A=B」のとき)に実行ピン EqualPin へ実行制御を移すステートメントを作成します。
関数 GenerateSimpleThenGoto は内部でステートメント KCST_UnconditionalGoto を作成し、Context.GotoFixupRequestMap.Add を呼び出して実行制御を移す先を登録する便利関数です。

実行ピンの処理を登録

実行ピンの処理を定義したクラス FKCHandler_MyCompareInt は、ノードのクラス UK2Node_MyCompareInt と関連付ける必要があります。
メンバ関数 CreateNodeHandler をオーバーライドし、クラス FKCHandler_MyCompareInt を戻り値として返すように処理を定義することで関連づけが可能です。

FNodeHandlingFunctor* UK2Node_MyCompareInt::CreateNodeHandler(class FKismetCompilerContext& CompilerContext) const
{
    return new FKCHandler_MyCompareInt(CompilerContext);
}

作成したノードの利用

作成したノードを利用してみましょう。
Actorを作成してブループリントエディタを開き、右クリックでメニューを表示して検索ボックスに「mycompareint」を入力します。

ノード「MyCompareInt」の検索

カテゴリ「Flow Control」に追加されているノード「MyComponentInt」を選択して配置します。

Actorの作成

作成したActorをレベルに配置して実行すると、ノードの入力値によって実行されるピンが変化することが確認できます。

ノード「MyCompareInt」の実行

ここでは実行ピン「Greater」を実行したときのものを示していますが、入力ピン「A」「B」の値をいろいろ変えてみてその結果を確認してみてください。

参考文献