前回の記事の続きです。

前回はややこしいサンプルを動作させるだけで終わりましたが、今回はもう少し実践的な内容として、MLAPIアプリにGameLiftを統合してみたいと思います。
GameLift統合の要点さえわかれば、UNETやMirrorでも大体同じ要領でできると思います。

記事のシリーズとしては一旦この記事で終わりです。ここまでやれたら後はどうとでもなると思うので。
記事の続きを書きたくても、とくにまだこの記事以上の事を自分でやれてないので、もっと色々やってネタが出来たら続きを書くかもしれません。

さて、MLAPIにはまだ有用なサンプルプロジェクトがほとんど存在しません。今回はUnityテクノロジーズジャパンの黒河さんがUnityステーションの動画用に用意してくださっている、ユニティちゃんをプレイヤーキャラとして操作できるマルチプレイヤーゲームサンプルを使用させていただきたいと思います。

↓サンプルのGitHubリポジトリはこちらです。

https://github.com/wotakuro/MLAPI_UnitychanSample

こちらのサンプルを改造して、GameLiftを統合していきます。
サンプルの説明についてはGitHubのREADMEを参照してください。
今回の記事ではGameLiftの統合に焦点を当てるため、MLAPI自体についての説明はしません。MLAPIについては上にリンクを張ったUnityステーションの動画の説明が参考になると思います。

GameLift統合サンプルを動かしてみる

私の方で、すでにMLAPIサンプルをフォークしてGameLiftを統合したプロジェクトを用意しました。

https://github.com/umiyuki/MLAPI_UnitychanSample

元のサンプルでは普通のネットワーク接続またはリレーサーバ接続だけが可能であり、”Hostとして起動”と”Clientとして起動”の2つのボタンが用意されていましたが、私が改造したサンプルでは”GameLift接続”ボタンが追加されています。このボタンを押せばGameLiftのサーバに接続できます。

統合を実現した具体的なコードの説明は後に回すとして、ひとまずこの統合サンプルの実行方法を説明します。

サンプルの実行環境としては、前回の記事で書いた、GameLiftのUnityサンプルが実行できる環境であることが前提です。
つまり、AWSCLIがインストールされていて、deployプロファイルやdemo-gamelift-unityプロファイルの資格情報がPCにインストールされている必要があります。

では、上記リンク先のリポジトリをクローンして、Unity(2019.4.11を使ってますが、2019.4以降なら多分動く)で開いてください。

ゲームサーバーをビルド

GameLiftにアップロードする、ゲームサーバーをビルドします。

PlayerSettingsのScripting Define Symbolsで”SERVER“を設定します。

Build SettingsからターゲットプラットフォームをLinuxにして、Server Buildにチェックを入れます。

ビルド先のフォルダは、Unityプロジェクトのディレクトリに”build_server_linux”というフォルダを作ってそこにMLAPITest.x86_64という名前でビルドします。(他のフォルダや実行ファイル名にしても構いませんが、その場合はこの後の説明を適宜読み替えてください)

ゲームサーバーをGameLiftにアップロード

ビルドしたゲームサーバーをGameLiftにアップロードして、クライアントから接続できるように設定しましょう。

手動でゲームサーバーをアップロードする場合、残念ながらブラウザのダッシュボードからはできません。AWS CLIから行う必要があります。
プロジェクトフォルダに”upload_gamelift_linux.bat“というバッチファイルを入れておいたので、これを実行すればアップロードできます。

バッチファイルの中身は、

これは、意味としては「GameLiftにゲームサーバーをアップロードしてください。OSはLinux、アップロードするフォルダは”build_server_linux”、ビルド名は”MLAPI Test”、バージョン名は”build 2″、リージョンは”ap-northeast-1″(東京)、使用するプロファイル名は”deploy”」みたいな感じです。必要に応じて引数は変更してください。

アップロードに成功したら、ブラウザからawsのGameLiftのダッシュボードを開いてください(リージョンは東京)。ビルドがアップロードされてる事が確認できると思います。

