こんにちは、金融ソリューション事業部の孫です。
前回のPart1記事に続きましてPart2では、OpenMatchとAgonesを使用して、柔軟性がありスケーラブルなゲームマッチングとゲームサーバー管理システムの構築方法を詳しく説明しました。
この記事(Part3)では、UnrealEngineを利用して、オンラインマルチプレーヤーゲームのデモを完成させます。
さらに、Part2で開発したマッチメイキングサービスをUEクライアント(UnrealEngine GameClient)に統合します。
全体アーキテクチャは以下の図の通りです。

UEクライアントの実装について、前の記事で作成したデモを基にさらに拡張します。
プレーヤーマッチング機能とサーバー管理機能を追加することで、この度のクライアントの実現を目指します。
- 実施手順
- 1.前記事のデモにマッチング機能の追加
- 2. Agones SDKを呼び出してGameServerの終了機能の実装
- 3.パッケージ化したUEサーバーでAgones Fleetの作成
- 4.動作確認
- 終わりに
- 参考
実施手順
以下の順序でシステムを構築します:
1.前記事のデモにマッチング機能の追加
この前の記事UE5ネットワーク同期のC++実装例では、プレーヤーが「Play」ボタンをクリックすると、UEゲームサーバーに接続してゲーム体験を開始する機能をすでに完成させています。
次に、マッチング機能のボタンを追加し、このボタンをクリックするとOpenMatchの Frontend API を呼び出してマッチング要求をリクエストします。
マッチング機能のボタンの追加
以下の図のように、テキストエリア(Region入力用)とボタン(「Match」)をUnrealEngineのBluePrint Widget UIに配置します。

