🕒

【UE5】FPendingLatentActionを使わずにLatentノードを作る

2024/11/16に公開

はじめに

Latentノード とは、Blueprintで非同期処理を実装する際に登場するおなじみのノードです。
代表的なLatentノードのひとつとして Delay が挙げられます。
コールバック地獄に陥りがちな非同期処理ですが、Latentノードを使用すると処理をワンラインで簡潔に記述できるため、非常に便利です。

独自のLatentノードをC++で実装する場合、一般的には FPendingLatentAction の継承クラスを新たに定義し、フレーム毎に非同期処理の進行状況をチェックして終了判定を行わせます。
しかし、この方法が冗長に感じるケースがあります。
例えば、コールバックやデリゲートで非同期処理の終了タイミングが通知される場合、毎フレームのチェックをわざわざ行う必要はなさそうです。

そこで、本記事では FPendingLatentAction を使わずに独自の Latentノード を実装する方法を共有します。

※ FPendingLatentAction について

実装方法

LatentノードをC++で定義するには UFUNCTION() を用いて特定の形式で関数を定義します。
その際、引数に FLatentActionInfo 構造体の変数を含めることが必須です。

この FLatentActionInfo 構造体は、非同期処理完了時に呼び出すべき後続ノードの情報を保持しています。
この構造体を利用して、以下のように後続ノードの呼び出しを実装することができます。

// FLatentActionInfo LatentActionInfo;

UObject* CallbackTarget = LatentActionInfo.CallbackTarget;
int32 Linkage = LatentActionInfo.Linkage;

if (CallbackTarget && Linkage != INDEX_NONE)
{
    // 後続ノードを呼び出し
    UFunction* ExecutionFunction = CallbackTarget->FindFunction(LatentActionInfo.ExecuteFunction);
    if (ExecutionFunction)
    {
        CallbackTarget->ProcessEvent(ExecutionFunction, &Linkage);
    }
}

つまり、上記のロジックを非同期処理のコールバックやデリゲートに仕込むことで、Latentノードを簡単に作成することができます。

実装例

前項の実装方法を踏まえて、TimerEvent を用いたオリジナルの Delayノード を実装してみます。
TimerManager に後続ノードの呼び出し処理を TimerEvent として登録します。

MyLatentFunctionLibrary.h
UCLASS()
class UMyLatentFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()

public:
    /**
     * タイマーイベントを利用したDelayノード
     * @param Duration - 待機時間 (秒)
     */
    UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", Latent, LatentInfo = "LatentActionInfo"))
    static void DelayAsTimerEvent(UObject* WorldContextObject, float Duration, FLatentActionInfo LatentActionInfo)
    {
        // FLatentActionManager で行っている Latentノード の待機終了時処理 (後続ノードの呼び出し処理) を,
        // タイマーイベントで直接行わせる
        //
        // @see FLatentActionManager::TickLatentActionForObject()

        // Completed ピンに接続されている後続ノードの情報を, LatentActionInfo から取得する
        TWeakObjectPtr<UObject> CallbackTarget = LatentActionInfo.CallbackTarget;
        int32 Linkage = LatentActionInfo.Linkage;

        if (!CallbackTarget.IsValid() || Linkage == INDEX_NONE)
        {
            return;
        }

        TWeakObjectPtr<UFunction> ExecutionFunction
            = CallbackTarget->FindFunction(LatentActionInfo.ExecutionFunction);

        // タイマーイベントに後続ノードの呼び出し処理を登録
        UWorld* World = GEngine->GetWorldFromContextObjectChecked(WorldContextObject);
        if (World)
        {
            FTimerDelegate TimerDelegate = FTimerDelegate::CreateWeakLambda(
                CallbackTarget.Get(), [CallbackTarget, ExecutionFunction, Linkage]
                {
                    if (CallbackTarget.IsValid() && ExecutionFunction.IsValid())
                    {
                        int32 Link = Linkage;
                        CallbackTarget->ProcessEvent(ExecutionFunction.Get(), &Link);
                    }
                });

            FTimerHandle TimerHandle;
            World->GetTimerManager().SetTimer(TimerHandle, TimerDelegate, Duration, false);
        }
    }
};

実装確認

実装した DelayAsTimerEvent を、適当にレベルブループリント等に置いて試してみます。
以下の例では、レベルブループリントの BeginPlay 直後と、DeleyAsTimerEvent で5秒間ディレイした直後の ゲーム時間を PrintText で出力させています。

FPendingLatentAction を使用せずLatentノードを動作させることができました。

留意点

FPendingLatentAction を使わずにLatentノード 実装する場合の欠点として、SetGamePaused によるポーズ処理を無視して後続ノードの呼び出しが行われてしまう点が挙げられます。
この挙動が利点となり得るケースがある一方で、バグや予期せぬ挙動の原因となることもあるため注意が必要です。

Discussion