[UnrealC++]任意のイベントをLatentノード化する

前回、Latentノードの作成方法と単純な実装モデルについて紹介しました。
今回は、前回の問題点を改善した実用的なLatentノード実装モデルを紹介します。具体的には、図1のような、特定のシグネチャを持つイベントをLatentノード化する(イベントが終了するまで待機する)WaitUntilFinishedの実装・使用方法について紹介します。
図1 WaitUntilFinishedを用いたLatentノード実装モデル

WaitUntilFinishedノードの作り方と使用方法のみを知りたい方は、「WaitUntilFinished Latentノードを使用する」項以降を参照してください。

イベント終了通知オブジェクトを作る

前回は、Latentノード化したイベントが実行しているかどうかはBoolean変数によって管理していました。しかし、Boolean変数で管理する場合はUPROPERTYをつけたメンバ変数を新たに宣言する必要があるため、Latentノード化したいイベントを追加する度に変数が増えてしまうという問題がありました。
そこでLatentノードが呼ばれる度に、終了を通知する機能を持つオブジェクトを動的に生成することで、過剰な変数宣言を防ぎます。最も単純なアイデアはnew演算子で動的にBoolean変数を割り当てる方法が考えられますが、これはBlueprintで対応できないため避けます。次のアイデアとしては、UObject継承クラスをNewObjectで生成する方法が考えられますが、これもGCの対応が面倒なので避けます。
様々なアイデアを試した結果、より良い結果を得られたのはAActor継承クラスをレベルにSpawnする方法です。この方法であれば、Blueprintとの相性もよく、DestoryActorによって明示的に破棄できるためGCの問題もありません。
LatentWaiter.hを次のように実装しました。

#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "LatentWaiter.generated.h"
UCLASS()
class BLOGTEST416_API ALatenterActor : public AActor
{
GENERATED_BODY()
public:
ALatenterActor();
UFUNCTION(BlueprintCallable, Category = LatentWaiter)
void FinishWait();
};
view raw LatentWaiter.h hosted with ❤ by GitHub
LatentWaiter.cppを次のように実装しました。

#include "LatentWaiter.h"
ALatenterActor::ALatenterActor()
{
PrimaryActorTick.bCanEverTick = false;
SetActorHiddenInGame(true);
}
void ALatenterActor::FinishWait()
{
Destroy();
}
ALatenterActorクラスは、そのインスタンスがレベルに存在する(DestroyActorが呼び出されていない、またIsValidがTrueになる)とき、イベントが継続していることを示し、レベルから破棄されたとき、イベントが終了していることを示すものとします。すなわち、このアクターをDestroyActor(もしくはDestroy)することによってレベルから破棄することが、イベント終了通知を送ることと同じ意味を持つものとします。よって、FinishWait関数は単にDestroyを呼び出します。これは終了通知を明示するためだけにあり、実際にはDestoryActorで直接破棄しても違いはありません。

イベント終了通知を受け取るLatentActionを作る

先程作った終了通知を監視するLatentActionを作ります。
LatentWaiter.hに次のような実装を追加しました。

#pragma once
#include "CoreMinimal.h"
#include "LatentActions.h"
#include "LatentWaiter.generated.h"
class FImplementableLatentAction : public FPendingLatentAction
{
public:
class ALatenterActor* Latenter;
FName ExecutionFunction;
int32 OutputLink;
FWeakObjectPtr CallbackTarget;
FImplementableLatentAction(UWorld* world_, const FLatentActionInfo& LatentInfo);
virtual void UpdateOperation(FLatentResponse& Response) override;
};
view raw LatentWaiter.h hosted with ❤ by GitHub
LatentWaiter.cppに次のような実装を追加しました。

#include "LatentWaiter.h"
FImplementableLatentAction::FImplementableLatentAction(UWorld* world_, const FLatentActionInfo& LatentInfo)
: Latenter(world_->SpawnActor<ALatenterActor>())
, ExecutionFunction(LatentInfo.ExecutionFunction)
, OutputLink(LatentInfo.Linkage)
, CallbackTarget(LatentInfo.CallbackTarget)
{
}
void FImplementableLatentAction::UpdateOperation(FLatentResponse& Response)
{
Response.FinishAndTriggerIf(!(Latenter->IsValidLowLevel() && !Latenter->IsActorBeingDestroyed()), ExecutionFunction, OutputLink, CallbackTarget);
}
まず、コンストラクタでは終了通知を受け取るためのALatenterActorをスポーンします。ここでスポーンするのは、1つのLatentActionに対し1つのALatenterActorが対応することを保証するためでもあります。
次に、UpdateOperationを実装します。監視するALatenterActorオブジェクトが破棄されていた場合に終了が通知されたとみなすので、ここではLatentWaiter.cppの13行目のような条件にしました。

