注: Google Maps Platform ゲームサービスへのサポートは、2021 年 10 月 18 日をもって終了いたしました。本サービスを利用しているユーザーは、2022 年 12 月 31 日まで引き続きアクセスできます。また、主要なバグとシステム障害へのサポートおよび修正にも、上記の期日まで対応する予定です。ゲームサービス移行ガイドで、プロジェクトの次のステップを計画するうえで役立つリソースをご確認ください。

Playable Locations API スタートガイド

概要

このスタートガイドでは、Playable Locations API の運用方法について説明します。Playable Locations API で地表上のエリアを指定する際に使用する S2 セルのメカニズムなど、コア技術の背景となる設計理論を理解できます。このガイドでは、簡単なゲームアプリ アーキテクチャに基づき、プレイアブル ロケーションの取得から使用までの全体的な流れを詳しく説明します。

サポート ライブラリ

このガイド全体で、次のサポート ライブラリを使用します。

ライブラリ説明
S2 Geometry 空間インデックスの柔軟なサポート。
Protocol Buffers 通信プロトコルやデータ ストレージなどで使用するために構造化データをシリアル化する拡張可能メカニズム。言語やプラットフォームに依存しません。
gRPC オープンソースのリモート プロシージャ コールの高性能フレームワーク。

Playable Locations API へのアクセス方法

Playable Locations API にアクセスするには、次の 2 つの方法があります。

方法 1

gRPC クライアント ライブラリを使用して Playable Locations API にアクセスします(推奨方法)。

  1. Google API GitHub リポジトリの API プロトコル定義を使用し、こちらの手順に沿って gRPC クライアント ライブラリの最新バージョンを生成します。
  2. このページの最後にあるサンプルコードに倣い、API キーを設定します。

方法 2

REST HTTP リクエストの送信と HTTP レスポンスの解析用に、JSON クライアント ライブラリをビルドします。これらのコンポーネントの作成と保守に伴うオーバーヘッドを考慮し、このガイドでは簡単な方法 1(gRPC)を使用します。

概念

サンプル リクエスト

プレイアブル ロケーションを取得するための呼び出しは、単純な SQL データベース クエリをモデルにしています。概念的には、呼び出しは次のような形式になります。

select FIELDS_TO_RETURN
from PlayableLocations
where FILTER

ゲームのプレイアブル ロケーションには、特定のゲーム オブジェクト タイプ(敵、宝物、パワーアップ アイテムなど)が配置されます。各ゲーム オブジェクト タイプは、それぞれ異なる Criterion に関連付けられます。すべての Criterion を SamplePlayableLocationsRequestcriteria[] 配列にまとめることができます。さらにリクエストでは、AreaFilter を使用して S2 セル(検索対象となるエリア)を指定する必要があります。

サンプル レスポンス

SamplePlayableLocationsResponse には次のフィールドがあります。

ゲーム オブジェクト タイプごとのロケーション
locations_per_game_object_type フィールドは、各ゲーム オブジェクト タイプを、対応するプレイアブル ロケーションのリストに関連付けるマップです。
有効期間
ttl フィールドは、プレイアブル ロケーションをキャッシュできる期間を指定します。この期間が過ぎた後は、新たに検索を実行して更新する必要があります。

クエリとキャッシュの相互関係

次のシーケンス図は、ゲーム クライアント、ゲームサーバー、ゲーム状態データベース、Playable Locations API 間の推奨ロジックフローを示しています。

シーケンス図

典型的なワークフロー

特定の S2 セルをカバーするプレイアブル ロケーション セットを取得し、使用するためのワークフローは次のとおりです。

  1. 特定の S2 セルをカバーするプレイアブル ロケーションのサンプル リクエストを送信します。
  2. レスポンスには、該当する S2 セルのプレイアブル ロケーション セットと、このセットの有効期間を示す値が含まれます。
  3. このデータをゲーム クライアントに返し、ゲーム状態データベースにキャッシュします。
  4. この S2 セルのプレイアブル ロケーションがキャッシュされたので、今後、同じ S2 セルのプレイアブル ロケーションがリクエストされたときは、キャッシュから直接処理できます。
  5. プレイアブル ロケーションが有効期限に達した場合は、同じ S2 セルをカバーするプレイアブル ロケーションを再度リクエストして、更新します。

このフローは、指定した相互作用半径(interaction radius)で定義される円のカバー状況に応じて、次の 5 つのシナリオに分けることができます。

シナリオ 1

S2 セルがキャッシュされていない。この場合、最大 4 つのサンプル リクエストを発行する必要があります。その際、相互作用エリアがカバーされるように、隣接する S2 セルを指定します。

円形エリアをカバーする S2 セルがキャッシュされていない

シナリオ 2、3、4

1 つ、2 つ、または 3 つの S2 セルがキャッシュされている。この場合、それぞれ 3 つ、2 つ、1 つの隣接する S2 セルを取得する必要があります。

円形エリアをカバーしている S2 セル

シナリオ 5

4 つの S2 セルがすべてキャッシュされている。この場合、追加の S2 セルをカバーするプレイアブル ロケーションをリクエストする必要がありません。

すべての S2 セルがキャッシュされている

これは典型的な使用シナリオです。キャッシュにデータが格納された状態で、新しいプレーヤーが同じエリアに入った場合、または既存のプレーヤーがこのエリア内を動き回る場合に該当します。この場合、サンプル リクエストを発行する必要はありません。

更新戦略と結果のストリーミング

ゲームサーバーを設計する際は、バックグラウンドで残りのコールド S2 セルを更新しつつ、できるだけ早く結果をクライアントにストリーミングすることおすすめします。ここでは、コールド S2 セルを更新するためのパターンをいくつか紹介します。

Producer-Consumer キュー

