Unity multiplayer game on GameLift #3

2018.05.30

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

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

範囲

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

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

サーバーサイドアプリ実装のポイント

Unity公式チュートリアルのマルチプレーヤゲームをGameLiftに適応させるには、サーバーサイドアプリとしていくつかの通信/処理を実装する必要があります。

Launch server processにおいて必要な通信/処理

  • 1.GameLiftServiceから起動されServerSDKの初期化処理を実行する
  • 2.GameLiftServiceにProcessReadyであることを通知する
  • 3.GameLiftServiceに定期的にプロセスの状態を通知する

Start gameにおいて必要な通信/処理

  • 4.Gameのサーバーサイドアプリケーションを起動する

Add playerにおいて必要な通信/処理

  • 5.GameLiftServiceにPlayerが追加されたことを通知する

Drop playerにおいて必要な通信/処理

  • 6.GameLiftServiceにPlayerが削除されたことを通知する

Stop gameにおいて必要な通信/処理

  • 7.GameLiftServiceにGameが終了したことを通知する

Shut down server processにおいて必要な通信/処理

  • 8.GameLiftServiceにGameLiftのprocessが終了したことを通知する

1〜4までの処理はサーバー起動時に実行する、または、サーバー起動時のコールバック関数として実装する箇所。5〜8まではアプリケーション内のイベントに応じて実行する箇所となります。

実現方法は色々ありますが、この通信/処理を満たすよう設計/実装することが、ゲーム(サーバーサイドアプリ)をGameLiftに適用させるためのポイントになります。
なお、この通信/処理を実現するためにはServerSDKをUnityプロジェクトに統合する必要があります。まだ実施されいない方は下記ブログを参考に実施してください。

サーバーサイドアプリ修正

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

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

C#スクリプトの追加/修正

3つのC#スクリプトを追加/修正します。

  • GameLift.cs
  • MyNetworkManager.cs
  • PlayerController.cs

GameLift.cs