指定イベントが終了を通知するまで待機するLatentノードを作る

先程作ったFImplementableLatentActionを用いて、イベント終了通知のためのシグネチャを持つイベントが、終了を通知するまで待機するLatentノードを作ります。
LatentWaiter.hに次のような実装を追加しました。

#pragma once
#include "CoreMinimal.h"
#include "LatentActions.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "LatentWaiter.generated.h"
DECLARE_DYNAMIC_DELEGATE_OneParam(FWaitUntilFinishedDispatcher, ALatenterActor*, latenter);
UCLASS()
class BLOGTEST416_API UBPFL_Latenter : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = Latenter, meta = (Latent, LatentInfo = "latentInfo_", HidePin = "worldContextObject_", DefaultToSelf = "worldContextObject_"))
static void WaitUntilFinished(const UObject* worldContextObject_, FLatentActionInfo latentInfo_, const FWaitUntilFinishedDispatcher onEventDispatcher);
};
view raw LatentWaiter.h hosted with ❤ by GitHub
LatentWaiter.cppに次のような実装を追加しました。

#include "LatentWaiter.h"
void UBPFL_Latenter::WaitUntilFinished(const UObject* worldContextObject_, FLatentActionInfo latentInfo_, const FWaitUntilFinishedDispatcher onEventDispatcher)
{
if (UWorld* world = GEngine->GetWorldFromContextObject(worldContextObject_))
{
FLatentActionManager& lam = world->GetLatentActionManager();
if (lam.FindExistingAction<FImplementableLatentAction>(latentInfo_.CallbackTarget, latentInfo_.UUID) == nullptr)
{
FImplementableLatentAction* action = new FImplementableLatentAction(world, latentInfo_);
lam.AddNewAction(latentInfo_.CallbackTarget, latentInfo_.UUID, action);
onEventDispatcher.ExecuteIfBound(action->Latenter);
}
}
}
まず、処理を終了するまで待機したいイベントは、終了を通知するためのALatenterActorを引数として受け取り、それを介してFinishWaitを実行することで終了を通知します。よって、そのようなイベントを受け取る動的デリゲートのシグネチャは、LatentWaiter.hの8行目のようにALatenterActorを引数とするよう宣言します。
次に、そのようなデリゲートを引数として受け取るWaitUntilFinished Latentノードを前回と同様に実装します。前回と異なるのはLatentWaiter.cppの13行目で、ここで引数として与えられた動的デリゲートに、終了を通知するためのALatenterActorを渡して実行しています。動的デリゲートを介して実行されたイベントには引数としてALatenterActorが与えられており、これを介してFinishWaitを実行すると、WaitUntilFinished Latentノードが終了して後続の処理が実行されるようになります。

WaitUntilFinished Latentノードを使用する

ここまでの実装で作成できたWaitUntilFinished Latentノードを使用して、任意のイベントをLatentノードとして実装してみます。
レベルブループリントを図2のように実装します。
図2 WaitUntilFinishedの使い方

WaitUntilFinishedに繋げるイベントは、ALatenterActorを引数とするイベントだけです。LatentEvent終了時にFinishWaitを実行することで、WaitUntilFinished Latentノードが終了して、Completed以降の処理が実行されます。
この処理は前回のものとまったく同じ挙動を示しますが、前回にはなかった次のような利点があります。

  1. 特定のシグネチャを持つ全てのイベントをLatentノード化できる
  2. 待機するためにフラグ変数の宣言を必要としない
  3. RPC化できる
  4. WaitUntilFinishedはBlueprintFunctionLibraryなので、任意のBlueprintクラス上で呼び出せる

WaitUntilDelayは特定のシグネチャに依存するため、引数を変えられないという欠点があります。そのような場合は、図3のように事前にSetupイベントを呼び出すことで対応します。図3のような実装は、ノード内に値がキャッシュされているため、問題なく動作します。
図3 LatentEventに引数が必要な場合

