Androidゲームでのターンベース型マルチプレイヤーのサポート



  • この記事は、Google Playゲームサービスに関する記事を和訳したものです。
  • 原文: Turn-based Multiplayer Support in Android Games
  • 元記事のライセンスは CC-BYで、この和訳記事のライセンスは CC-BYです。
  • 自己責任でご利用ください。
  • 和訳した時期は 2019年7月ころです。

このガイドでは、Androidアプリケーションにて Google Playゲームサービスを使用するターンベース型マルチプレイヤーゲームを実装する方法を示します。 APIは、com.google.android.gms.games.multiplayercom.google.android.gms.games.multiplayer.turnbased、および com.google.android.gms.gamesパッケージにあります。

始める前に

もしまだそうしていなければ、ターンベース型マルチプレイヤーのゲームコンセプトをレビューすることは役立つかもしれません。

あなたのターンベース型マルチプレイヤーゲームをコーディングし始める前に:

ターンベース型マルチプレイヤークライアントを取得する

ターンベース型マルチプレイヤー APIを使用し始めるには、あなたのゲームは、まず TurnBasedMultiplayerClientオブジェクトを取得しなければなりません。 Games.getTurnBasedMultiplayerClient()メソッドを呼び出し、アクティビティと、現在のプレイヤーの GoogleSignInAccountを渡すことによって、これを行うことができます。 プレイヤーのアカウント情報を取得する方法については、Androidゲームでのサインインを参照してください。

マッチを開始する

ターンベースのマッチを開始するには、あなたのゲームは、プレイヤーに、彼らがどのように他の参加者とマッチされたいかを選択するよう、あるいは幾つかのデフォルトの基準を使用してマッチを構築するよう促すことができます。 Google Playゲームサービスは、あなたのゲームに TurnBasedMatchオブジェクトを返すためにあなたのゲームからマッチの構成データを使用します; このオブジェクトはターンベースのマッチのライフライクルを通してすべての参加者と非同期的に更新および共有されます。

マッチを開始するには、次の手順を実行します:

  1. あなたのアプリにて、プレイヤーがプレイしたいターンベースのマッチの種類の仕様と、誰がマッチに参加することができるかを収集します。
    • あなたのゲームは SDKによって提供されたビルトインのプレイヤー選択ユーザインタフェース(UI)を表示するか、このデータを収集するための独自のカスタム UIを使用することができます。 プレイヤー選択 UIを使用する方法については、デフォルトのユーザインタフェースを用いてプレイヤーを選択するを参照してください。
    • 別の方法として、ユーザにプレイヤー選択 UIをバイパスさせ、代わりにマッチを構築するために幾つかのデフォルトの基準を使用させるために、Quick Startボタンを実装することができます。
  2. TurnBasedConfigObject内のマッチの構成データを設定するために TurnBasedMatchConfig.Builderを使用します。
  3. TurnBasedMultiplayerClient.createMatch()を呼び出し、作成した TurnBasedConfigObjectを渡します。 もしオートマッチが指定されているならば、Google Playゲームサービスは既存のゲームにプレイヤーをマッチさせようとするでしょう; 詳細については、オートマッチの実装を参照してください。

デフォルトのユーザインタフェースを用いてプレイヤーを選択する

Google Playゲームサービスは、プレイヤーに、マッチに招くために彼らの友人を選択させる、あるいは、ランダムなプレイヤーを用いてオートマッチされることを選択させる、デフォルトのプレイヤー選択 UIを提供します。

デフォルトのプレイヤー選択 UIからマッチを開始するには、TurnBasedMultiplayerClient.getSelectOpponentsIntent()メソッドを呼び出し、アクティビティを開始するために、それが返すインテントを使用します。

例えば:

private static final int RC_SELECT_PLAYERS = 9010;


public void onStartMatchClicked(View view) {
  boolean allowAutoMatch = true;
  Games.getTurnBasedMultiplayerClient(this, GoogleSignIn.getLastSignedInAccount(this))
      .getSelectOpponentsIntent(1, 7, allowAutoMatch)
      .addOnSuccessListener(new OnSuccessListener<Intent>() {
        @Override
        public void onSuccess(Intent intent) {
          startActivityForResult(intent, RC_SELECT_PLAYERS);
        }
      });
}