ビルドがアップされていても、フリートとエイリアスが無いとクライアントから接続できないので、作成する必要があります。

フリートから作成しましょう。

ビルドを選択して、”アクション”→”ビルドからフリートを作成”をクリックします。

”フリートの詳細”の”名前”の欄に、分かりやすい名前を設定します。
”プロセス管理”の”起動パス”の欄には、アップロードしたフォルダ内のゲームサーバーの実行ファイル名を記入します。

”同時プロセス”の欄で一つのEC2インスタンスで最大何個までサーバーを起動するかを設定できますが、とりあえず1でいいです。

EC2 ポート設定“でポート開放設定を行います。今回のプロジェクトでは1935番のポートを使用しているので、”ポート範囲”は1935、プロトコルはUDP、IPアドレス範囲は0.0.0.0/0(全部有効)を設定します。

これで”フリートの初期化“をクリックすればフリートが作成されます。

フリートの状態ですが、”初期化中”とか”アクティブ化中”とかになって、最終的に準備完了すれば数分で”アクティブ”になります。サーバーがバグってると”エラー”とかになっちゃいます。

最後にエイリアスを作成しましょう。(フリートがアクティブになるのを待つ必要はありません)

”エイリアスの作成”をクリックして、適当にエイリアス名を付けて、”関連付けられたフリート”で先ほど作成したフリートを選択して、”エイリアスの設定”をクリックすればエイリアスが作成されます。

ビルドとフリートとエイリアスが作成出来たら、GameLiftの設定はOKです。クライアントから接続できる状態になります。

今回は手順を理解するために手動で設定しましたが、手作業だと面倒くさいしミスが起きたりするので、ゆくゆくは自動でアップロードからビルド、フリート、エイリアス作成まで行えるようにした方がいいでしょう。

前回の記事で使ったUnityサンプルに自動デプロイツールのVisualStudioプロジェクトが同梱されているので、あれを弄れば自動デプロイができると思われます。

エディタからGameLiftに接続してみる

とりあえずエディタからGameLiftに接続してみましょう。

ソースコードに接続先エイリアスIDを設定する必要があります。ダッシュボードから自分のエイリアスのIDを確認して、GameLift.csの392行目を書き換えてください。

Scripting Define Symbolsで”SERVER”を消して、”CLIENT“を追加します。

この状態でプレイモードに入って”GameLift接続”ボタンを押すと、GameLiftサーバに接続できます。

無事にGameLiftサーバに接続できました!やったぜ。

このままだと、エディタ上では1つしかゲームを起動できないので、ビルドしないとマルチプレイのテストができません。ParrelSyncを使えば一つのUnityプロジェクトを複数のエディタで起動できるので、ビルドしなくてもマルチプレイのテストができます。

↓ParrelSyncの解説についてはこちら

ParrelSyncを使ってUnityのネットワークマルチゲーム開発を倍速で進めよう

クライアントをビルドする

クライアントのゲームアプリをビルドしてみましょう。

エディタからGameLiftに接続できる状態になってれば、あとはBuildSettingsでターゲットプラットフォームをWindowsにして、Server Buildのチェックを外してビルドすればOKです。

エディタの時と同様にGameLiftに接続できるはずです。
クライアントはいくつでも起動できるので、好きなだけの人数のマルチプレイをテストできます。

しかし、複数のクライアントを起動しても、操作できるのは自分ひとりなので、自分のキャラしか動かせなくて、マルチプレイしてる感が無いですよね。

そんな時はダミークライアント機能が使用できます。

ダミークライアントを実行する

MLAPIサンプルは、バッチモードで起動すると自動でランダム操作するダミークライアントとして立ち上がるという便利な機能があります。

私が改造したサンプルでは、ダミークライアントは強制的にGameLiftサーバに接続するようにしてます。

↓このような内容でバッチファイルを作成してクライアント実行ファイルと同じフォルダに保存して、実行してください。

バッチファイルを実行する度にダミークライアントが立ち上がります。ダミーのユニティちゃんはランダムに歩き回ります。