Frontend APIの呼びだす機能実装
- 以下のモジュールをxxx.Build.csファイルに追加する
今回では、Agones/HTTP/Json/JsonUtilitiesの4つのモジュールを使用します。
各モジュールは次のような役割を果たします。
//xxx.Build.cs
using UnrealBuildTool;
//:(省略)
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "EnhancedInput", "Agones", "HTTP", "Json", "JsonUtilities" });
//:(省略)
LoginHUDWidget.hを編集する
まずは、配置したWidgetsをバインドすることから始めます。
※注意:BluePrint内のWidget名は LoginHUDWidget.h ファイル内の変数名と同じでなければならず、meta = (BindWidget) タグを追加してWidgetをバインドします
それを完了したら、httpモジュールをincludeした上でコールバック関数を作成します。
//LoginHUDWidget.h
// "Http.h"ヘッダーファイルの追加
#include "Http.h"
UCLASS()
class TEST_DESERVER_API ULoginHUDWidget : public UUserWidget
{
GENERATED_BODY()
public:
//:(省略)
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget))
class UEditableText* regionName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget))
class UTextBlock* matchLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget))
class UButton* matchBtn;
private:
FHttpModule* Http;
void OnFetchGameServerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
}
LoginHUDWidget.cppにマッチングボタンのクリックロジックを実装する
//LoginHUDWidget.cpp
// "Json.h", "JsonUtilities.h"ヘッダーファイルの追加
#include "Json.h"
#include "JsonUtilities.h"
// 構造関数内で関連コントロールとHttpモジュールを初期化します
// MatchボタンにOnMatchmakingButtonClickedトリガー関数をバインドします
void ULoginHUDWidget::NativePreConstruct()
{
Super::NativePreConstruct();
//:(省略)
matchLabel->SetText(FText::FromString("Match"));
FScriptDelegate MatchmakingDelegate;
MatchmakingDelegate.BindUFunction(this, "OnMatchmakingButtonClicked");
matchBtn->OnClicked.Add(MatchmakingDelegate);
//:(省略)
Http = &FHttpModule::Get();
}
// OnMatchmakingButtonClickedトリガー関数の処理ロジックを追加します
void ULoginHUDWidget::OnMatchmakingButtonClicked()
{
statusLabel->SetText(FText::FromString("Start Matching!! Please wait"));
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> FetchGameServerHttpRequest = Http->CreateRequest();
FString frontendUrl = "127.0.0.1:8081/play/" + FString(regionName->GetText().ToString());
FetchGameServerHttpRequest->SetVerb("GET");
FetchGameServerHttpRequest->SetURL(frontendUrl);
FetchGameServerHttpRequest->SetHeader("Content-Type", "application/json");
FetchGameServerHttpRequest->OnProcessRequestComplete().BindUObject(this, &ULoginHUDWidget::OnFetchGameServerResponse);
FetchGameServerHttpRequest->ProcessRequest();
};
// Httpのコールバック関数の処理ロジックを追加します
void ULoginHUDWidget::OnFetchGameServerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
if (bWasSuccessful) {
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
if (FJsonSerializer::Deserialize(Reader, JsonObject)) {
FString IpAddress = JsonObject->GetStringField("ip");
FString Port = JsonObject->GetStringField("port");
FString LevelName = IpAddress + ":" + Port;
statusLabel->SetText(FText::FromString(LevelName));
playBtn->SetVisibility(ESlateVisibility::Visible);
}
}
}
// OnPlayGameButtonClickedトリガー関数の処理ロジックを変更します
// OpenMatchから返されたGameServerアドレスを接続ターゲットとして設定します
void ULoginHUDWidget::OnPlayGameButtonClicked()
{
//:(省略)
FString LevelName = statusLabel->GetText().ToString();
//:(省略)
}
2. Agones SDKを呼び出してGameServerの終了機能の実装
すべてのプレイヤーがオフラインになった場合、AgonesSDKを呼び出して利用済のGameServerを削除した後、新しいGameServerを再作成します。
ユーザー数の管理機能の追加
UnrealEngine Gamemodeクラスにおいて PlayerNum 変数を追加して現在のプレーヤー数を保存します。
また、少なくとも1人のプレーヤーがこのGameServerに接続したことがあることを判断するため、HasPlayerConnected のBool変数を追加します。
前の記事で、UnrealEngineによく使われるサーバー側の関数について既にいくつか紹介しました。
今回はその中のPostLogin 、Logout 関数を用いることで、プレーヤー数の簡易的な統計データが取得します。
//xxxGameMode.h
public:
//:(省略)
virtual void PostLogin(APlayerController* NewPlayer) override
virtual void Logout(AController* Exiting) override
private:
//:(省略)
int32 PlayerNum;
bool HasPlayerConnected;
//xxxGameMode.cpp
// 構造関数で変数を初期化します
xxxGameMode::xxxGameMode(){
//:(省略)
int32 PlayerNum = 0;
bool HasPlayerConnected = false;
}
void xxxGameMode::PostLogin(APlayerController* NewPlayer)
{
Super::PostLogin(NewPlayer);
PlayerNum++;
HasPlayerConnected = true;
if (!HasPlayerConnected)
{
HasPlayerConnected = true;
GetWorldTimerManager().SetTimer(CountDownPlayerNumHandle, this, &xxxGameMode::TickCount, 1.0f, true);
}
}
void xxxGameMode::Logout(AController* Exiting)
{
Super::Logout(Exiting);
PlayerNum--;
}
Agones SDKを利用してGameServerの終了実装
UnrealEngine Gamemodeクラスにおいて、Agones SDKのShutdown()関数を呼び出してGameServerがシャットダウンされます。
※Agonesは一定量のGameServerを維持し、GameServerが閉じられると新たなGameServerの生成がトリガーされます。
//xxxGameMode.h
public:
//:(省略)
virtual void Tick(float DeltaTime) override
private:
//:(省略)
//Agones SDKの処理結果に対応するレスポンス関数
//成功処理後のレスポンス関数
void HandleShutdownSuccess(const FEmptyResponse& Response);
//成功失敗時のレスポンス関数
void HandleShutdownError(const FAgonesError& Error);
void TickCount();
//xxxGameMode.cpp
void Atest_DEServerGameMode::HandleShutdownSuccess(const FEmptyResponse& Response)
{
//デモのため、実際の処理を行いません
UE_LOG(LogTemp, Log, TEXT("Game server successfully shutdown"));
}
void Atest_DEServerGameMode::HandleShutdownError(const FAgonesError& Error)
{
//デモのため、実際の処理を行いません
UE_LOG(LogTemp, Error, TEXT("shutting down failed: %s"), *Error.ErrorMessage);
}
void xxxGameMode::TickCount()
{
Super::Tick(DeltaTime);
if (HasPlayerConnected && PlayerNum <= 0)
{
FShutdownDelegate SuccessDelegate;
SuccessDelegate.BindUFunction(this, FName("HandleShutdownSuccess"));
FAgonesErrorDelegate ErrorDelegate;
ErrorDelegate.BindUFunction(this, FName("HandleShutdownError"));
GetWorldTimerManager().ClearTimer(CountDownPlayerNumHandle);
AgonesSDK->Shutdown(SuccessDelegate, ErrorDelegate);
}
}
3.パッケージ化したUEサーバーでAgones Fleetの作成
UnrealEngine Editorを使用して、Linuxプラットフォーム向けのDedicated Serverをパッケージ化します。
以下の図のように、ターゲットプラットフォームLinux-> Development -> Linux(server)を選択し、パッケージします。
※Windows OSではもしかするとターゲットプラットフォームの選択肢に Linux が存在しないかもしれません。
これは、Linuxプラットフォームのサポートが設定されていないためです。
具体的な設定方法については、こちらを参照してください。