ゲームサーバーに producer-consumer キューを実装し、Google からデータを取得します。キューの並列化係数を慎重に調整して、適切な応答時間が得られるようにしてください。このパターンの利点は、結果を待っているクライアントを満足させるために必要なリクエストを自動的に優先することです。

非同期更新

もう 1 つの方法は、S2 セルの Least Recently Used(LRU)リストを調べて、更新する方法です。Producer-Consumer キューよりシンプルですが、新しく要求された S2 セルを追跡するのが難しいという欠点があります。

S2 セルサイズの決定

実際のゲームでテストし、次の点を考慮して最適な S2 セルサイズを決定してください。

  • S2 セルのサイズを小さくする: 高レベルの S2 セルを使用すると小さいエリアをカバーできますが、送信するリクエストの数が増えます。
  • S2 セルのサイズを大きくする: 低レベルの S2 セルを使用すると大きいエリアをカバーできます。各リクエストに含まれるデータは多くなりますが、取得するプレイアブル ロケーションがプレーヤーから離れすぎて、プレーヤーとの相互作用が難しくなる傾向にあります。

ゲーム オブジェクト タイプの数と条件

ゲーム オブジェクト タイプの最適な数を決めるとき、および各タイプの条件の設定方法を決めるときは、注意が必要です。

デフォルト値がすべてのケースに適している可能性は低いため、条件ごとに maxLocationCount を指定する必要があります。プレイアブル ロケーションの実際の数は 0~maxLocationCount になり、フィルタや間隔によって異なります。なお、spacingmaxLocationCount より優先されます。たとえば、レベル 14 の S2 セル(1 辺がおよそ 600 m)の場合、互いの 1 km 以内に 500 のプレイアブル ロケーションを配置することはできません。

間隔のオプション

spacing を設定すると、希少なゲーム オブジェクトを適切な間隔で配置できます。SpacingOptions は、特定のゲーム オブジェクト タイプの間隔だけでなく、他のすべてのゲーム オブジェクト タイプとの間隔も定義します。このスキームでは暗黙的な階層が定義されます。つまり、最初のゲーム オブジェクト タイプは出現頻度が最も低く(間隔が最も広い)、最後のゲーム オブジェクト タイプは出現頻度が最も高くなります(間隔が最も狭い)。

返されるフィールド

各ゲーム オブジェクト タイプの条件には返されるフィールドのセットが含まれ、これはゲーム オブジェクト タイプによって異なります。

ストレージ レイヤ

BigQuery を使用して、キャッシュされたプレイアブル ロケーションに関連付けられたゲーム状態を保存することをおすすめします。TTL の有効期限を処理するには、同じ S2 セルキーでインデックスを作成することをおすすめします。

キャッシュのプレウォーミング

キャッシュのプレウォーミング(事前読み込み)はおすすめしません。プレーヤーが新しいエリアに入ってから、徐々にデータをキャッシュすることをおすすめします。これにより、データベースを常に最新、かつサイズを小さく維持することができます。

サンプルコード

次の Java コードは、gRPC リクエストをビルドし、ClientInterceptor を使用して API キーを関連付ける方法を示しています。

import com.google.maps.playablelocations.v3.sample.AreaFilter;
import com.google.maps.playablelocations.v3.sample.Criterion;
import com.google.maps.playablelocations.v3.PlayableLocationsGrpc;
import com.google.maps.playablelocations.v3.PlayableLocationsGrpc.PlayableLocationsBlockingStub;
import com.google.maps.playablelocations.v3.SamplePlayableLocationsRequest;
import com.google.maps.playablelocations.v3.SamplePlayableLocationsResponse;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ClientInterceptors;
import io.grpc.ForwardingClientCall;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import java.util.logging.Logger;

public class ApiKeyAuthSample {

  public static void main(String[] args) {

    final String API_KEY = "YOUR_API_KEY";    // Replace with your API key.

    // Creates a Channel to access the Playable Locations API.
    Channel c = ManagedChannelBuilder.forAddress("playablelocations.googleapis.com", 443).useTransportSecurity().build();

    // To use your API key with the requests, attach the Interceptor to the
    // channel.
    c = ClientInterceptors.intercept(c, new Interceptor(API_KEY));

    PlayableLocationsBlockingStub pls = PlayableLocationsGrpc.newBlockingStub(c);

    SamplePlayableLocationsResponse r = pls.samplePlayableLocations(SamplePlayableLocationsRequest.newBuilder().setAreaFilter(
        AreaFilter.newBuilder().setS2CellId(7715420650000613376L))
        .addCriteria(
            Criterion.newBuilder().setGameObjectType(0).build())
        .addCriteria(
            Criterion.newBuilder().setGameObjectType(1).build()).build());

    System.out.println("RESULT:" + r.getLocationsPerGameObjectTypeMap().get(0));
    System.out.println("RESULT:" + r.getLocationsPerGameObjectTypeMap().get(1));
  }

  /**
   * An Interceptor that attaches the API key to the gRPC requests.
   */
  private static final class Interceptor implements ClientInterceptor {

    private static Logger LOGGER = Logger.getLogger("InfoLogging");

    private static Metadata.Key<String> API_KEY_HEADER = Metadata.Key.of("x-goog-api-key", Metadata.ASCII_STRING_MARSHALLER);

    private final String apiKey;

    public Interceptor(String apiKey) {
      this.apiKey = apiKey;
    }

    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {

      LOGGER.info("Intercepted " + method.getFullMethodName());

      ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);

      call = new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(call) {

      @Override
      public void start(Listener<RespT> responseListener, Metadata headers) {

        LOGGER.info("Attaching API Key: " + apiKey);

        headers.put(API_KEY_HEADER, apiKey);

        super.start(responseListener, headers);
      }
      };

      return call;

    }
  }
}