UE5のBlueprintでGraphQLリクエストをLatentノードで実装する

# リクエスト結果の受信がイベント発火型だと、ノードのつなげ方が分かりにくかった

前回、[UE5のBlueprintでGraphQLをリクエストする](https://qiita.com/so2/items/2ccc4245895b88d6c5d8)で、GraphQLでのリクエストと、結果の受信ができるようになりました
ただ、下記のように、リクエストと、受信のフローが別れてしまって、パッと見でどことどこがつながっているのかわかりにくい状態です
![ノードのつなげ方.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601616/27972bf4-335e-b834-ba8a-b81e5463cdff.png)

## Latentノードにする

UEのBlueprintノードにはLatentという、処理待ちできるノードがありました

上の図、下段で最後の方についているDelayがそれです（ノード右肩に時計のマークがついている）

Queryのノード自体をそちらに変更することで、リクエストから受信までのノードをつなげることができ、見た目がわかりやすくなりました![Latentノードでつなげると.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601616/08db64c7-3a07-3fe4-c733-233ced32aa09.png)

## 環境
| ソフトウェア | バージョン | 
|:-----------|------------:|
| MacOS       | 10.15.7        | 
| UnrealEngine | 5.0.0 |
| Visual Studio Code |  1.61.2 |
| Xcode | 12.4 |
| .NET SDK |5.0.402|

## コード
```C++:GraphqlFunctionLibrary.h
#pragma once

#include "Http.h"
#include "CoreMinimal.h"
#include "LatentActions.h"
#include "Engine/LatentActionManager.h"
#include "Kismet/BlueprintFunctionLibrary.h"

#include "GraphqlFunctionLibrary.generated.h"

UCLASS()
class GRAPHQLTEST_API UGraphqlFunctionLibrary
    : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()

    UFUNCTION(BlueprintCallable, Category = "Graphql", meta = (DisplayName = "Send GraphQL Request", Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo"))
    static void Query(UObject *WorldContextObject, FLatentActionInfo LatentInfo, FString endpoint, FString document, bool &success, FString &jsonString);
};

class FQueryAction
    : public FPendingLatentAction
{
    FLatentActionInfo m_LatentInfo;
    FHttpModule *Http;
    FString *jsonStringPtr;
    bool *successPtr;
    bool isCompleted = false;

public:
    FQueryAction(const FLatentActionInfo &LatentInfo, FString endpoint, FString document, FString method, bool &success, FString &jsonString);
    virtual void UpdateOperation(FLatentResponse &Response) override;

    // HTTP通信を行ってレスポンスが返ってきた際のイベント処理
    void OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
};
```

```C++:GraphqlFunctionLibrary.cpp
#include "GraphqlFunctionLibrary.h"

void UGraphqlFunctionLibrary::Query(UObject *WorldContextObject, FLatentActionInfo LatentInfo, FString endpoint, FString document, bool &success, FString &jsonString)
{
    if (UWorld *World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
    {
        FLatentActionManager &LatentActionManager = World->GetLatentActionManager();
        LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FQueryAction(LatentInfo, endpoint, document, "query", success, jsonString));
    }
}

FQueryAction::FQueryAction(const FLatentActionInfo &LatentInfo, FString endpoint, FString document, FString method, bool &success, FString &jsonString)
    : m_LatentInfo(LatentInfo)
{
    GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, document + method);

    Http = &FHttpModule::Get();
    // ノードから出力する変数にOnResponseReceived内でアクセスできるようメンバ変数にポインタをコピー
    jsonStringPtr = &jsonString;
    successPtr = &success;

    // Jsonデータの作成
    TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
    JsonObject->SetStringField(method, document);
    // JsonStringにJson書き出し
    FString JsonString;
    TSharedRef<TJsonWriter<TCHAR> > JsonWriter = TJsonWriterFactory<>::Create(&JsonString);
    FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter);

    // Httpリクエストの作成
    TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = Http->CreateRequest();
    // BindBuildではコンパイルエラーになったので、BindRawを使用
    Request->OnProcessRequestComplete().BindRaw(this, &FQueryAction::OnResponseReceived);
    Request->SetURL(endpoint);
    Request->SetVerb("POST");
    Request->SetHeader(TEXT("User-Agent"), TEXT("X-UnrealEngine-Agent"));
    Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
    Request->SetHeader(TEXT("Accepts"), TEXT("application/json"));
    Request->SetContentAsString(JsonString);
    Request->ProcessRequest();
}

void FQueryAction::UpdateOperation(FLatentResponse &Response)
{
    Response.FinishAndTriggerIf(isCompleted, m_LatentInfo.ExecutionFunction, m_LatentInfo.Linkage, m_LatentInfo.CallbackTarget);
}

void FQueryAction::OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
    if (!Response.IsValid())
    {
        GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, "No Valid");
        *successPtr = false;
    }
    else if (EHttpResponseCodes::IsOk(Response->GetResponseCode()))
    {
        FString ContentAsString = Response->GetContentAsString();

        TSharedPtr<FJsonObject> JsonObject;
        TSharedRef<TJsonReader<> > Reader = TJsonReaderFactory<>::Create(ContentAsString);

        // Jsonオブジェクトをデシリアライズ
        if (FJsonSerializer::Deserialize(Reader, JsonObject))
        {
            GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Red, "Received JSON String");
            *jsonStringPtr = ContentAsString;
            *successPtr = true;
        }
    }
    isCompleted = true;
}
```

FLatentActionManagerにLatent化したいクラスの登録と、FPendingLatentActionを継承したクラスに、実際の処理と、受信後に待機状態を解除する処理を追加しています

リクエストの受信時の処理自体は、`IHttpRequest`のcallback関数なので、ノードで出力したい変数の取り扱いが、ポインタのリレーになっています
動いているけれど、これが安全な処理なのか不安が残りました

実際にクエリーを投げたり受け取ったりする部分の処理は前回と同じです

JSONを受け取っていますが、Blueprintに出力しているのはstringifyした状態の文字列なのも前回と同じです

## さいごに

Webで検索するとUE4での実装例が出てきますが、公式のドキュメントには仕様だけで実装例がなく、これで様々な機能を実装できる先人の理解力に頭がさばるばかりです

## 参考
[UE4：ディレイ系ノードの作り方（LatentAction）](https://toncrimentan-w.hatenablog.com/entry/2021/03/06/014025)