パッケージ化が完了したら、Part2で各モジュールをデプロイしたのと同じ手順で、EKSにGameServerをデプロイします。
- ローカルにおいてDockerImageをコンパイルする
下記のDockerfileをパッケージ化の際に指定された保存先のルートディレクトリに配置し、Dockerイメージ作成コマンドを実行します。
## DockerFile
## 「AgonesOMServer」を実際のプロジェクト名に置き換えます
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y \
libxcursor1 \
libxrandr2 \
libxinerama1 \
libxi6 \
libgl1-mesa-glx \
&& rm -rf /var/lib/apt/lists/*
RUN addgroup --gid 1000 gameserver && \
adduser --gid 1000 --uid 1000 --shell /usr/sbin/nologin --home /home/gameserver --gecos "" --disabled-login --disabled-password gameserver
COPY --chown=gameserver:gameserver ./LinuxServer /home/gameserver/LinuxServer
RUN chmod -R 770 /home/gameserver/LinuxServer
WORKDIR /home/gameserver/LinuxServer
USER gameserver
EXPOSE 7777/udp
ENTRYPOINT ["/home/gameserver/LinuxServer/「AgonesOMServer」.sh"]
## Docker image build command
$ docker build -t localimage/ue_server:0.1 .
- DockerImageをAmazon ECRにアップロードする
Part2と同様に、ue_serverという名前のAmazon ECRプライベートリポジトリを新規作成します。
次に、DockerイメージをAmazon ECRにアップロードします。
$ docker tag localimage/ue_server:0.1 {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/ue_server:0.1
$ docker push {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/ue_server:0.1
- Amazon EKSにおいてAgones Fleetを作成する
ローカルでAgones Fleetの作成用のfleet.yamlファイルを作成します。
## fleet.yaml
---
apiVersion: "agones.dev/v1"
kind: Fleet
metadata:
name: fleet-ap-northeast-1
spec:
replicas: 2
scheduling: Packed
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
template:
metadata:
namespace: meta-poc
labels:
region: ap-northeast-1
spec:
players:
initialCapacity: 4
ports:
- name: default
containerPort: 7654
health:
initialDelaySeconds: 30
periodSeconds: 60
template:
metadata:
namespace: meta-poc
labels:
region: ap-northeast-1
spec:
containers:
- name: ue5-server
image: {AWS ACCOUNT ID}.dkr.ecr.{AWS REGION}.amazonaws.com/ue_server:0.1
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1"
fleet.yamlファイルをAmazon EKSに適用します。
$ kubectl apply -f fleet.yaml
4.動作確認
- UnrealEngine Editorを使用して4つのクライアントを起動する
- 具体的な操作手順は、以下の図のとおり
New Editor Window(PIE)->Number Of Players: 4->Play Standaloneを設定する
- 具体的な操作手順は、以下の図のとおり

- 以下のコマンドでAgones GameServerのステータス監視を起動する
$ watch kubectl get gs
- OpenMatch Frontend Serviceをローカルにマッピングする
# OpenMatch Frontend サービスがローカルの8081ポートにマッピングされます kubectl port-forward services/frontend-ednpoint 8081:80
- マッチング機能をテストする
- 確認ポイント:
- 4つのクライアントが同じGameServerアドレスにマッチングされていること
- GameServerのステータスがAllocatedになっていること
- 確認ポイント:


- DedicatedServerへの接続をテストする
- 確認ポイント:
- Playボタンをクリックした後、Dedicated Serverに成功したこと
- Characterの頭上に表示されているNickNameが、クライアントが入力したNickNameと同じであること
- 確認ポイント:

- AgonesによるGameServerのシャットダウンをテストする
- 確認ポイント:
- 全てのGameClientを閉じた後、以前Allocated状態だったサーバーが削除されること
- その代わりに新たにReady状態のGameServerが作成されること
- 確認ポイント:



終わりに
これで、マッチング機能を備え、AgonesでUnreal Engine DedicatedServerをスケジューリングするオンラインマルチプレイヤーゲームの作成が完了しました。
この一連の記事では、EKSを使ってKubernetesの特性を活用し、Unreal Engine のDedicated Serverを管理する方法について深く探求しました。
その中で、マッチングシステムとしてOpenMatchを使用し、その強力な拡張性と設定性を活用してニーズに合ったマッチングルールをカスタマイズしました。同時に、Agonesを用いてゲームサーバーのスケジューリングを行い、効率的で安定したサーバー管理を実現しました。
次は、さらなるゲームに関連するインフラ設計のソリューションを見つけ出し、ゲーム体験をさらに向上させる方法を継続に探求します。
現在ISIDはweb3領域のグループ横断組織を立ち上げ、Web3およびメタバース領域のR&Dを行っております(カテゴリー「3DCG」の記事はこちら)。
もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください!
私たちと同じチームで働いてくれる仲間を、是非お待ちしております!
ISID採用ページ(Web3/メタバース/AI)
参考
- https://docs.unrealengine.com/5.2/ja/linux-game-development-in-unreal-engine/
- https://agones.dev/site/docs/reference/fleet/
執筆:@chen.sun、レビュー:@yamashita.yuki
(Shodoで執筆されました)