サーバーサイドアプリ起動時に実行される処理です。「1.GameLiftServiceから起動されServerSDKの初期化処理を実行する」の処理は26行目、「2.GameLiftServiceにProcessReadyであることを通知する」の処理は49行目で実施しています。コールバック関数は29行目ProcessParametersにて実装しており、第1引数がOnStartGameSession(「4.Gameのサーバーサイドアプリケーションを起動する」、第2引数がOnProcessTerminate、第3引数がOnHealthCheck(「3.GameLiftServiceに定期的にプロセスの状態を通知する」、第4引数がport、第5引数が logParametersとなります。

また、このサーバーサイドアプリでは起動時にはリスニングポートを指定できるようにしています。このように実装している理由は第5回のブログで紹介します。

GameLift.cs

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

public class GameLift : MonoBehaviour
{
	public static int listeningPort = 7777;
	void Awake()
	{
	}

	public void Start()
	{
		string[] args = System.Environment.GetCommandLineArgs();
		int len = args.Length;

		for (int i = 0; i < args.Length; i++) {
			Debug.Log(args[i]);
			if (args [i] == "-listeningPort") {
				listeningPort = int.Parse(args[i+1]);
			}
		}

		var initSDKOutcome = GameLiftServerAPI.InitSDK();
		if (initSDKOutcome.Success)
		{
			ProcessParameters processParameters = new ProcessParameters(
				(gameSession) => {
					NetworkManager.singleton.networkAddress = "localhost";
					NetworkManager.singleton.networkPort = listeningPort;
					NetworkManager.singleton.StartServer ();
                    GameLiftServerAPI.ActivateGameSession();
                },
				() => {
					GameLiftServerAPI.ProcessEnding();
                },
				() => {
                    return true;
                },
				listeningPort,
				new LogParameters(new List<string>()
				{
					"/local/game/logs"
				})
			);

			var processReadyOutcome = GameLiftServerAPI.ProcessReady(processParameters);
			if (processReadyOutcome.Success)
			{
				print("ProcessReady success.");
			}
			else
			{
				print("ProcessReady failure : " + processReadyOutcome.Error.ToString());
			}
		}
		else
		{
			print("InitSDK failure : " + initSDKOutcome.Error.ToString());
		}

	}
	void OnApplicationQuit()
	{
		GameLiftServerAPI.Destroy();
	}
}

MyNetworkManager.cs

NetworkManagerを継承したクラスです。NetworkManagerにはoerride可能なメソッドがいくつか用意されており、OnServerAddPlayerOnServerDisconnectもその一つです。
5.GameLiftServiceにPlayerが追加されたことを通知するの処理はOnServerAddPlayerメソッド(15行目)にて実装しています。この関数は、クライアントがサーバーに接続した際にサーバーサイドで呼び出されます。関数内ではクライアントサイドが送信したplayerSessionIdを取得後、AcceptPlayerSessionを実行し、GameLiftServiceにプレイヤーが追加されたことを通知します。 また、プレイヤー追加時には、connectionIdをキーにplayerSessionIdplayerSessionDataに登録し現在このゲームセッションに接続中のプレイヤーを管理します。
6.GameLiftServiceにPlayerが削除されたことを通知するの処理はOnServerDisconnectメソッド(24行目)にて実装しています。この関数は、クライアントが切断された際にサーバーサイドで呼び出されます。関数内ではconnectionIdをキーにplayerSessionIdを取得後、RemovePlayerSessionを実行し、GameLiftServiceにプレイヤーが削除されたことを通知します。

MyNetworkManager.cs

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

public class MyNetworkManager : NetworkManager {

    public static Dictionary<int, string> playerSessionData = new Dictionary<int, string>();
 
    public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader extraMessageReader)
    {
        Debug.Log("OnServerAddPlayer");
        var playerSessionId = extraMessageReader.ReadMessage<StringMessage>().value;
        GameLiftServerAPI.AcceptPlayerSession(playerSessionId);
        base.OnServerAddPlayer(conn,playerControllerId);
        playerSessionData.Add(conn.connectionId,playerSessionId);
    }

    public override void OnServerDisconnect(NetworkConnection conn)
    {
        Debug.Log("OnServerDisconnect");
        string playerSessionId = playerSessionData[conn.connectionId];
        GameLiftServerAPI.RemovePlayerSession(playerSessionId);   
        playerSessionData.Remove(conn.connectionId);
        base.OnServerDisconnect(conn);
    }

    public override void OnClientConnect(NetworkConnection conn)
    {
        #クライアントサイドで実行される処理
    }
}

PlayerController.cs

プレイヤーの操作を司るクラスです。今回はこのクラスに「プレイヤーがQを押した時にゲームを終了する」処理を追加します。7.GameLiftServiceにGameが終了したことを通知するの処理を61行目、8.GameLiftServiceにGameLiftのprocessが終了したことを通知するの処理を62行目でそれぞれ実装しています。

PlayerController.cs

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

public class PlayerController : NetworkBehaviour
{
    public GameObject bulletPrefab;
    public Transform bulletSpawn;

    void Update()
    {
        if (!isLocalPlayer)
        {
            return;
        }

        var x = Input.GetAxis("Horizontal") * Time.deltaTime * 150.0f;
        var z = Input.GetAxis("Vertical") * Time.deltaTime * 3.0f;

        transform.Rotate(0, x, 0);
        transform.Translate(0, 0, z);

        if (Input.GetKeyDown(KeyCode.Space))
        {
            CmdFire();
        }

        if (Input.GetKeyDown(KeyCode.Q))
        {
            CmdStopGame();
        }
    }

    [Command]
    void CmdFire()
    {
        var bullet = (GameObject)Instantiate(
            bulletPrefab,
            bulletSpawn.position,
            bulletSpawn.rotation);

        bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * 6;

        NetworkServer.Spawn(bullet);

        Destroy(bullet, 2.0f);
    }

    public override void OnStartLocalPlayer()
    {
        GetComponent<Renderer>().material.color = Color.blue;
    }

    [Command]
	private void CmdStopGame()
	{
		NetworkManager.singleton.StopServer ();
		GameLiftServerAPI.TerminateGameSession();
		GameLiftServerAPI.ProcessEnding();
	}

}

GameObjectの追加

空のゲームオブジェクトを追加し、先ほど作成したGameLift.csをアタッチします。

まとめ

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