既にあるイベントをLatentノード化したい場合は、図4のようにシグネチャの合うイベントを用意し、処理の終了時にFinishWaitを実行します。
図4 既存のイベントをLatentノード化する

また、Latentノード化したイベントは、WaitUntilFinishedに繋げずに呼び出すことも可能です。その場合はLatentノードとしては機能しないため、処理の終了を待機しません。また、引数のALatenterActorは適切に与えられていないため、IsValidノードでチェックする必要があります。図5はLatentノードとしても非Latentノードとしても呼び出すことのできるイベントの例です。
図5 Latent・非Latentノードとして呼び出せるイベント

おわりに

WaitUntilFinishedは、任意のイベントをLatentノード化することができました。前回の方法は問題がたくさんありましたが、こちらはかなり実用的なものになったと思います。もしLatentノードを自作する必要がでた場合は、この記事を参考にしていただければ幸いです。今回作成した最終的なソースコードは次のようになりました。

#pragma once
#include "CoreMinimal.h"
#include "LatentActions.h"
#include "GameFramework/Actor.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "LatentWaiter.generated.h"
class FImplementableLatentAction : public FPendingLatentAction
{
public:
class ALatenterActor* Latenter;
FName ExecutionFunction;
int32 OutputLink;
FWeakObjectPtr CallbackTarget;
FImplementableLatentAction(UWorld* world_, const FLatentActionInfo& LatentInfo);
virtual void UpdateOperation(FLatentResponse& Response) override;
};
UCLASS()
class BLOGTEST416_API ALatenterActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ALatenterActor();
UFUNCTION(BlueprintCallable, Category = LatentWaiter)
void FinishWait();
};
DECLARE_DYNAMIC_DELEGATE_OneParam(FWaitUntilFinishedDispatcher, ALatenterActor*, latenter);
UCLASS()
class BLOGTEST416_API UBPFL_Latenter : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = Latenter, meta = (Latent, LatentInfo = "latentInfo_", HidePin = "worldContextObject_", DefaultToSelf = "worldContextObject_"))
static void WaitUntilFinished(const UObject* worldContextObject_, FLatentActionInfo latentInfo_, const FWaitUntilFinishedDispatcher onEventDispatcher);
};
view raw LatentWaiter.h hosted with ❤ by GitHub
#include "LatentWaiter.h"
FImplementableLatentAction::FImplementableLatentAction(UWorld* world_, const FLatentActionInfo& LatentInfo)
: Latenter(world_->SpawnActor<ALatenterActor>())
, ExecutionFunction(LatentInfo.ExecutionFunction)
, OutputLink(LatentInfo.Linkage)
, CallbackTarget(LatentInfo.CallbackTarget)
{
}
// Latentノードを終了させるかどうか、毎フレーム実行される関数
void FImplementableLatentAction::UpdateOperation(FLatentResponse& Response)
{
Response.FinishAndTriggerIf(!(Latenter->IsValidLowLevel() && !Latenter->IsActorBeingDestroyed()), ExecutionFunction, OutputLink, CallbackTarget);
}
ALatenterActor::ALatenterActor()
{
PrimaryActorTick.bCanEverTick = false;
SetActorHiddenInGame(true);
}
void ALatenterActor::FinishWait()
{
Destroy();
}
void UBPFL_Latenter::WaitUntilFinished(const UObject* worldContextObject_, FLatentActionInfo latentInfo_, const FWaitUntilFinishedDispatcher onEventDispatcher)
{
if (UWorld* world = GEngine->GetWorldFromContextObject(worldContextObject_))
{
FLatentActionManager& lam = world->GetLatentActionManager();
if (lam.FindExistingAction<FImplementableLatentAction>(latentInfo_.CallbackTarget, latentInfo_.UUID) == nullptr)
{
FImplementableLatentAction* action = new FImplementableLatentAction(world, latentInfo_);
lam.AddNewAction(latentInfo_.CallbackTarget, latentInfo_.UUID, action);
onEventDispatcher.ExecuteIfBound(action->Latenter);
}
}
}

この記事は次のバージョンで作成されました。
Unreal Editor(4.16.1-3466753+++UE4+Release-4.16)
Microsoft Visual Studio Community 2017 Version 15.1 (26403.7)

コメント