本記事はQiita UnrealEngine AdventCalendar 2022の記事となります。毎年UnrealEngine関連の素晴らしい記事を提供してくださる方々に感謝いたします。
UnrealEngineでWebSocketを実装してみましょう
今年のAdventCalendarは私の得意分野であるクラウドサービス系・WEB系とUnrealEngineを連携させてみよう思い、チャレンジしてみることにしました。
本記事で紹介するWebSocketは、リアルタイムな双方向通信が実現できますので、ゲーム分野では他ユーザーとのネットワーク対戦やチャットやランキングなど、ノンゲーム分野でも遠隔PCとのリアルタイム連携などで利用できるかと思います。
WebSocketの実装にはサーバが必要になってきますが、こちらに関してもMicrosoft Azureが提供しているWeb PubSubというサービスを利用することで、サーバレスで、しかも(一定の通信量であれば)無償で構築することが可能ですので、皆様も是非構築してみてください。
動作デモ
最終的な成果物はこんな感じのものになります。
WEBブラウザに画像をドロップすると、UE上でQuinnちゃんの服装に反映される・・・というだけです。これを、リアルタイム連携で行っています。
インフラ設計
下図の通り、AzureのWeb PubSubサービスを利用して、WebブラウザとUnrealEngineからそれぞれWebSocket接続で双方向通信の常時接続ネットワークを張っています。
Webブラウザから送信された写真は、UnrealEngineで受信され、UnrealEngineが扱えるTexture2D型に変換され、動的にマテリアルを差し替えることでQuinnちゃんの一部マテリアルが変化し、着せ替えしているように見せています。
写真以外にも文字列なども送信できますので、UnrealEngine同士をWebSocketで接続してリアルタイムに連携したり、スコアランキングなんかの仕組みも作れるようになります。
Microsoft Azure Web PubSubで5分でWebSocketサーバ構築
(事前にAzureで使えるアカウントの準備してログインしておいてください)
Azureの管理画面にログインしホーム画面が表示されている状態からスタートします。
画面上「リソースの作成」を作成をクリックしてください。
検索欄に”Web PubSub Service”と入力すると、検索結果が表示されますので、作成をクリック。
Web PubSubを新規作成する画面が表示されますので、以下の値を設定してください。
サブスクリプション:(基本的にはデフォルトで良いですが、複数ある場合は適切なものを選択)
リソースグループ:新規作成から”UnrealEngine”という名前で作成しましたが、何でも構いません
リソース名:他のサービス利用者と重複しなければなんでもOKです。今回は”uewebsockettest”としました
リージョン:Japan Eastを選択してください
価格レベル:「変更」をクリックし、「無料」を選択してください
(注意:価格レベルは必ず「無料」を選択してください。それ以外を選択すると請求が発生してしまいますのでご注意ください)
「次:ネットワーク」ボタンをクリックすると、以下の画面が表示されますので、「パブリックエンドポイント」を選択します。(今回のデモはインターネット上に公開されたサーバを使ってやりとりする為です)
タグの設定は不要なので「確認と作成」をクリックしてください。すると、サーバーの検証が動作します。ちょっと待つと、「作成ボタン」が活性化します。
作成ボタンをクリックすると、「デプロイが進行中です」という画面が表示されますので、数分待機します。
「デプロイが完了しました」という表示に切り替わったら「リソースに移動」をクリックしてください。
以下のような画面が表示されたら、WebSocketサーバの完成です。これだけでサーバが構築できてしまうなんて良い世の中になりました。
Azure Web PubSubのセキュリティ&認証
ここまでの作業でWebSocketサーバが構築できましたが、認証も無く、誰でもこのサーバに接続できてしまっては困ってしまいます。
しかし、認証処理の実装は複雑になるという欠点があります。
ちょうど良いことに、Azure Web PubSubには容易にゲストアカウント(のようなもの)を発行できる機能がありますので、今回のデモはこの機能を使って接続を行います。
上記で作成した、Web PubSubのリソース管理画面の右側に表示されているメニューの中から「キー」をクリックし、「クライアントURLジェネレーター」という欄に、以下の情報を入力します。
ハブ:UE
ユーザーID:UE
トークンの有効時間(分):何分でも良いですが、長い方が良いかもしれません(今回はデフォルトの60)
Roles:「グループに送信」と「グループに参加または脱退」の両方にチェック
すると、「クライアントアクセスURL」にゴチャゴチャしたURLが表示されます。今回のデモでは”このURLを知っている=利用できるユーザー”ということで実装を行っていきます。
もし実装の途中で挙動がおかしいとか、うまく行かない場合は、ここで作成したURLの有効期限が切れている可能性がありますので、この画面に戻ってきて、新たにURLを作成しなおしてください。
以降、ここで作成したURLのことは「クライアントアクセスURL」と呼びますので、この言葉が登場する度に、ここのURLを確認してください。
Webブラウザで動作させるHTML側の実装
早くUEの実装に入れ!という感じですが、WebSocketの基本を理解しやすいように、Webブラウザで動作するHTMLから作成していきます。
テキストエディタ等で、以下のHTMLを入力してみてください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>UnrealEngineとWebSocketで連携</title>
<script type="text/javascript">
// グループの設定
const GROUP = "UE";
// Web PubSub用データ送信作成処理
function makeData(data){
return JSON.stringify({ type: "sendToGroup", group: GROUP, data: data });
}
// Azure Web PubsSubに接続
var webSocket = new WebSocket("「クライアントアクセスURL」", "json.webpubsub.azure.v1");
webSocket.onmessage = e => {
if (e.data) {
var message = JSON.parse(e.data);
// 自身のグループ宛てのメッセージだった場合はチャットメッセージを追加
if (message.type === "message" && message.group === GROUP){
var li = document.createElement('li');
li.innerHTML = message.data;
document.getElementById("messages").appendChild(li);
}
}
};
// WebSocket接続と同時にグループに所属させる命令を送信
webSocket.onopen = e => {
webSocket.send(JSON.stringify({type: "joinGroup", group: GROUP }));
};
// チャットメッセージ送信
function send(){
webSocket.send(makeData(document.getElementById('message').value));
}
</script>
</head>
<body>
<input type="text" id="message">
<button type="button" onclick="send();">送信</button>
<ul id="messages"></ul>
</body>
</html>
作成したHTMLを、お手持ちのWebブラウザを2つ立ち上げで、それぞれのブラウザに作成したHTMLをドラッグ&ドロップすると簡易的なチャットが起動します。
入力欄に文字を入力し、送信ボタンをクリックすると、2つのWebブラウザ上で同じメッセージが表示されるようになったかと思います。
これは、先ほど作成したAzure Web PubSubサーバを介して、各ブラウザから接続されたWebSocketでメッセージを通信していることで実現しています。
今は同じPC上で2つのWebブラウザを立ち上げているので感動は少ないかもしれませんが、複数台のPCを持っている場合は、このHTMLをそれぞれのPCで読み込むだけでチャットプログラムが完成してしまうのです。
ということで、今はWebブラウザ間の通信ですが、この片一方をUnrealEngineに置き換える事にチャレンジしていきます。
Unreal Engine 5でWebSocketモジュールを使った実装
それでは、お待ちかねのUnreal Engine側を作成していきます。
サードパーソンテンプレートを選択し、適当なプロジェクト名を設定してください。(今回は”UEWebSocket”というプロジェクトで作成しました)
デバッグ表示等を分かりやすくする為に「アウトプットログ」ウインドウ(メニューの「ウィンドウ->アウトプットログ」)を表示しておきましょう。
早速ですが、GameInstanceSubsystemにWebSocketの実装を行います。
メニューの「ツール->新規C++クラス」を選択し、”GameInstanceSubsystem”を選択します。
ファイル名デフォルトのままで”MyGameInstanceSubsystem”のまま「クラスを作成」ボタンをクリックします。初めてC++を追加すると、IDEからビルドすることを促されるダイアログが表示されますので、「OK」ボタンをクリックし、一度UnrealEngineを終了し、VisualStudioから起動または、当該プロジェクトを再度開きなおしてください。
正常にC++が追加されると、先ほど作成した”MyGameInstanceSubsystem”がプロジェクトに追加されていることが確認できると思います。こちらのファイルをダブルクリックして編集していきます。
(C++開発環境が整っていれば)VisualStudioが開きますので、まずは”プロジェクト名.Build.cs”(サンプルの場合は”UEWebSocket.Build.cs”)の”PublicDependencyModuleNames”のAddRange内の文字列配列を追記します。
これは、WebSocketのモジュールを使用することを宣言し、更にQuinnちゃんを着替えさえる為の画像変換や、WebSocketで送られてくるJSONデータのデコードのモジュールを使うことを宣言しています。
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "WebSockets", "ImageWrapper", "Json", "JsonUtilities" });
引き続き、”MyGameInstanceSubsystem.h”を以下のようにします。
初期処理・終了処理とWebSocketでの接続・接続エラー・切断・メッセージ受信時の定義を行っているだけとなります。
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "MyGameInstanceSubsystem.generated.h"
class IWebSocket;
DECLARE_LOG_CATEGORY_EXTERN(LogMyGameInstance, Log, All);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSockMsg, FString, msg);
/**
* MyGameInstanceSubsystem
*/
UCLASS()
class UEWEBSOCKET_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection)override;
virtual void Deinitialize()override;
UPROPERTY(BlueprintAssignable)
FSockMsg OnReceiveMessage;
private:
TSharedPtr<IWebSocket> WebSocket = nullptr;
FString message;
void OnSocketConnected()const;
void OnSocketConnectioinError(const FString& err)const;
void OnSocketClosed(const int statusCode, const FString& reason, const bool wasClean)const;
void OnSocketMessage(const FString& msg);
};
“MyGameInstanceSubsystem.cpp”も実装します。
こちらが実処理の実装となります。具体的には、Azure Web PubSubで発行した「クライアントアクセスURL」を使ってサーバにアクセスし、WebSocketの接続・接続エラー・切断・メッセージ受信時にアウトプットログに内容を出力しているだけの処理となります。
// Fill out your copyright notice in the Description page of Project Settings.
#include "MyGameInstanceSubsystem.h"
#include "WebSocketsModule.h"
#include "IWebSocket.h"
DEFINE_LOG_CATEGORY(LogMyGameInstance);
/**
* @brief
* 初期処理
* @param Collection
*/
void UMyGameInstanceSubsystem::Initialize(FSubsystemCollectionBase& Collection) {
// Azure Web PubSubの接続情報を記述
const FString ServerUri = TEXT("「クライアントアクセスURL」");
const FString ServerPrt = TEXT("json.webpubsub.azure.v1");
// WebSocket生成
WebSocket = FWebSocketsModule::Get().CreateWebSocket(ServerUri, ServerPrt);
// イベントハンドラー
WebSocket->OnConnected().AddUObject(this, &UMyGameInstanceSubsystem::OnSocketConnected);
WebSocket->OnConnectionError().AddUObject(this, &UMyGameInstanceSubsystem::OnSocketConnectioinError);
WebSocket->OnClosed().AddUObject(this, &UMyGameInstanceSubsystem::OnSocketClosed);
WebSocket->OnMessage().AddUObject(this, &UMyGameInstanceSubsystem::OnSocketMessage);
// WebSocket接続+グループに所属
WebSocket->Connect();
WebSocket->Send(TEXT("{\"type\":\"joinGroup\",\"group\":\"UE\"}"));
}
/**
* @brief
* 終了処理
*/
void UMyGameInstanceSubsystem::Deinitialize() {
WebSocket->Close();
WebSocket = nullptr;
}
/**
* @brief
* WebSocketに接続した時に呼び出される処理
*/
void UMyGameInstanceSubsystem::OnSocketConnected() const {
UE_LOG(LogMyGameInstance, Log, TEXT("WebSocket Connected"));
}
/**
* @brief
* WebSocketの接続に失敗したときに呼び出される処理
* @param err
*/
void UMyGameInstanceSubsystem::OnSocketConnectioinError(const FString& err) const {
UE_LOG(LogMyGameInstance, Warning, TEXT("WebSocket Error: %s"), *err);
}
/**
* @brief
* WebSocketが閉じられた時に呼び出される処理
* @param statusCode
* @param reason
* @param wasClean
*/
void UMyGameInstanceSubsystem::OnSocketClosed(const int statusCode, const FString& reason, const bool wasClean) const {
UE_LOG(LogMyGameInstance, Log, TEXT("WebSocket Closed: status %d, reason %s"), statusCode, *reason);
}
/**
* @brief
* WebSocketからメッセージを受信した時に呼び出される処理
* @param msg
*/
void UMyGameInstanceSubsystem::OnSocketMessage(const FString& msg) {
// 受信メッセージ溜めこみ
message += msg;
// JSON変換
TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(*message);
TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject());
// JSON変換ができて、dataフィールドを持っている場合は、受信したdataフィールドをBPに渡す
if (FJsonSerializer::Deserialize(JsonReader, JsonObject)){
if (JsonObject->HasField("data")){
message = JsonObject->GetStringField("data");
UE_LOG(LogMyGameInstance, Log, TEXT("WebSocket Message: %s"), *message);
OnReceiveMessage.Broadcast(message);
}
message = "";
}
}
全て入力し終わったらC++をビルドします。
その後、プレイボタンをクリックすると、アウトプットログに”LogMyGameInstance: WebSocket Connected”と表示されるので、WebSocketサーバに接続されていることが分かります。
試しに、先ほど作成したHTMLをWebブラウザで開き、チャットにメッセージを入力して送信してみてください。
UnrealEngineのアウトプットログに、入力した文字が表示されるはずです。(うまくいかない場合は、「クライアントアクセスURL」を作り直して、C++とHTMLの両方に反映させてみてください)
これで、WebブラウザとUnrealEngineを繋げることができました。
WebSocketの実装という点だけで言えば、ここまでで実装が完了していますが、更に突っ込んでWebブラウザで指定した画像でQuinnちゃんのマテリアルを動的に差し替えるという処理を作っていきます。
Webブラウザ側の改修
Webブラウザ側では、画像ファイルをアップロードする仕組みを作る必要がありますので改修していきます。
WebSocketには画像ファイル等のBinaryデータを送信する仕組みもありますが、今回は先ほど作成したチャットシステムで送っているように、文字として画像データを送信する仕組みで作ってみます。
こんな時に役に立つのがBase64という技術ですので、これを利用してみましょう。(ただ、速度を求める処理や大容量のデータ送信には基本的に使わない技術です。今回はサンプルということでお許しを。)
ということで、完成したものがこちらです。(青い部分が変更した点になります)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>UnrealEngineとWebSocketで連携</title>
<script type="text/javascript">
// グループの設定
const GROUP = "UE";
// Web PubSub用データ送信作成処理
function makeData(data){
return JSON.stringify({ type: "sendToGroup", group: GROUP, data: data });
}
// Azure Web PubsSubに接続
var webSocket = new WebSocket("「クライアントアクセスURL」", "json.webpubsub.azure.v1");
webSocket.onmessage = e => {
if (e.data) {
var message = JSON.parse(e.data);
// 自身のグループ宛てのメッセージだった場合はチャットメッセージを追加
if (message.type === "message" && message.group === GROUP){
var li = document.createElement('li');
li.innerHTML = message.data;
document.getElementById("messages").appendChild(li);
}
}
};
// WebSocket接続と同時にグループに所属させる命令を送信
webSocket.onopen = e => {
webSocket.send(JSON.stringify({type: "joinGroup", group: GROUP }));
};
// チャットメッセージ送信
function send(){
webSocket.send(makeData(document.getElementById('message').value));
}
// 画像のドロップ
function dropFile(event){
event.preventDefault();
var files = event.dataTransfer.files;
var reader = new FileReader();
reader.onload = function(event) {
webSocket.send(makeData(reader.result));
}
reader.readAsDataURL(files[0]);
}
// 画像のドロップを可能にする
function dragOver(event){
event.preventDefault();
}
</script>
</head>
<body>
<input type="text" id="message">
<button type="button" onclick="send();">送信</button>
<div style="width:300px;height:300px;border:1px solid #000000;" ondrop="dropFile(event)" ondragover="dragOver(event)"></div>
<ul id="messages"></ul>
</body>
</html>
本来であれば、入力値チェック(本当にファイルか?ファイルが1つだけか?アップロードされたファイルの形式は正しいか?)を入れる必要がありますが、今回は変更点を分かりやすくする為に、チェック処理は取り除いていますのでご注意ください。
Unreal Engine側でBase64で受け取った画像をテクスチャに変換する
WEBブラウザから送られてくる画像データをUnrealEngineが扱えるTexture2D形式に変換したいので、C++で変換処理を実装し、BPから呼び出せるようにしましょう。
このようなC++で作る汎用的な機能の実装は、Blueprint Function Libraryに実装するのが適切な為、以下の手順で変換関数を作成してみましょう。
メニューの「ツール->新規C++クラス」を選択し、”Blueprint Function Library”を選択します。
ファイル名はデフォルトの”MyBlueprintFunctionLibrary”のままいきます。
“MyBlueprintFunctionLibrary.h”は以下のようになります。
後程定義するBase64からテクスチャ2Dに変換する関数をBPから呼び出せるように定義しているだけです。(関数名はConvBase64ToImageです。)
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "IImageWrapperModule.h"
#include "IImageWrapper.h"
#include "MyBlueprintFunctionLibrary.generated.h"
/**
*
*/
UCLASS()
class UEWEBSOCKET_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Image", BlueprintPure)
static UTexture2D* ConvBase64ToImage(FString base64);
};
“MyBlueprintFunctionLibrary.cpp”は以下のようになります。
具体的な処理はコメントを参照して頂くとして、簡単に言えばBase64文字列を渡すと、UE上で使用できるテクスチャ2D型になって返ってくるだけの処理となります。
// Fill out your copyright notice in the Description page of Project Settings.
#include "MyBlueprintFunctionLibrary.h"
UTexture2D* UMyBlueprintFunctionLibrary::ConvBase64ToImage(FString base64){
// ダウンロードした画像が格納される
UTexture2D* LoadedT2D = NULL;
// Base64で受信したデータをバイナリ形式に変換する
TArray<uint8> RawImageData;
bool isDecode = FBase64::Decode(base64, RawImageData);
// バイナリから画像ファイルへの変換モジュールの定義
IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::JPEG);
// Base64のデコードが正常終了していたら
if (isDecode){
// バイナリから画像に変換できたら
if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(RawImageData.GetData(), RawImageData.Num())){
// 画像ファイルの格納エリア
TArray64<uint8> UncompressedBGRA;
if (ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA)){
// 画像の幅と高さを取得する
int32 Width = ImageWrapper->GetWidth();
int32 Height = ImageWrapper->GetHeight();
// Texture2Dのキャンバスを生成
LoadedT2D = UTexture2D::CreateTransient(Width, Height, PF_B8G8R8A8);
if (!LoadedT2D){ return NULL; }
// オンメモリで画像をTexture2Dに書き出す
void* TextureData = LoadedT2D->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
FMemory::Memcpy(TextureData, UncompressedBGRA.GetData(), UncompressedBGRA.Num());
LoadedT2D->PlatformData->Mips[0].BulkData.Unlock();
// Texture2Dを最新状態に更新する
LoadedT2D->UpdateResource();
}
}
}
return LoadedT2D;
}
これで、Base64からテクスチャ2Dへの変換が可能になりました。
残りはBPを組んでいくだけの作業です。
Quinnの着せ替えシステムの実装
UnrealエディタでBPを使ってQuinn用のマテリアルを作成し、WebSocketのイベントを拾って着替えさえる処理を実装していきます。
マテリアルは以下のように作成します。
TextureSampleノードに適当なテクスチャを指定し、ベースカラーに接続しているだけです。ただし、動的にテクスチャを切り替えたいのでTextureSampleノードをパラメータ化しています。(パラメータ名はTextureです)
この時、マテリアルの詳細タブの”Used with Skeletal Mesh”にチェックを入れるのを忘れないでください。
次にロジックを作成していきます。レベルブループリントを開いて以下のように実装します。
(こちらは全体像になります。具体的な処理は追って説明します)
それでは、処理を追ってみましょう。
レベルブループリントのBeginPlayイベントにて、MyGameInstanceSubsystemがWebSocketでメッセージを受け取った時のイベントを登録しています。(WebSocketへの接続はMyGameInstanceSubsystemのInitializeで行っているので、プレイ開始直後に接続が試みられます)
WebSocketで受け取ったメッセージに”data:image/jpeg;base64,”が含まれているかをチェックします。
Base64では先頭にファイルタイプが記載されているので、「このBase64がjpeg画像である場合」という条件分岐だと思ってください。
jpeg画像以外の場合はPrintStringで受け取った文字列を表示して終了となります。
jpeg画像だった場合、MyBlueprintFunctionLibraryで作成したBase64からテクスチャ2Dに変換する処理を行います。この時、文字列の最初についているファイルタイプのデータが不要なので、Replaceノードでファイルタイプ部分の文字列を削除しています。
最後に、作っておいたマテリアルに対してTextureパラメータとして変換したテスクチャ2D画像を渡して、プレイヤーのMeshのマテリアルとしてセットしています。これで画面上のプレイヤーのマテリアルを差し替えることができます。(かなり簡易的な差し替えですが・・・)
動作確認
これで全ての仕組みが完成しました。
プレイしてみて、Webブラウザ側でjpgファイルをアップロードエリアにドラッグ&ドロップしてみましょう。以下のように、Quinnちゃんのマテリアルが差し変わるはずです。
Azure Web PubSubの制限として、一回のメッセージ送信は1MBまでという制限がある為、ドロップする画像は700KB~800KBまでしか対応していないので、その点はご注意ください。(画像をBase64に変換している為、送信データサイズはオリジナルの画像サイズより若干大きくなってしまいます。)
また、今回は「クライアントアクセスURL」で接続していますが、実際のケースではこれを採用することはできませんので、認証周りについては別途実装が必要となります。
最後に
UnrealEngineとWebSocketの接続を検証してみました。
最近はクラウドサービスを使うことで、サーバ構築が容易に行えるようになっていることから、このようなサンプルを作る時も重宝しています。
リアルタイムで双方向通信できることで、ゲーム・ノンゲーム共に制作できる作品の幅が広がると思いますので、活用して頂ければと思います。
One response
素晴らしいブログの共有を、ありがとうございます!
WebsocketUE側からWebブラウザへ、
例えば単純な文字列を送りたい場合、
UMyGameInstanceSubsystemへ、
どのようにイベントを作成すればよろしいでしょうか?
WebSocket->Send(TEXT(“{\”type\”:\”joinGroup\”,\”group\”:\”UE\”}”));
のように、Sendメソッドを組み込んだりできるのでしょうか?