こんにちは、金融ソリューション事業部の孫です。
前回の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で執筆されました)