次に、ユーザによって提供された基準を使用する TurnBasedMatchConfigオブジェクトを構築するために、onActivityResult()コールバックをオーバーライドします。 TurnBasedMatchConfigオブジェクトは、ユーザがプレイしたいマッチの特性を定義します。 Google Playゲームサービスは、新しいマッチを構築するか既存のゲームにユーザをオートマッチするかを決定するために、この構成データを使用します。

例えば:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  if (requestCode == RC_SELECT_PLAYERS) {
    if (resultCode != Activity.RESULT_OK) {
      // Canceled or other unrecoverable error.
      return;
    }
    ArrayList<String> invitees = data.getStringArrayListExtra(Games.EXTRA_PLAYER_IDS);

    // Get automatch criteria
    Bundle autoMatchCriteria = null;
    int minAutoPlayers = data.getIntExtra(Multiplayer.EXTRA_MIN_AUTOMATCH_PLAYERS, 0);
    int maxAutoPlayers = data.getIntExtra(Multiplayer.EXTRA_MAX_AUTOMATCH_PLAYERS, 0);

    TurnBasedMatchConfig.Builder builder = TurnBasedMatchConfig.builder()
        .addInvitedPlayers(invitees);
    if (minAutoPlayers > 0) {
      builder.setAutoMatchCriteria(
          RoomConfig.createAutoMatchCriteria(minAutoPlayers, maxAutoPlayers, 0));
    }
    Games.getTurnBasedMultiplayerClient(this, GoogleSignIn.getLastSignedInAccount(this))
        .createMatch(builder.build()).addOnCompleteListener(new OnCompleteListener<TurnBasedMatch>() {
      @Override
      public void onComplete(@NonNull Task<TurnBasedMatch> task) {
        if (task.isSuccessful()) {
          TurnBasedMatch match = task.getResult();
          if (match.getData() == null) {
            // First turn, initialize the game data.
            // (You need to implement this).
            initializeGameData(match);
          }

          // Show the turn UI.
          // (Game specific logic)
          showTurnUI(match);
        } else {
          // There was an error. Show the error.
          int status = CommonStatusCodes.DEVELOPER_ERROR;
          Exception exception = task.getException();
          if (exception instanceof ApiException) {
            ApiException apiException = (ApiException) exception;
            status = apiException.getStatusCode();
          }
          handleError(status, exception);
        }
      }
    });
  }
}

もし、デフォルトのプレイヤー選択 UIを使用することなくマッチを作成したいならば、あなたのゲームは、TurnBasedMultiplayerClient.createMatch()に渡す TurnBasedMatchConfigオブジェクトにて招待される者のプレイヤー IDとオートマッチの基準を提供しなければなりません。

必要に応じて、ゲームのバリアントの特定のタイプに興味を持っているプレイヤーのみが一緒にオートマッチされることを保証したいかもしれません。 もしあなたのアプリに異なったバージョンがあるならば、あなたのゲームは、互換性のあるバージョンのプレイヤーのみがオートマッチされることを保証するためにバリアントを使用することもできます。

ターンベースのマッチを作成するときにバリアントを指定するには、setVariant()メソッドを使用します。 さらには、あなたのゲームはゲーム内で特定の排他的な役割を果たすことに興味を持っているオートマッチされたプレイヤーをペアリングするために createAutoMatchCriteria()にて exclusiveBitMaskパラメータを使用することもできます。

最初のターンを取る

もし TurnBasedMultiplayerClient.createMatch()呼び出しが成功したならば、Google Playゲームサービスは、TurnBasedMatchインスタンスを非同期に読み込む Taskオブジェクトを返します。

先に進む前に、あなたのゲームによって必要に応じてゲームデータを初期化することを忘れないでください。 例えば、戦略ゲームではプレイヤーのために最初の位置を初期化する必要があるかもしれません、あるいは、カードゲームではプレイヤーのために最初の持ち札を初期化する必要があるかもしれません。