ダミーと言えども入力をシミュレートしてサーバに送信しているので、サーバ負荷などは人間がプレイした時と同様の条件でテストができます。

大量のクライアントで接続してみる

MLAPIとGameLiftの組み合わせで、何人くらいまで接続できるもんなのでしょうか?

ダミークライアントを大量に起動してテストしてみました。
GameLiftのEC2インスタンスはデフォルトで設定されてるc5.largeを使用しました。

うじゃうじゃ…

大体これで70人です。
ちょっとだけカクカクし始めてます。
サーバー的にはまだ余力があるのかもしれませんが、見ての通り、1台のPCで大量のダミークライアントを立ち上げているため、クライアントPC側のCPU、メモリ、GPUの方が一杯いっぱいになってきたのでこのくらいで止めておきました。

まあ70人で同じ部屋に接続出来たら十分満足な気がしますね。

MLAPIサンプル改造の流れ

GameLift統合サンプルの中身を解説…というよりどのような流れで元のサンプルにGameLiftを統合していったのかを説明します。

流れが分かれば自身のプロジェクトへの統合方法やMirrorやUNETの場合での応用も理解できると思います。

GameLiftのUnityサンプルからコードとDLLをコピーしてくる

まず最初に、前回使ったGameLiftのUnityサンプルからソースをコピーしてMLAPIサンプルプロジェクトに持ってきます。

持って来るのはGameLift.csとCredentials.csの二つです。GameLogic.csは要りません。

そして、Pluginsフォルダの中にあるdllファイルも全部コピーしてきます。

GameLift.csはほぼこのままでも使えるのですが、いくつかエラーが出てしまってるので修正します。

GameLift.csの変更

GameLogicクラスに依存してる箇所がちょっとあるので、コメントアウトします。gl.gameliftStatusを参照してる箇所については、GameLiftクラスにgameliftStatus変数を追加してそっちを参照するように変更します。

また、サーバーをアップロードするリージョンを東京リージョンに変更していたので、参照するリージョン先も東京リージョンに変更します。399行目です。

今の設定だと部屋に4人までしか入れないので、とりあえず100人まで入れるようにしてみます。513行目です。

そして、ひとつ重要な機能追加を行います。GameLiftにUnityログを収集させて後からログファイルを確認できるようにする機能です。GameLiftでは基本的にはEC2インスタンスは直接見れないので、サーバーで何が起こってるか確認するにはログを見るしかありません。地味ですがデバッグ的には最重要機能です。154行目あたりに追加してます。

あとはこのAwake()を呼ぶようにしたり、LogParametersでこのログファイルを指定したりします。

ConfigureConnectionBehaviour.csの変更

GameLiftサンプル側の変更が終わったので、MLAPIサンプル側を変更してGameLift接続に対応できるようにします。

まず、ConfigureConnectionBehaviour.csをちょっと変更します。

ゲームサーバーの待ち受けポートを指定するために、serverPortフィールドを追加します。デフォルトで1935番にしているのは、GameLift.csでゲームサーバーとGameLiftサービス間の通信にも1935番が指定されてるので、どうせなのでゲームサーバーとクライアント間の通信も同じ1935番使っとくかという事です。

GameLift接続ボタンとGameLiftクラスの参照用フィールドも追加しときます。

GameLift接続ボタンを押したときのメソッドも実装します。

GameLiftクラスのGetConnectionInfo()を呼んで、サーバのIPアドレスとポート番号を取得してそこに接続する感じです。

また、ダミークライアント起動時にもGameLiftへ接続するように変更します。

そして、NetworkUtility.GetLocalIP()を呼んでる箇所がありますが、このメソッドはGameLift上のゲームサーバーから呼ぶと何故かエラーが出るので、呼ばないように変更します。

ServerManager.csの変更

最後に、ServerManager.csを変更します。

まず、GameLiftを参照するフィールドを追加します。

そして、クライアントの承認機能を追加します。
クライアントの承認ってなんだ?という話ですが、MLAPIもそうですが、デフォルトではクライアントは無条件でサーバーに接続できます。

