Unity multiplayer game on GameLift #2

Unityで作成したマルチプレイヤーゲームをGameLiftで動かしてみました!!
このシリーズでは、GameLiftを利用するために必要なアプリケーション側での振る舞いをサンプルコードを交えながら紹介していきます。

ということで前回に続く、第2回の記事となります。

範囲

*の箇所が第2回の範囲です。

  • 全体構成の説明
  • *クライアントサイドアプリ実装のポイント
  • サーバーサイドアプリ実装のポイント
  • API Gateway + Lambda実装のポイント
  • GameLiftへのデプロイ
  • ゲームプレイ

クライアントサイドアプリ実装のポイント

前回も説明しましたが、GameLiftにデプロイするゲームはUnity公式チュートリアルのマルチプレーヤゲームをベースとしています。

そして、このゲームをGameLiftに適応させるには、クライアントサイドアプリ(以下、GameClient)としていくつかの通信を実装する必要があります。

Start Gameにおいて必要な通信

  • 1.GameLift Serviceにルーム作成リクエストを送信する
  • 2.GameLift Serviceよりルーム一覧を取得、表示する

Add Playerにおいて必要な通信

  • 3.プレイヤー追加の前にGameLift ServiceよりGameSessionIdを取得する
  • 4.プレイヤー追加時にGameSessionIdをサーバーに送信する

2.の通信については必須ではありませんが、多くのゲームの場合必要な処理になるかと思います。
今回、1.2.3.のメイン処理の部分はLambda関数が実行するため、GameClientはAPI Gatewayのエンドポイントに対しリクエストすることで必要な情報を取得/送信します。4.は言葉の通りです。

実現手段は色々ありますが、GameClientをこの通信を満たすよう設計/実装することが、ゲームをGameLiftに適用させるためのポイントになるかと思います。

クライアントサイドアプリ修正

ということで、実際にチュートリアルのマルチプレーヤゲームをGameLiftに適用させてみました。 実施した処理は、大きく分けて以下の2つです。

  • C#スクリプトの追加
  • GameObjectの追加

C#スクリプトの追加

4つのC#スクリプトを追加します。

  • MyNetworkManagerHUD.cs
  • MyNetworkManager.cs
  • GameInfo.cs
  • PlayerInfo.cs

MyNetworkManagerHUD.cs

ゲームのUI部分です。以下のコードをベースに必要な処理を追加しています。

先ほど説明した通信をするため、CreateGameSessionGetGameSessionsCreatePlayerSession を追加しています。また、それらを呼び出すためのボタンを配置し、必要に応じてAPI Gatewayのエンドポイントに対してリクエストを送信します。エンドポイントのURLについてはAPI Gateway作成時に置き換えます。

using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using System.Collections;

public class MyNetworkManagerHUD : MonoBehaviour
{
	private NetworkManager manager;
	private GameInfo gameInfo;

	public static string playerSessionId ;
	private string gameSessionId;
	private string playerId = "taro";
	private int offsetX;
	private int offsetY;

	void Awake()
	{
		manager = GetComponent<MyNetworkManager>();
		manager.matchName = "gamelift_room";
	}

    void OnGUI()
	{
		int xpos = 10 + offsetX;
        int ypos = 40 + offsetY;
        const int spacing = 24;

		if (!manager.IsClientConnected()){
			if (gameInfo == null)
			{
				GUI.Label(new Rect(xpos, ypos, 100, 20), "Room Name:");
				manager.matchName = GUI.TextField(new Rect(xpos + 100, ypos, 100, 20), manager.matchName);
				ypos += spacing;

				if (GUI.Button(new Rect(xpos, ypos, 200, 20), "Create Room"))
				{
					StartCoroutine (CreateGameSession (resultCreateGameSession => {
						Debug.Log(resultCreateGameSession);
					}));
				}
				ypos += spacing;


				if (GUI.Button(new Rect(xpos, ypos, 200, 20), "Find Room"))
				{
					StartCoroutine (GetGameSessions (resultGetGameSessions => {
						Debug.Log(resultGetGameSessions);
						gameInfo = JsonUtility.FromJson<GameInfo>(resultGetGameSessions);
					}));
				}
				ypos += spacing;
			} else {

				GUI.Label(new Rect(xpos, ypos, 100, 20), "Player Name:");
				playerId = GUI.TextField(new Rect(xpos + 100, ypos, 100, 20), playerId);
				ypos += spacing;

				for (int i = 0; i < gameInfo.GameSessions.Length; i++)
				{
					var room = gameInfo.GameSessions[i];
					if (GUI.Button(new Rect(xpos, ypos, 200, 20), "Join Room:" + room.Name))
					{
						gameSessionId = room.GameSessionId;
						StartCoroutine (CreatePlayerSession (resultCreatePlayerSession => {
							Debug.Log(resultCreatePlayerSession);
							PlayerInfo playerInfo = JsonUtility.FromJson<PlayerInfo>(resultCreatePlayerSession);
							manager.networkAddress = playerInfo.PlayerSession.IpAddress;
							manager.networkPort = int.Parse(playerInfo.PlayerSession.Port);

							playerSessionId = playerInfo.PlayerSession.PlayerSessionId;
							manager.StartClient();
						}));
					}
					ypos += spacing;
				}
				if (GUI.Button(new Rect(xpos, ypos, 200, 20), "Back to Match Menu"))
				{
					gameInfo = null;
				}
				ypos += spacing;
			}
		}else {
			if (GUI.Button(new Rect(xpos, ypos, 200, 20), "Stop (X)"))
			{
				manager.StopClient();
			}
			ypos += spacing;
		}

	}

