Unreal Engine5でMessagePackをプラグイン化~実装編~
前回の記事でプラグインの雛形を作り、サードパーティのソースを追加しました。
今回は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」と名前を付け作成します。(名前は任意)
今回は細かいところは割愛でソースを全文載せます
#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);
};
#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形式で取得する実装にしています。
#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);
};
#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