つまり、サーバーのIPとポート番号さえ知ってればいくらでも不正に接続出来ちゃいます。

サーバーにクライアント承認機能があれば、接続してきたクライアントが正当な資格を持ってるか検証して、不正なクライアント接続は弾く事が出来ます。
MLAPIはクライアント承認機能を利用可能です。

https://mlapi.network/wiki/connection-approval/

どういう仕組みかと言うと、クライアントはサーバに接続要求を出すときに、任意のデータ(byte[])をサーバに渡せます。サーバ側はApprovalCheck()でそのデータを見て正しいクライアントかどうかをチェックできます。

何のデータを渡せばいいの?と言うと、GameLiftではクライアントがプレイヤーセッションを生成した時に、GameLiftサービスからプレイヤーセッションIDをもらえます。このIDを渡せばOKです。すでにConfigureConnectionBehaviour.csのOnClickGameLiftConnect()でそのように実装しています。

サーバ側ではGameLift.ConnectPlayer()を使って、受け取ったプレイヤーセッションIDが正当かどうかをGameLiftサービスに問い合わせができます。間違いなければ接続承認してあげます。89行目あたりから実装してます。

接続時のプレイヤーセッションの追加が実装されたので、次は切断時にプレイヤーセッションの終了を通知する部分も作ります。136行目あたりからです。

ところで、今の時点では、一旦始まったゲームセッションは永遠に終了しません。ゲームの終了条件を特に作ってないからです。一応、ゲームサーバーの切断ボタンが押されたらゲームセッションを終了するようにしておきます。まあ、ヘッドレスサーバーなのでボタンは押せませんが一応。

ソースの変更はこれでおしまいです。お疲れさまでした。

Unityシーンの変更

変更したソースが正常に動作するように、Unityエディタでシーンを調整します。

まず、空のゲームオブジェクトをシーンにおいて、GameLiftクラスをアタッチします。
そして、インスペクタからServerManagerとConfigureConnectionBehaviourのGameLiftフィールドにこのオブジェクトを突っ込みます。

次に、接続UI上にGameLift接続ボタンを作成して、ConfigureConnectionBehaviourのConnectGameLiftButton参照に突っ込みます。

そして、MLAPIのクライアント承認機能を有効化するために、NetworkManagerのConnection Approvalにチェックを入れます。

以上で完成です。細々としたところが抜けてるかもしれませんが、詳細は改造サンプルの方を参照してください。

オマケ その他のGameLift情報

MLAPI統合の説明は以上で終了ですが、せっかくなのでGameLift使ってて知ったTipsみたいなのも書いときます。

テストが済んだらフリートを消しておくべし!

実際、何よりも重要な話ですが、GameLiftのフリートを作りっぱなしで放置しておくと、EC2インスタンスが起動し続けて(デフォルトだと誰も接続しなくても最低1つのインスタンスが常時起動してる設定になってるハズ。そうじゃないとプレイヤーがいつ接続してくるか分からないので)どんどん課金が発生してしまいます。

テストなどが済んだらフリートを全部消すか、手動でフリート内のインスタンス数をゼロに設定しておく必要があります。

ちなみに”ビルド”があるだけでもS3料金がかかります。まあこちらは微々たる金額だと思いますが。

チュートリアルを途中まで進めただけで数万円の支払が発生したAWS

GameLiftの無料枠は月当たり150時間しかありません。

ゲームセッションを強制終了させる裏技

GameLiftでは、ゲームセッション単位でログをダウンロードできます。逆に言えば、ゲームセッションが終了しない限り、ログを見れません。そして、今回作成したサンプルではゲームセッション終了処理を入れてないので、永遠にゲームセッションは終了しません。つまり、ログが見れないので、ゲームサーバーのデバッグが不可能です。

これは困りましたね。

しかし、実はフリートのゲームセッションを強制終了させる裏技があります。
フリートを開いて”スケーリング”タブから”最大インスタンス数の設定”と”手動で目的のインスタンス数を調整する”の数値を両方ゼロに設定しましょう。すると、しばらくするとゲームセッションが強制終了されます。

