Amazon GameSparks(Preview)のMessage機能を使ってゲームの実績機能を作ってみた

2022.10.20

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ゲームのバックエンド開発を効率化するためのAWSサービスとして、Amazon GameSparksが現在プレビュー版で公開されています。
今回は、GameSparksで用意されている機能の1つであるMessage機能でクライアントとゲームサーバ間でどのような種類の通信が可能かについて解説し、実際にUnityで動作する様子もご紹介します。

なお、サービスの概要については以下の記事をご参照ください。

Message機能について

公式ドキュメントでは以下のように記載されています。

  • ゲームクライアントとGameSparks上に構築されたゲームサーバ環境の間で、WebSocketによるメッセージ送受信を行う。
  • 各メッセージには任意のデータ形式を定義できる。
  • メッセージの受信を起点に、ゲームサーバ上にあらかじめ書いたコードでゲームのロジックをサーバ内で実行することが可能。
  • コードは2022年10月現時点ではJavaScript(ES5.1)のみ対応している。

クライアントとサーバのリアルタイム通信のコードを自前で作成するには、専門的な知識と大きな工数が必要になります。
しかし、GameSparksのMessage機能を使用すれば、通信のためのコードは自動的に生成されるため、後はそれをUnityに組み込むだけで双方向通信が実現できるようになります。

Messageの種類

GameSparksで扱うMessageは3種類あります。図に表すと下記のようにそれぞれ役割が異なります。

Event

  • クライアントからバックエンドへの応答不要のメッセージです
  • 任意でクライアントにNotificationを送ることもできます

Requests

  • クライアントが何らかのレスポンスを要求するための送るメッセージです
  • Requesetsを受けたゲームサーバは、クライアントに対しレスポンスを返します
  • こちらも任意でクライアントにNotificationを送ることができます

Notification

  • EventやRequestsを受け取った際に、サーバがライアントへメッセージを送る機能です

尚、上述した通り各メッセージにはデータ形式の定義を設定できます。例えば、クライアントにPlayer情報を送りたい場合、以下のように設定します

名前
name String
level Integer

プリミティブ型としてはString, Integerなど定番の型は一通り揃っており、ListやMapなども使用可能です。

設定したデータ形式はUnityで使用するために生成されるC#コードに反映されるため、クライアントとサーバでの仕様の相違の発生を防ぐことができます。
詳細は公式ドキュメントをご確認ください。

Unityで使ってみる

それでは、実際にMessage機能を使用してUnityで開発したゲームクライアントとGameSparksのゲームサーバ間で双方向通信を行ってみましょう。

以降の解説で使用しているUnityのバージョンは2020.3.40f1です。Unityの最新のLTSは2021.3.11f1ですが、GameSparksのDeveloper Guideでバージョンを指定されているため、合わせてあります。

尚、プロジェクトの作成手順やMessagの設定手順についてはこちらの記事などをご参照ください。

作っていくのはよくある実績機能です。特定の敵を一定回数以上倒すなどの条件を達成したプレイヤーに対し、トロフィーなどの報酬を与える、といった機能ですね。
今回は、画面に表示されているクッキーを5回以上クリックしたら実績を解除という非常に簡単な条件で作ってみます。

全体の構成としては、以下のとおりです。

  • ゲームクライアントは、プレイヤーがクッキーをクリックする度にゲームサーバへその情報を渡す
  • ゲームサーバは、クライアントからクリックの情報が届くたびに合計クリック数を加算する
  • ゲームサーバは、ゲームクライアントから実績達成状況の取得リクエストが来たらその情報を返す
  • ゲームサーバは、合計クリック数が実績解除の条件を達成したらゲームクライアントへ通知する

実際に使われている実績機能でプレイヤー毎の進捗状況をわざわざサーバで管理しているかは分かりませんが、今回は不正防止を目的にゲームサーバ側で情報管理をする仕様ということにします。

Eventの作成

Event Messageを使用して以下の機能を実装します。

  • ゲームクライアントは、プレイヤーがクッキーをクリックする度にゲームサーバへその情報を渡す
  • ゲームサーバは、クライアントからクリックの情報が届くたびに合計クリック数を加算する

まず、GameSparks上でClickedEventを作成します。イベント通知ができれば良いので、データ形式はフィールド無しにします。
次に、ClickedEventで呼ばれるハンドラとしてClickHandlerを作成し、クライアントから通信が届いたら以下のコードが実行されるよう設定します。

GameSparks().Logging().Debug("クリックイベントハンドラ起動");

let data = GameSparks().CurrentPlayer().GetData(["Clicked"]);

// 値が未定義の場合初期化
if (data.Clicked === undefined) {
  data.Clicked = 0;
}

data.Clicked++;

GameSparks().CurrentPlayer().SetData(data);

GameSparks上に保存されている合計クリック数データを取得し、1回分加算して再度保存するというコードです。GameSparksには、クライアント(Player)ごとにデータを保管しておく機能があるため、それを利用しています。

次に、Unityで使用するコードを作成します。

