🗨️

Unreal Engine5でMessagePackをプラグイン化~実装編~

2024/11/17に公開

前回

前回の記事でプラグインの雛形を作り、サードパーティのソースを追加しました。
今回はUnreal Engineの中身の実装をしていきます。

今回試したこと

・JSON形式のMessagePackのシリアライズデータを受け取って、Unreal Engine上でデシリアライズする
※データ自体は別でURLにアクセスして取得できるように別で作っておく
・ブループリントライブラリを作り、デシリアライズのBPノードを作成する
・HTTP通信をするために、UBlueprintAsyncActionBaseを使ってタスクノードを作成する
・UMGでボタンを配置して、クリックしたらHTTP通信をしてデータを取得するように作る

UMGでボタンを配置

コンテンツドロワー内に何かしらのフォルダを作成(testというフォルダをコンテンツフォルダ内に作りました)
 ↓
作成したフォルダ内で[右クリック]-[ユーザーインターフェース]-[ウィジェットブループリント]を選択肢して作成。名前を「WB_UI」とでも作成して置く

以下の画像のようにボタンを配置しておく(細かい部分は割愛)

ボタンクリックでイベントがトリガーするように作成したボタンを選択して[詳細]下部のイベントにある[On Clicked]の[+]をクリックしておく
画像の様なイベントノードがイベントグラフ内に出来上がることを確認

HTTP通信を行うタスクノードを作成

前準備として、[プロジェクト名].Build.csのPublicDependencyModuleNamesに「"HTTP", "Json"」を追加する。

エディタのツールから「C++新規クラス」を選択肢、BlueprintAsyncActionBaseを継承してクラスを作成


名前は「HttpRequestAsyncTask」と名前を付け作成します。(名前は任意)

今回は細かいところは割愛でソースを全文載せます

HttpRequestAsyncTask.h
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "Http.h"
#include "HttpRequestAsyncTask.generated.h"

//デリゲートの宣言
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHttpRequestCompleted, const TArray<uint8>&, Response);

UCLASS()
class ASOBIBA_API UHttpRequestAsyncTask : public UBlueprintAsyncActionBase
{
    GENERATED_BODY()

public:
    //OnCompletedデリゲートの宣言(ピンの追加)
    UPROPERTY(BlueprintAssignable)
    FOnHttpRequestCompleted OnCompleted;

    //ノードの内容
    UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"), Category = "HTTP")
    static UHttpRequestAsyncTask* SendHttpRequest(const FString& Url, const FString& Verb);

    //ノード実行時に呼ばれる関数
    virtual void Activate() override;

private:
    FString RequestUrl;
    FString RequestVerb;

    //リクエストの完了時に呼ばれる関数
    void HandleRequestCompleted(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
};
HttpRequestAsyncTask.cpp
#include "HttpRequestAsyncTask.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"
#include "HttpManager.h"
#include "Http.h"

UHttpRequestAsyncTask* UHttpRequestAsyncTask::SendHttpRequest(const FString& Url, const FString& Verb)
{
    //リクエストが作成されるときに呼ばれる
    UHttpRequestAsyncTask* RequestTask = NewObject<UHttpRequestAsyncTask>();
    RequestTask->RequestUrl = Url;
    RequestTask->RequestVerb = Verb;
    return RequestTask;
}

void UHttpRequestAsyncTask::Activate()
{
    //リクエストの送信
    TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
    Request->OnProcessRequestComplete().BindUObject(this, &UHttpRequestAsyncTask::HandleRequestCompleted);
    Request->SetURL(RequestUrl);
    Request->SetVerb(RequestVerb);
    Request->ProcessRequest();
}

void UHttpRequestAsyncTask::HandleRequestCompleted(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
    //リクエストの完了時に呼ばれる
    TArray<uint8> ResponseArray;
    if (bWasSuccessful && Response.IsValid())
    {
        //リクエストが成功した場合にデータを取得
        ResponseArray = Response->GetContent();
    }
    else
    {   
        //リクエストが失敗した場合
        UE_LOG(LogTemp, Error, TEXT("HTTP Request failed"));
    }
    //デリゲートの実行
    OnCompleted.Broadcast(ResponseArray);
}

上記をビルドすることで以下のようなノードが使えるようになります。

これをボタンクリックの先に接続。
URL
Verb
をそれぞれ設定します。

流れとしては
URLへVerbの形式でリクエスト通信をする
 ↓
リクエスト後は一番上の線に抜ける
 ↓
リクエストが完了したら「On Completed」から線が抜けていき「Response」というデータを戻り値として返す

MessagePackのプラグインでデシリアライズする

BlueprintFunctionLibraryを継承してC++のクラスを作成
※この時作成先のRuntimeを[プラグイン名]の方にして作成します。

出来上がったソースに実装をしていきます。
細かい部分は割愛しますが、今回は返してきたMessagePackのシリアライズデータをFString形式で取得する実装にしています。

BP_MsgPackLibrary.h
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include <msgpack.hpp> // MessagePackのヘッダーをインクルード 

#include "BP_MsgPackLibrary.generated.h"

namespace msgpack {
	MSGPACK_API_VERSION_NAMESPACE(MSGPACK_DEFAULT_API_NS) {
		namespace adaptor {

			template<>
			struct convert<FString> {
				const msgpack::object& operator()(const msgpack::object& obj, FString& str) const {
					if (obj.type != msgpack::type::BIN && obj.type != msgpack::type::STR) {
						throw msgpack::type_error();
					}
					str = FString(UTF8_TO_TCHAR(obj.via.str.ptr));
					return obj;
				}
			};

			template<>
			struct pack<FString> {
				template <typename Stream>
				packer<Stream>& operator()(msgpack::packer<Stream>& o, const FString& str) const {
					o.pack_str(str.Len());  // 文字列の長さを指定
					o.pack_str_body(TCHAR_TO_UTF8(*str), str.Len());  // 文字列の内容を指定
					return o;
				}
			};
		} // namespace adaptor
	} // MSGPACK_API_VERSION_NAMESPACE(MSGPACK_DEFAULT_API_NS)
} // namespace msgpack

UCLASS()
class PLG_MSGPACK_API UBP_MsgPackLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable, Category = "BP_MsgPackLibrary") 
	static bool DeserializeString(const TArray<uint8>& Data, FString& OutString);
};
BP_MsgPackLibrary.cpp
#include "BP_MsgPackLibrary.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"