最初のターンを実装するには、次の手順に従います:

  1. TurnBasedMatchインスタンス上の getData()メソッドを呼び出します。
    • もし呼び出しが null値を返したならば、これは、プレイヤーがまだターンを取っていないのでゲームデータが初期化されていない(つまり、現在のプレイヤーがこのマッチでの最初のプレイヤーである)ことを示しています。 あなたのゲームはゲームデータを初期化し、それをバイト配列に格納することができます。 ゲームデータのサイズは、TurnBasedMultiplayerClient.getMaxMatchDataSize()によって返されるサイズより小さくなければなりません。 返されるデータサイズは、少なくとも 128KBであることが保障されています。
    • もし呼び出しが nullではない値を返したならば、これは、ゲームが既に開始しており、ゲームデータが既に初期化されていることを示しているので、あなたのゲームがデータを再び初期化していないことを確認してください。
  2. 必要に応じて、すぐに TurnBasedMultiplayerClient.takeTurn()を呼び出します。 もしあなたの初期状態がランダム値に基づいていて(例えば、プレイヤーがランダムな持ち札とともに開始するカードゲーム)、かつ、あなたのゲームが Googleのサーバにデータを永続化するために TurnBasedMultiplayerClient.takeTurn()を呼び出さなければ、プレイヤーはより良い開始位置を取得しようとするために最初のターンをリスタートし続けることができます。
  3. プレイヤーにゲームアクションを実行させます、そして適切な場合に、ゲームデータを含むバイト配列を更新します。
  4. TurnBasedMultiplayerClient.takeTurn()を呼び出すことによって、ゲームデータを Googleのサーバに保存します。 あなたのゲームは次のプレイヤーの参加者 IDを指定することができます、あるいは Google Playゲームサービスにオートマッチのためのプレイヤーを見つけさせるために nullを指定することができます。 あなたのゲームは、すべての招待されたプレイヤーが参加していないとき、あるいはオートマッチのスロットがまだ利用可能であるときでさえマッチの参加者にターンを取らせることができます。 プレイヤーがより早くゲームプレイに参加するために、あなたのゲームは、プレイヤーが、彼らが参加したらすぐに彼らのターンを取ることができるようにする必要があります。 TurnBasedMultiplayerClient.takeTurn()を呼び出したとき、Google Playゲームサービスは保留中の参加者に通知を送信し、すべての参加者の端末上のターンデータを非同期的に更新します。
  5. 次のプレイヤーがターンを取ったとき、プレイヤーにゲームアクションを実行させる前に getData()を呼び出すことによって初期化されたデータをロードしてください。

オートマッチを実装する

あなたのゲームが TurnBasedMultiplayerClient.createMatch()を呼び出し、オートマッチをリクエストしたとき、Google Playゲームサービスは、まず既存のゲームにプレイヤーをマッチさせようとします。

もしプレイヤーのオートマッチの基準を満たしているマッチが見つかったならば、Google Playゲームサービスは、既存のマッチにオートマッチされたプレイヤーとして、プレイヤーと任意の招待されたプレイヤーに自動的に参加します。

もしマッチが見つからなければ、あなたのゲームは、次の手順を実行することによって、オートマッチが利用可能であることをシグナルする必要があります:

  1. 開始プレイヤーに最初のターンを取るよう促します。
  2. 必要に応じて、開始プレイヤーが彼らの最初のターンを完了した後、あなたのゲームはマッチに参加した他の招待されたプレイヤーに彼らの最初のターンを取らせることもできます。
  3. オートマッチをトリガするために、TurnBasedMultiplayerClient.takeTurn()を呼び出し、pendingParticipantIdパラメータに nullを渡します。

次のコードは、あなたのゲームがラウンドロビン方式で次の参加者を取得する方法を示しています、そこでは、すべての既知のプレイヤーは、すべてのオートマッチプレイヤーより前にターンを取ります:

