Unreal Engine C++ の非同期処理を JavaScript の Promise ライクに書く方法

2024/08/13に公開

方法

TPromiseTFuture を利用することで、デリゲートや std::function / TFunction の複雑な処理の受け渡しを減らすことができ、コードが簡潔になる。

TFutureNext() の呼び出しは TPromise の結果を待たずに処理を終了するため(TPromise に値が与えられたタイミングで実行されるため)、GameThread で以下の呼び出しをしてもブロックすることはない。

TFuture<TOptional<FString>> SendHttpRequest(const FString& Url, const FString& Verb) {
    auto Promise = MakeShared<TPromise<TOptional<FString>>>();
    TFuture<TOptional<FString>> FutureResult = Promise->GetFuture();
    const auto Req = FHttpModule::Get().CreateRequest();
    Req->OnProcessRequestComplete().BindLambda([CapturedPromise = MoveTemp(Promise)](FHttpRequestPtr Req, FHttpResponsePtr Res, bool Suc) mutable {
        if (Suc && EHttpResponseCodes::IsOk(Res->GetResponseCode())) {
            CapturedPromise->SetValue(Res->GetContentAsString());
        }
        else {
            CapturedPromise->SetValue(NullOpt);
        }
    });
    Req->SetURL(Url);
    Req->SetVerb(Verb);
    Req->ProcessRequest();
    return FutureResult;
}

TFuture<TOptional<FString>> ParseResponseData() {
    return SendHttpRequest("https://jsonplaceholder.typicode.com/todos/1", "GET").Next([](TOptional<FString> Response) {
        // 実際は何かする
        return Response;
    });
}

TFuture<TOptional<FString>> ProcessBusinessLogic() {
    return ParseResponseData().Next([](TOptional<FString> ParsedData) {
        // 実際は何かする
        return ParsedData;
    });
}

void ExecuteWorkflow()
{
    UE_LOG(LogTemp, Log, TEXT("Start request"));
    ProcessBusinessLogic().Next([](TOptional<FString> Result) {
        UE_LOG(LogTemp, Log, TEXT("Result: %s"), *(Result.Get("Something went wrong.")));
    });
    UE_LOG(LogTemp, Log, TEXT("Exiting function."));
}

log

LogTemp: Start request
LogTemp: Exiting function.
LogTemp: Result: {
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

注意点

TPromiseSetValue() メソッドは一度しか呼び出してはいけない。複数回呼ばれる可能性がある場合は特に注意が必要。
(5.4.3 の Engine コード。SetValue())
https://github.com/EpicGames/UnrealEngine/blob/7ba7e260a528ffbab6d0157890f8ecbe363925f4/Engine/Source/Runtime/Core/Public/Async/Future.h#L1028

おまけ(Async 関数)

Async 関数を使うと、並列タスクの結果を TFuture として受け取ることができ、ConstructAndDispatchWhenReady() を使うよりも便利。GameThread で呼び出してもスレッドの実行をブロックしない。

void TestFunction()
{
    TFuture<int32> Res = Async(EAsyncExecution::TaskGraphMainThread, []() {
        UE_LOG(LogTemp, Log, TEXT("Executing async task."));
        return 33;
    });
    Res.Next([](int32 Value) {
        UE_LOG(LogTemp, Log, TEXT("Async task completed. Result: %d"), Value);
    });
    UE_LOG(LogTemp, Log, TEXT("Exiting function."));
}

// LogTemp: Exiting function.
// LogTemp: Executing async task.
// LogTemp: Async task completed. Result: 33

スクラップ

https://zenn.dev/hakoai/scraps/79eeac6921f6e3

Discussion