bool UBP_MsgPackLibrary::DeserializeString(const TArray<uint8>& Data, FString& OutString)
{
    try
    {
        msgpack::object_handle oh = msgpack::unpack(reinterpret_cast<const char*>(Data.GetData()), Data.Num());
        msgpack::object obj = oh.get();

        // std::mapにデシリアライズ
        std::map<std::string, msgpack::object> deserializedMap;
        obj.convert(deserializedMap);

        // JSON形式の文字列に変換
        TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutString);
        Writer->WriteObjectStart();
        for (const auto& Pair : deserializedMap)
        {
            if (Pair.second.type == msgpack::type::STR)
            {
                Writer->WriteValue(UTF8_TO_TCHAR(Pair.first.c_str()), UTF8_TO_TCHAR(Pair.second.as<std::string>().c_str()));
            }
            else if (Pair.second.type == msgpack::type::POSITIVE_INTEGER)
            {
                Writer->WriteValue(UTF8_TO_TCHAR(Pair.first.c_str()), Pair.second.as<int>());
            }
            // 他の型も必要に応じて追加
        }
        Writer->WriteObjectEnd();
        Writer->Close();

        return true;
    }
    catch (const msgpack::insufficient_bytes& e)
    {
        UE_LOG(LogTemp, Error, TEXT("Insufficient bytes: %s"), UTF8_TO_TCHAR(e.what()));
        return false;
    }
    catch (const std::exception& e)
    {
        UE_LOG(LogTemp, Error, TEXT("Exception: %s"), UTF8_TO_TCHAR(e.what()));
        return false;
    }
}

上記のAPI名など細々したところはありますが、調整をしてビルドを通してもらい、実際にUMGのボタンをクリックした際にデータを拾ってくるようにします。

以下WB_UIのボタンクリック時の全貌

今回はテストなのでサードパーソンプロジェクトに最初から備わっている、BP_ThirdPersonCharacterを使い「Event BeginPlay」からUIを読み込みます。

後はデータを取得できるようにしておけば実行時にボタンが表示されます。

ボタンをクリックすると・・・

今回はMessagePackを使ったデシリアライズの実装例を紹介しました。
MessagePackを使うためのチュートリアルとして見てもらえれば幸いです。

---余談---
ただこのままだと現場で使うには厳しいかとは思います。

もう少し抽象度の高い形で作り、どのようなデータでも対応できるようにかつ、クライアント(Unreal Engine側)からデータのリクエストをシリアライズして送りサーバーがそのデータをデシリアライズしてDBからデータを取得→レスポンスするといった動作ができないといけません。
また、データが固定ではいけないので、データセットをお互いに認識できる共通のフォーマットを用意するなどして、対応が必要そう(正直現場で使うならばまだまだ道は長そうです)

Discussion