using Amazon.GameSparks.Unity.Editor.Assets;
using Amazon.GameSparks.Unity.Generated;
using UnityEngine;

namespace Amazon.GameSparks.Unity.Samples.UsingScriptableObjects.Scripts
{
    public class Cookie : MonoBehaviour
    {
        [SerializeField]
        private ConnectionScriptableObject connectionScriptableObject = default;
        
        private void OnMouseUp()
        {
            var clickEvent = new AchievementTestOperations.Clicked();
            
            Debug.Log("クリックイベント実行");
            
            connectionScriptableObject.Connection.EnqueueClicked(
                clickEvent,
                error => {Debug.LogError("クリックイベント失敗: " + error);}
            );
        }
    }
}

マウス操作によりOnMouseUp()が実行されることで、GameSparksへイベントを飛ばすコードです。

ConnectionScriptableObject型のフィールドは、GameSparksと通信するためのものです。通信を行うには、Unity上で専用のGame Objectを作成してアタッチする必要があります。詳細な設定方法については公式ドキュメントのチュートリアルに記載してあります。

ゲームサーバとの通信は、GameSparksで自動生成されたC#コード内のクラスを使って行います。
OnMouseUp()メソッド内のnew AchievementTestOperations.Clicked()は、GameSparksが作ったClickedクラスのインスタンスを生成しています。ClickedクラスはGameSparksとの通信時に使うDTOです。
GameSparksでEventのフィールドを設定していた場合はインスタンス生成時に値を渡すことになりますが、ClickedEventはフィールドが無いため引数無しで生成しています。
このインスタンスを次のEnqueueClicked()に渡して実行することで、GameSparksのClickedEventへ通信が送られる仕組みになっています。

作成したC#コードをUnity上のクッキーオブジェクトにアタッチすることで、クッキーのクリックを起点にする一連の処理が完成します。

Requestの作成

Request Messageを使用して以下の機能を実装します。

  • ゲームサーバは、ゲームクライアントから実績達成状況の取得リクエストが来たらその情報を返す。

Eventの時と同じように、GameSparksでGetAchievementScoreRequestを作成します。こちらもリクエストしたことがサーバに届くだけで良いので、リクエストのデータ形式はフィールド無しにします。
一方で、レスポンスについては決まった形でクライアントへデータを送りたいので、以下のデータ形式を設定します。

名前
title String
description String
currentClick Integer
targetClick Integer
isUnlock Boolean

次に、GetAchievementScoreRequestで呼ばれるハンドラとしてRequestHandlerを作成し、以下のコードが実行されるよう設定します。

GameSparks().Logging().Debug("実績達成状況確認リクエストハンドラ起動");

const TARGET_CLICK = 5;

let data = GameSparks().CurrentPlayer().GetData(["Clicked"]);

// 値が未定義の場合初期化
if (data.Clicked === undefined) {
  data.Clicked = 0;
}

let isUnlock = false;
if(data.Clicked >= TARGET_CLICK) {
  isUnlock = true;
} 

return GameSparks().Messaging().Response(
  {
    "title": "クリッカー初級者",
    "description": "クッキーを5回クリックする",
    "currentClick": data.Clicked,
    "targetClick": TARGET_CLICK,
    "isUnlock": isUnlock
  }
);

GameSparks上に保存されている合計クリック数データを取得し、それを実績名、実績説明文、実績達成に必要なクリック数、実績達成状況と共にクライアントへレスポンスを送るというコードです。実績達成状況(isUnlock)については、達成しているかどうかを途中で判定して、返す値を決めています。

次に、Unityで使用するコードを作成します。

using System;
using Amazon.GameSparks.Unity.DotNet;
using Amazon.GameSparks.Unity.Editor.Assets;
using Amazon.GameSparks.Unity.Generated;
using UnityEngine;
using UnityEngine.UI;

namespace Amazon.GameSparks.Unity.Samples.UsingScriptableObjects.Scripts
{
    public class ReloadButton : MonoBehaviour
    {
        [SerializeField]
        private ConnectionScriptableObject connectionScriptableObject = default;

        [SerializeField]
        private Text title;
        [SerializeField]
        private Text description;
        [SerializeField]
        private Text status;
        [SerializeField]
        private Text unlocked;

        public void OnClick()
        {
            var getAchievementScoreRequest = new AchievementTestOperations.GetAchievementScoreRequest();
            
            Debug.Log("実績達成状況確認リクエスト実行");

            connectionScriptableObject.Connection.EnqueueGetAchievementScoreRequest(
                getAchievementScoreRequest,
                HandleGetAchievementScoreResponse,
                error => {Debug.LogError("実績達成状況確認リクエスト失敗");},
                () => { Debug.LogError("リクエストタイムアウト");},
                TimeSpan.FromMinutes(2)
            );
        }
        
        private void HandleGetAchievementScoreResponse(
            Message<AchievementTestOperations.GetAchievementScoreResponse> response)
        {
            title.text = response.Payload.title;
            description.text = response.Payload.description;
            status.text = response.Payload.currentClick.ToString() + "/" + response.Payload.targetClick.ToString();
            unlocked.gameObject.SetActive(response.Payload.isUnlock);
            
            Debug.Log("実績達成状況確認リクエスト完了");
        }
    }
}

