[UnrealC++]Latentノードの作り方
今回は、Latentノードの作り方を紹介します。
図2は、Heavy Eventが終了するまでPrint Stringの実行を待機するという疑似的なLatentノードの処理を行っています。「Latentモデル」とコメントされている部分が、Heavy Eventを擬似Latentノードにするための実装です。
はじめに、FPendingLatentActionクラスを継承するために、LatentActions.hをMyLatentActor.generated.hよりも上に記述します。
次に、FPendingLatentActionを継承したFMyLatentActionを定義します。色々なメンバが宣言されていますが、重要なのはIsRunningLatentActionというフラグ変数リファレンスのみで、それ以外はLatentActionを定義するための定型的な記述と捉えるのが簡単です(私も、全部を理解しているわけではないので……)。
Latentノードとは?
Latentノードとは、図1のような、ノードの右上に時計のアイコンが付いているものを指します。基本的にこれらのノードは「処理が終了するまでの間、後続の処理の実行を待機する」という動作を1ノードで実現するためのもので、処理の可読性向上に役立ちます。
本記事では、処理の完了に時間がかかるユーザー定義の処理を、Latentノードとして1つにまとめることを目標とします。
![]() |
図1 Latentノード |
Latentノードを作る前に
※本項はLatentノードの作り方とは関係のない話なので、手っ取り早く実装法が知りたい方は、次の項を読んで下さい。
Latentノードは処理の可読性を上げますが、一方で何でもかんでもLatentノードにすればよいというものでもありません。Latentノードを使いすぎると、どこで処理が止まっているのか・いつ処理が再開されるかといった把握が難しくなりますし、そもそも大前提としてC++が必要なためBP Onlyほど気軽には作れません。
そこで個人的に有用と考えている、BP Onlyで実現できるLatentモデルの代替を紹介します。
![]() |
図2 非LatentノードによるLatentモデル |
図2は、Heavy Eventが終了するまでPrint Stringの実行を待機するという疑似的なLatentノードの処理を行っています。「Latentモデル」とコメントされている部分が、Heavy Eventを擬似Latentノードにするための実装です。
これを見てわかるように、Heavy EventのLatentノードを作ったとしても、節約できるノードはコメント部の数ノードだけです。よって、Latentノードを作るときは、作る労力がこの代替に見合うかを考える必要があります。
とはいえ、実際にはLatentノードを作る度に待機フラグ変数を追加し、Branch-Delayで待機モデルを作るのはスマートではありません。なにより、Heavy Eventは後続の処理が待機される前提の実装をするのに、それが図1のアイコンのような形で明示されないのは非常に気持ちが悪いです。
Latentノードの作り方
次の2ステップで実装します。
- FPendingLatentActionを継承して、Latentノードが終了したかどうかを監視するためのクラスを定義する
- UFUNCTIONにLatentノード化するための記述を加えた関数を作成する
まず、Latentノードを作りたいクラス(今回はMyLatentActorとします)に、FPendingLatentActionを継承したクラスを定義します。MyLatentActor.hを次のようにしました。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
#include "CoreMinimal.h" | |
#include "GameFramework/Actor.h" | |
#include "LatentActions.h" // Added | |
#include "MyLatentActor.generated.h" | |
// Added | |
class FMyLatentAction : public FPendingLatentAction | |
{ | |
public: | |
const bool& IsRunningLatentAction; | |
FName ExecutionFunction; | |
int32 OutputLink; | |
FWeakObjectPtr CallbackTarget; | |
FMyLatentAction(const bool& isRunning, const FLatentActionInfo& LatentInfo) | |
: IsRunningLatentAction(isRunning) | |
, ExecutionFunction(LatentInfo.ExecutionFunction) | |
, OutputLink(LatentInfo.Linkage) | |
, CallbackTarget(LatentInfo.CallbackTarget) | |
{ | |
} | |
virtual void UpdateOperation(FLatentResponse& Response) override | |
{ | |
Response.FinishAndTriggerIf(!IsRunningLatentAction, ExecutionFunction, OutputLink, CallbackTarget); | |
} | |
}; |
次に、FPendingLatentActionを継承したFMyLatentActionを定義します。色々なメンバが宣言されていますが、重要なのはIsRunningLatentActionというフラグ変数リファレンスのみで、それ以外はLatentActionを定義するための定型的な記述と捉えるのが簡単です(私も、全部を理解しているわけではないので……)。
コンストラクタは、指定のメンバを初期化しているだけなので説明を省きます。
UpdateOperationですが、この関数はLatentノード実行後に、Latentノードを終了させるかどうかを監視するために毎フレーム呼ばれる関数です。基本的には、指定条件が満たされた時にLatentノードに終了を通知するResponse.FinishAndTriggerIfを実行するためにあります。また、FinishAndTriggerIfも含めて、FLatentResponseにはLatentノードの待機状態を扱うための次のような関数が用意されています。
これを踏まえて上記のコードを見てみます。FinishAndTriggerIfのConditionには、外部の待機フラグ変数リファレンスIsRunningLatentActionが与えられています。これは、このLatentActionを生成時に指定した待機フラグ変数がFalseになったとき、Latentノードが終了することを意味しています。
次に、定義したMyLatentActionを用いてLatentノードを実装します。
MyLatentActor.hに次のような実装を追加します。
1つずつメンバを見ていきます。
最後に、Blueprintで実際にLatentノードを実装してテストしてみます。
MyLatentActorを継承したBP_MyLatentActorを作成し、次のように実装します。
このように実装したBP_MyLatentActorをレベルに配置して実行すると、5秒後にHelloを出力され、HeavyEventが処理をせき止めていることを確認できます。これで、C++を用いてLatentノードを作成することができました。
UpdateOperationですが、この関数はLatentノード実行後に、Latentノードを終了させるかどうかを監視するために毎フレーム呼ばれる関数です。基本的には、指定条件が満たされた時にLatentノードに終了を通知するResponse.FinishAndTriggerIfを実行するためにあります。また、FinishAndTriggerIfも含めて、FLatentResponseにはLatentノードの待機状態を扱うための次のような関数が用意されています。
- float ElapsedTime() const
フレーム間のDelta Timeを取得します。
- FLatentResponse& DoneIf(bool Condition)
ConditionがTrueのとき、このLatentActionをLatentActionManagerから取り除きます。取り除かれた後は、UpdateOperationが実行されなくなります。もしDoneIfの直後にTriggerLinkを実行しなかった場合、Latentノードは後続の処理を実行せずに強制的に終了します。
- FLatentResponse& TriggerLink(FName ExecutionFunction, int32 LinkID, FWeakObjectPtr InCallbackTarget)
後続の処理へ実行ピンが繋がり、後続の処理が強制的に実行されます。
- FLatentResponse& FinishAndTriggerIf(bool Condition, FName ExecutionFunction, int32 LinkID, FWeakObjectPtr InCallbackTarget)
DoneIfとTriggerLinkを合わせた関数です。すなわち、ConditionがTrueのとき、LatentActionを停止させ、一度のみ後続の処理を実行します。
次に、定義したMyLatentActionを用いてLatentノードを実装します。
MyLatentActor.hに次のような実装を追加します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
UCLASS() | |
class BLOGTEST416_API AMyLatentActor : public AActor | |
{ | |
GENERATED_BODY() | |
public: | |
UPROPERTY(BlueprintReadWrite) | |
bool IsRunningHeavyEvent = false; | |
UFUNCTION(BlueprintImplementableEvent, Category = LatentTest, meta = (BlueprintInternalUseOnly, BlueprintProtected)) | |
void HeavyEventInternal(); | |
UFUNCTION(BlueprintCallable, Category = LatentTest, meta = (Latent, LatentInfo = "latentInfo_", HidePin = "worldContextObject_", DefaultToSelf = "worldContextObject_")) | |
void HeavyEvent(const UObject* worldContextObject_, FLatentActionInfo latentInfo_) | |
{ | |
if (UWorld* world = GEngine->GetWorldFromContextObject(worldContextObject_)) | |
{ | |
FLatentActionManager& lam = world->GetLatentActionManager(); | |
if (lam.FindExistingAction<FMyLatentAction>(latentInfo_.CallbackTarget, latentInfo_.UUID) == nullptr) | |
{ | |
FMyLatentAction* action = new FMyLatentAction(IsRunningHeavyEvent, latentInfo_); | |
lam.AddNewAction(latentInfo_.CallbackTarget, latentInfo_.UUID, action); | |
HeavyEventInternal(); | |
} | |
} | |
} | |
}; |
- IsRunningHeavyEvent
MyLatetnActionに与える、Latentノードを終了させるための待機フラグ変数です。この変数がBlueprint上でFalseに変更されると、Latentノードは終了します。
- void HeavyEventInternal()
HeavyEventの本体をBlueprint上で実装するための内部関数です。これは後々Blueprintで実装します。
- void HeavyEvent(const UObject* worldContextObject_, FLatentActionInfo latentInfo_)
Latentノード化されたHeavyEventです。UFUNCTIONにLatentノードであることを指定するメタ指定子が記述されていることに注意してください。関数は、worldContextObject_からWorldオブジェクトを取得し、Worldオブジェクト経由でFLatentActionManagerを取得します。その後、引数として与えられたlatentInfo_を用いてMyLatentActionを生成し、LatentActionManagerに追加しています。MyLatentActionが追加されると、そのMyLatentActionはUpdateOperationが実行されるようになり、Latentノードの状態が監視されるようになります。追加後は、HeavyEventInternal関数を呼び出してこの関数は終了します。なお、この関数が終了しても後続の処理は実行されません。
最後に、Blueprintで実際にLatentノードを実装してテストしてみます。
MyLatentActorを継承したBP_MyLatentActorを作成し、次のように実装します。
![]() |
図3 LatenNode化されたHeavy Event |
このように実装したBP_MyLatentActorをレベルに配置して実行すると、5秒後にHelloを出力され、HeavyEventが処理をせき止めていることを確認できます。これで、C++を用いてLatentノードを作成することができました。
おわりに
今回の例では、図2との対称性を考えて実装したため、あまり意味のあるものに見えないかもしれません。ただ、このような形式のモデルは実装が単純であり、新しいLatentノードを追加したいとき、このモデルをほぼコピペすることで実装出来る点は非常に魅力的です。
しかし実際には問題も多く、汎用的なLatentモデルとして扱うことは厳しいです。例えば、次のような問題が挙げられます。
しかし実際には問題も多く、汎用的なLatentモデルとして扱うことは厳しいです。例えば、次のような問題が挙げられます。
- Latentノードを作る度に関数、フラグ変数の定義が増え続ける
- 外部クラスからこのイベントの定義を参照しようとしたとき、定義に直接ジャンプできない(本体と内部関数は別のため)
- 関数をRPC化しようとしたとき、内部関数はRPC化できない(BlueprintImplementableEventとRPCのための指定子は混在できないため)
今回はLatentノードの基本的な実装方法のみを紹介しましたが、次回は、これらの問題を解決したより実用的なLatentノード実装モデルを紹介したいと思います。
この記事は次のバージョンで作成されました。
Unreal Editor(4.16.1-3466753+++UE4+Release-4.16)
Microsoft Visual Studio Community 2017 Version 15.1 (26403.7)
コメント
コメントを投稿