	IEnumerator CreateGameSession(System.Action<string> callback) {
		WWWForm form = new WWWForm();
		form.AddField("room-name", manager.matchName);

		UnityWebRequest www = UnityWebRequest.Post("https://XXXXXXX.execute-api.ap-northeast-1.amazonaws.com/XXXX", form);
		yield return www.Send();

		if(www.isNetworkError) {
			Debug.Log(www.error);
		}
		else {
			Debug.Log(www.downloadHandler.text);
			callback.Invoke(www.downloadHandler.text);
		}        
	}

	IEnumerator GetGameSessions(System.Action<string> callback) {
		UnityWebRequest www = UnityWebRequest.Get("https://XXXXXXX.execute-api.ap-northeast-1.amazonaws.com/XXXX");
		yield return www.Send();

		if(www.isNetworkError) {
			Debug.Log(www.error);
		}
		else {
			callback.Invoke(www.downloadHandler.text);
		}        
	}

	IEnumerator CreatePlayerSession(System.Action<string> callback) {
		WWWForm form = new WWWForm();
		form.AddField("game-session-id", gameSessionId);
		form.AddField("player-id", playerId);

		UnityWebRequest www = UnityWebRequest.Post("https://XXXXXXX.execute-api.ap-northeast-1.amazonaws.com/XXXX", form);
		yield return www.Send();

		if(www.isNetworkError) {
			Debug.Log(www.error);
		}
		else {
			callback.Invoke(www.downloadHandler.text);
		}
	}
}

MyNetworkManager.cs

NetworkManagerを継承したクラスです。NetworkManagerにはoerride可能なメソッドがいくつか用意されており、OnClientConnectもその一つです。このメソッドは、クライアントがサーバーに接続した際にクライアントサイドで呼び出される処理になります。

今回はプレイヤー追加時にサーバーに対してplayerSessionIdを送信する必要があるのでClientScene.AddPlayerの第3引数にplayerSessionIdを追加します。

using UnityEngine;
using UnityEngine.Networking;
using System.Collections.Generic;
using UnityEngine.Networking.NetworkSystem; 
using Aws.GameLift.Server;

public class MyNetworkManager : NetworkManager {

    public override void OnClientConnect(NetworkConnection conn)
    {
		Debug.Log("OnClientConnect");
        if (!clientLoadedScene)
		{
			ClientScene.Ready(conn);
			var message = new StringMessage(MyNetworkManagerHUD.playerSessionId);
			ClientScene.AddPlayer(ClientScene.readyConnection,0,message);
		}
    }
}

GameInfo.cs

API Gatewayから返却されたjsonをparseするためのクラスです。GetGameSessions実行後にデータをオブジェクト化するために利用します。

using System;

[Serializable]
public class GameInfo {
    public GameSession[] GameSessions;
}

[Serializable]
public class GameSession {
    public string GameSessionId;
    public string FleetId;
    public string Name;
}

PlayerInfo.cs

API Gatewayから返却されたjsonをparseするためのクラスです。CreatePlayerSession実行後にデータをオブジェクト化するために利用します。

using System;

[Serializable]
public class PlayerInfo {
    public PlayerSession PlayerSession;
}

[Serializable]
public class PlayerSession {
    public string PlayerSessionId;
    public string IpAddress;
    public string Port;
}

GameObjectの追加

空のゲームオブジェクトを追加し、先ほど作成したMyNetworkManagerHUD.csMyNetworkManager.csをアタッチします。そして、Network InfoSpown Infoの値をチュートリアルのNetworkManagerと同じ状態にしておきます。

まとめ

GameLiftを利用するためのGameClientの実装のポイントを確認しました。次回はサーバーサイドアプリケーション実装のポイントを紹介しようと思います。