マウス操作によりOnClick()が実行されることでGameSparksへリクエストを飛ばし、レスポンスが返ってきたらそれをゲーム画面に表示するためのText Game Objectへ格納するというコードです。

基本的な流れはEventの時と変わりません。
ConnectionScriptableObject型のフィールドは、Eventの時と同様の使い方です。
GameSparksが作ったGetAchievementScoreRequestというDTOのインスタンスを生成し、それをEnqueueGetAchievementScoreRequest()に渡して実行することでGameSparksのGetAchievementScoreRequestへ通信が送られます。
EnqueueGetAchievementScoreRequest()の引数には、レスポンス時に実行するメソッドも渡す必要があり、それが取得したデータを各Text Game Objectへセットする処理を書いているHandleGetAchievementScoreResponse()です。

作成したC#コードをUnity上の「実績情報取得」ボタンにアタッチし、更に各Game ObjectをC#スクリプトにアタッチすることで、ボタンを押したらゲームサーバから取得した実績取得情報を表示する処理が完成します。

Notification

最後に、Notification Messageを使用して以下の機能を実装します。

  • ゲームサーバは、合計クリック数が実績解除の条件を達成したらゲームクライアントへ通知する

EventとRequest同様、GameSparksでAchievementUnlockedNotificationを作成します。クライアントへ送る通知のデータ形式は以下の通りに設定します。

名前
title String

次に、Notificationを送信するコードを実行するハンドラとして、既に作成しているClickedEventに新しくUnlockedNotificationHandlerを作成します。これにより、ClickedEventを受け取った際に以下のコードを実行するようになります。

GameSparks().Logging().Debug("実績解除通知イベントハンドラ起動");

let data = GameSparks().CurrentPlayer().GetData(["Clicked"]);

// 値が未定義の場合初期化
if (data.Clicked === undefined) {
  data.Clicked = 0;
}

if(data.Clicked >= 5) {
  GameSparks().CurrentPlayer().SendNotification(
    "Custom.Game.AchievementUnlocked",
    {
      "title": "クリッカー初級者",
    }
  );
}

GameSparks上に保存されている合計クリック数が実績解除の条件(5回以上)を超えていたら、クライアントへ解除した実績の名前が入った通知を送るというコードです。

次に、Unityで通知を受け取るためのコードを作成します。

using System;
using Amazon.GameSparks.Unity.Editor.Assets;
using Amazon.GameSparks.Unity.Generated;
using UnityEngine;
using UnityEngine.UI;

namespace Amazon.GameSparks.Unity.Samples.UsingScriptableObjects.Scripts
{
    public class AchievementUnlockedNotification : MonoBehaviour
    {
        [SerializeField]
        private ConnectionScriptableObject connectionScriptableObject = default;

        [SerializeField] private Text notification;

        private IDisposable subscription;

        private void Awake()
        {
            subscription = connectionScriptableObject.Connection.SubscribeToAchievementUnlocked(OnUnlockedNotification);
        }
        
        private void OnUnlockedNotification(AchievementTestOperations.AchievementUnlocked serverNotification)
        {
            Debug.Log("実績解除通知取得");
            notification.text = "実績を解除しました\n" + serverNotification.title;
        }

        private void OnDestroy()
        {
            subscription.Dispose();
        }
    }
}

Game Objectが生成されると共にGameSparksからの通知を待機し、通知が飛んできたら専用のText Game Objectへテキストを格納するというコードです。

ConnectionScriptableObject型のフィールドは、これまでと同様の存在です。
Awake()内のSubscribeToAchievementUnlocked()によって、subscription変数が購読状態に入ります。その際、通知を受けたときに実行するメソッドを渡す必要があり、それがText Game Objectへ実績を解除したというテキストを入れる処理を書いているOnUnlockedNotification()です。

作成したC#コードをUnity上で実績解除情報を表示したいText Game Objectへアタッチすることで、ゲームサーバから取得した実績解除通知を表示する処理が完成します。

完成

作成した全ての機能を実際に動かした様子です。

GameSparksのプレビュー版は米国バージニア北部リージョンで動いているためかなり遅延が発生していますが、クライアントとサーバの双方向通信で必要な機能を実装できているのが分かります。

まとめ

GameSparksのMessage機能を使用する具体的な流れを解説しました。
サンプルゲームの作成にあたって行ったことは、この記事に載せたコードの作成とGameSparksでの簡単な設定、Unityでの一般的なゲーム開発作業のみであり、インフラに関係する複雑な作業は一切行っていません
コード例の通り、GameSparksが自動的に生成したクラスのメソッドを呼び出すだけで、簡単にバックエンドサーバとの通信処理をゲームクライアントに組み込むことができました。

このように、GameSparksを活用することで、サーバとの連携が必要なゲームの機能が従来よりも手軽に開発できるようになります。