public String getNextParticipantId(String myPlayerId, TurnBasedMatch match) {
  String myParticipantId = match.getParticipantId(myPlayerId);

  ArrayList<String> participantIds = match.getParticipantIds();

  int desiredIndex = -1;

  for (int i = 0; i < participantIds.size(); i++) {
    if (participantIds.get(i).equals(myParticipantId)) {
      desiredIndex = i + 1;
    }
  }

  if (desiredIndex < participantIds.size()) {
    return participantIds.get(desiredIndex);
  }

  if (match.getAvailableAutoMatchSlots() <= 0) {
    // You've run out of automatch slots, so we start over.
    return participantIds.get(0);
  } else {
    // You have not yet fully automatched, so null will find a new
    // person to play against.
    return null;
  }
}

招待を処理する

ひとたびプレイヤーがあなたのゲームにサインインしたならば、プレイヤーは他のプレイヤーによって作成されたターンベースのマッチに参加するための招待を受信するかもしれません。 コーディングの時間を節約し、アプリケーションを超えてマッチの招待に応答するための一貫性のある UIをユーザに提供するために、SDKによって提供されたデフォルトのマッチ受信トレイ UIを使用することができます。

デフォルトのマッチ受信トレイ UIを起動するには、TurnBasedMultiplayerClient.getInboxIntent()メソッドを呼び出し、それから、アクティビティを開始するために、それが返すインテントを使用します。

ターンを取る

最初のプレイヤーがターンを取った後、あなたのゲームは同じ参加者あるいは他の参加者が次のターンを取るようにさせることができます。 一般的に、ターンテイキングは、Google Playゲームサービスから最新のマッチデータをロードし、参加者にあなたのゲームと対話させ、マッチデータを更新し他の参加者にターンを渡すために TurnBasedMultiplayerClient.takeTurn()を呼び出すことを伴います。

ターンテイキングを実装するには、これらの手順を実行します:

  1. 必要に応じて、もしマッチ内のいかなる参加者がターンを取るたびに通知されたいならば、あなたのアクティビティに TurnBasedMatchUpdateCallbackをアタッチします。 マッチが次のプレイヤーのターンの後で更新されるたびに、あなたのアクティビティは TurnBasedMatchUpdateCallback.onTurnBasedMatchedReceived()メソッドを経由して通知されます。
  2. 参加者がターンを取るために、マッチはアクティブ状態でなければならず、それは、その参加者のターンでなければなりません。 あなたのゲームはこれを確認するために TurnBasedMatchオブジェクトを使用することができます。
    • getStatus()を呼び出すことによってマッチの状態をチェックします。 MATCH_STATUS_ACTIVEという結果は、マッチがアクティブであることを示しています。
    • getTurnStatus()を呼び出すことによって、マッチターンの状態をチェックします。 MATCH_TURN_STATUS_MY_TURNという結果は、今、ユーザのターンであることを示しています。
  3. getData()を呼び出すことによって、Google Playゲームサービスから最新のマッチデータをロードし、あなたのゲームをそれに応じてレンダリングします。
  4. ユーザにゲームアクションを実行させ、適切な場合には、更新されたゲームデータをバイト配列に保持します。
  5. 次のターンを取る必要がある保留中の参加者を決定します。 これは、通常、あなたのゲーム設計によって指定されたプレイの順序に依存しています。 保留中の参加者は、マッチ内の他の参加者、あるいは現在のターンである参加者であることができます。 さらには、あなたのゲームは、保留中の参加者に nullを設定することによって、オートマッチによって参加している次のプレイヤーにターンを渡すこともできます。
  6. 最新のゲームデータを用いて Google Playゲームサービスを更新するために TurnBasedMultiplayerClient.takeTurn()を呼び出し、保留中の参加者にターンを渡します。 もし更新が成功したならば、Google Playゲームサービスは彼らに彼らのターンであることを知らせるために保留中の参加者に通知を送信します。

例えば:

private void playTurn(TurnBasedMatch match) {
  String nextParticipantId = getNextParticipantId(mMyPlayerId, match);

  // This calls a game specific method to get the bytes that represent the game state
  // including the current player's turn.
  byte[] gameData = serializeGameData();

  Games.getTurnBasedMultiplayerClient(this, GoogleSignIn.getLastSignedInAccount(this))
      .takeTurn(match.getMatchId(), gameData, nextParticipantId)
      .addOnCompleteListener(new OnCompleteListener<TurnBasedMatch>() {
        @Override
        public void onComplete(@NonNull Task<TurnBasedMatch> task) {
          if (task.isSuccessful()) {
            TurnBasedMatch match = task.getResult();
          } else {
            // Handle exceptions.
          }
        }
      });
}

ゲームの状態を保存する

あなたのゲームの状態データが許されたサイズに収まる場合は、あなたのゲームの状態を管理するためにターンベース型マルチプレイヤー APIを使用することができます。 データサイズの制限は、TurnBasedMultiplayerClient.getMaxMatchDataSize()を呼び出すことによって取得することができます。 返されるデータサイズは、少なくとも 128KBであることが保障されています。

ターンベース型マルチプレイヤー APIを用いてゲームの状態を保存するには:

  1. TurnBasedMultiplayerClient.takeTurn()を呼び出し、あなたのゲーム状態のデータを matchDataパラメータとして渡します。
  2. もし呼び出しが成功したならば、Google Playゲームサービスは更新についてマッチ内の他の参加者に通知し、マッチのデータをすべての参加者の端末上で利用可能にします。
  3. あなたのゲームは、それから、更新されたゲームの状態を取得するために getData()を呼び出すことができます。

プレイヤーのターンが中断されるとき、あるいは、プレイヤーが一時的にゲームから離れなければならない(例えば、電話の着信呼び出しによって)ときはいつでも、あなたのゲームは部分的に完成したターンのためにゲームデータを保存しようとする必要があります。 これを行なうには、TurnBasedMultiplayerClient.takeTurn()を呼び出すために、あなたのアクティビティの onStop()メソッドをオーバーライドします。 TurnBasedMultiplayerClient.takeTurn()への最後の呼び出しと同じ参加者 IDを使用することによって、保留中の参加者として現在のプレイヤーを指定してください。 もし成功したならば、呼び出しは Googleのサーバにてゲームデータを格納しますが、新しいターンの通知を生成しません。

マッチを完了する

マッチが最後までプレイされたとき(例えば、ユーザがゲームに勝った)、あなたのゲームはユーザのゲームデータをアップロードするために TurnBasedMultiplayerClient.finishMatch()を呼び出し、他の参加者にそのマッチが終わったことをシグナルする必要があります。 あなたのゲームがマッチ中に初めてこのメソッドを起動するときは、ユーザのターン中でなければなりません。 マッチは、それから、ユーザのマッチリスト UIにて Completed Matchesカテゴリの下に表示されます。

ひとたびプレイヤーがマッチ内で初めて TurnBasedMultiplayerClient.finishMatch()を呼び出したならば、あなたのゲームはこのマッチ内で再び TurnBasedMultiplayerClient.takeTurn()を呼び出すことはできません。 Google Playゲームサービスは、マッチが終わったことを彼らに通知するためにすべての他のマッチの参加者に通知を送信します。 これらの参加者は、彼らそれぞれのマッチリスト UIにて Your Turnカテゴリの下にこのマッチを見ます。 この時点で、あなたのゲームは、これらの参加者が彼らの最後のゲームデータを保存するために、TurnBasedMultiplayerClient.finishMatch()を呼び出すことができます。 さらには、このメソッドを呼び出すと、マッチを、参加者のマッチリスト UI内の Completed Matchesカテゴリに移動させます。

マッチから離れる

参加者は、マッチが続けるようにしている間は、マッチ中のいつでも離れることを選ぶことができます。 参加者が離れていることをシグナルするために、あなたのゲームは TurnBasedMultiplayerClient.leaveMatch() あるいは TurnBasedMultiplayerClient.leaveMatchDuringTurn()のいずれかを呼び出す必要があります。 参加者がマッチから離れたとき、マッチはこれらの条件が満たされる限り、他の参加者とともにまだ続けることができます:

  • 他の残っている参加者が 2人以上いる、あるいは
  • 残っている参加者が 1人いて、少なくとも 1つの空のオートマッチのスロットが利用可能である。