ゲームセッションが終了してこれまたしばらく経つと、ログが収集されてログファイルがダウンロードできるようになります。(強制終了でなく、正常にゲームセッション終了した場合はすぐにログダウンロード可能になります)

ちなみに当然ですが前述したようにプログラム内でUnityログをGameLiftが収集するように設定してないとログは空です。

先ほどゼロにした二つの数値を1に戻せばフリートのインスタンスが復活します。ちなみにゼロにしておけばEC2インスタンスがゼロになるのでGameLiftの料金がかからない状態になるハズです。

どうしてもフリートのEC2インスタンスに直接アクセスしたい

「サーバーに繋がらない理由が分からない!ログを見ても分からない!どうしても直接EC2がどうなってるか見たい!」という時もあるでしょう。

そんな時は、手間ですが一応EC2にアクセスする方法はあります。

↓こちらに方法が書いてます。

https://docs.aws.amazon.com/ja_jp/gamelift/latest/developerguide/fleets-remote-access.html

まず、AWS CLIを使用して、describe-instancesコマンドでフリート内のインスタンスIDをゲットできます。

次に、get-instance-accessコマンドでインスタンスにアクセスするための秘密鍵をゲットできます。

その秘密鍵をpemファイルとして保存すれば、sshコマンドで秘密鍵を使って目的のEC2インスタンスに入れます。

また、フリートの設定からsshで使用するポートを解放しておく必要もあります。

手間がかかるので最後の手段として使いましょう。

エイリアスを駆使したサーバ運用

実際にGameLiftを使ってサーバを実運用する場合、どんな感じになるのでしょうか?

色々なパターンを想定して考えてみようと思います。

①ゲームが2種類になった時

GameLiftで別々の2種類のゲームを運用する事になった時は、まあそれぞれのゲームに対してビルド、フリート、インスタンスをそれぞれ用意してあげればいいと思います。

②クライアントとサーバを両方更新する時

Unityでヘッドレスサーバを使ってサーバ運用する場合、大抵はクライアントを更新したらサーバも更新するハメになると思います。

まず、新しいサーバビルドをGameLiftにアップロードしましょう。
次に、新しいビルドのフリートを作成します。新しいエイリアスも作ります。

そして、新しいクライアントからは、新しいエイリアスを参照するようにプログラムから設定します。そして、アプリストアのクライアントを更新しましょう。

これで更新完了ですが、古いフリートを建てておくのはサーバ代の無駄なので消したいです。
実はエイリアスには、フリートをリンクする代わりにメッセージを送信する機能もあります。ですので、古いエイリアスからフリートへのリンクを外して、「クライアントを更新してください」みたいなメッセージを設定しましょう。

こうすれば古いクライアントからはサーバに接続できなくなり、アプリ更新が促されます。

これで、しばらくしてプレイヤーが誰もいなくなった古いフリートを削除できます。

③クライアントだけを更新する場合

クライアントだけを更新したい場合は、GameLift側は特に何も更新する必要ないはずです。
しかし、別々のバージョンのクライアントが同じサーバに接続してマルチプレイするのってなんか問題起きそうで気持ち悪いです。

新しいクライアント用には新しいフリートとエイリアスを用意して、マルチプレイでクライアントが混ざらないようにした方がいいような気がします。

④サーバだけを更新する場合

クライアントは更新せずにサーバだけを更新する場合は、とりあえず新しいサーバビルドをGameLiftにアップして、新しいフリートも作成します。

それからエイリアスの向き先を古いフリートから新しいフリートに変更しちゃいます。
そうすれば、その後のゲーム試合は新しいフリートで始まりますし、現在すでに始まってる試合は引き続き古いフリートで続行されます。

しばらく経てば古いフリートのプレイヤーはいなくなるはずなので、古いフリートを削除できます。

こういう感じでGameLiftではダウンタイム無しのサーバ更新が可能です。