そうでなければ、マッチはキャンセルされます。

一般的には、参加者がマッチから離れたとき、別のプレイヤーがその参加者の場所に参加し、取ることはできません。 1つの例外は、オートマッチによって参加した、ターンを取ったことがないプレイヤーが、TurnBasedMultiplayerClient.leaveMatchDuringTurn()を呼び出すときです。 この場合、マッチは MATCH_STATUS_AUTO_MATCHING状態に戻り、別のプレイヤーが、離れた参加者の場所を引き継ぐことができます。

マッチをキャンセルする

TurnBasedMultiplayerClient.cancelMatch()を呼び出すことによって、あなたのゲームはマッチが正常に最後までプレイされる前にすべての参加者のためにマッチを終了することができます。 このメソッドを呼び出した後、あなたのゲームはこのゲームにて再び TurnBasedMultiplayerClient.takeTurn()を呼び出すことはできません; マッチは、今、マッチリスト UIにて Completed Matchesカテゴリの下に表示されます。

マッチを却下する

マッチを却下するには、TurnBasedMultiplayerClient.dismissMatch()を呼び出します。

あなたのゲームはユーザにターンあるいは招待を却下させることができます、そうすれば、彼らはマッチを再び見る必要がありません。 これは却下した者のマッチリスト UIからマッチを隠し、マッチが最終的には失効することを引き起こします。 他のマッチの参加者は、却下されたマッチが 2週間後に失効するまで、あるいはマッチが最後までプレイされるかキャンセルされるまで(どちらか早いほう)、プレイを続けることができます。 他の参加者には、却下した者はまだマッチの参加者として表示されます。 他のプレイヤーが却下した者の場所を取ることはできません。

マッチの失効を追跡する

プレイヤーがマッチあるいはターンの通知に 2週間応答しない場合には、マッチは失効することができます(例えば、そのターンであるプレイヤーが、デフォルトのマッチリスト UIにて Dismissを選択した後)。

どのプレイヤーがゲームの失効を引き起こしたかを識別するために、ゲームクライアント上で次のアプローチを使用することができます:

  1. TurnBasedMultiplayerClient.loadMatchesByStatus()を呼び出すことによって、現在サインインしているプレイヤーのゲームのリストをロードします。
  2. 次に、MATCH_TURN_STATUS_COMPLETE状態にあるマッチのリストを含む TurnBasedMatchBufferを取得するために、getCompletedMatches()を呼び出します。
  3. それから、MATCH_STATUS_EXPIRED状態にあるマッチのために、そのリストをフィルタリングします。
  4. もしマッチの参加者が STATUS_UNRESPONSIVE状態を持っているならば、それは、このプレイヤーがゲームを失効させたことを示しています。

再マッチを実装する

マッチが終わったとき、参加者は同じ招待された者およびオートマッチの相手のセットとの再マッチをプレイしたいかもしれません。 あなたのゲームは TurnBasedMultiplayerClient.rematch()を呼び出すことによって再マッチを開始することができます。 あなたのゲームは、マッチの状態が MATCH_STATUS_COMPLETEで、他の参加者が再マッチをリクエストしていないときのみ、このメソッドを呼び出すことができます。 もし呼び出しが成功したならば、Google Playゲームサービスはすべての再マッチされた相手に招待の通知を送信します。

もしあなたのゲームがデフォルトのマッチ受信トレイ UIを使用しているならば、プレイヤーはその UIから再マッチを開始することもできます。 プレイヤーがマッチ受信トレイ UIから再マッチを開始するとき、Google Playゲームサービスは、マッチ受信トレイのアクティビティの onActivityResult()コールバックにて Intentエキストラとして、新しいマッチのオブジェクトを返します。 もしあなたのゲームが新しいマッチのためにゲームデータを初期化する必要があるならば、onActivityResult()コールバックにて返されるマッチのオブジェクトを使用してください。