あなたのゲームに Nearby接続を追加する



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

このガイドでは、あなたの C++アプリケーションにて Nearby接続 APIを使用する方法を示しています。 この APIは、あなたのアプリが簡単に、ローカルネットワーク上の他の端末を発見し、それらと接続し、それらを用いてリアルタイムにメッセージを交換できるようにします。 この機能は、特に 2つのタイプのユーザエクスペリエンスに有効です:

  • Local multiplayer gaming: 1人のプレイヤーに、ネットワーク上の他のプレイヤーが参加することができるローカルなマルチプレイヤーのゲームをセットアップできるようにします。 さらには、あなたのアプリは、プレイヤーに、近くの参加者が十分に参加したときにゲーム内のミッションを開始できるようにすることができます。
  • Multi-screen gaming: プレイヤーに、Android TVといった、近くの大画面の Android端末上に表示されるゲームをプレイするためのゲームコントローラとして、彼らの携帯電話あるいはタブレットを使用できるようにします。 さらには、あなたのアプリは、プレイヤーに、すべての近くの参加者がテーブルトップの Android端末上で共有された共通の表示を見ながら、彼らの個人的な端末にカスタマイズされたゲーム画面を表示できるようにすることができます。

この APIは Playゲーム C++ SDKの一部であり、WiFiネットワーク上の複数の近くの Android端末と一緒に参加することができるゲームを構築させます。 1つの端末がホストとしてネットワーク上に宣伝し、その間、他の端末はクライアントとして動作し、ホストへの接続リクエストを送信します。

始める前に

Nearby接続 APIを使用してコーディングし始める前に:

  • Google Play Services SDKをインストールします。
  • Nearby接続 APIの code sampleをダウンロードし、レビューします。
  • マルチキャストが有効になっている同じ WiFiネットワークに、複数の Android端末を接続します。

ひとたびシステムが構築され、NearbyConnectionsオブジェクトが構成されたならば、あなたのゲームは Nearby接続 APIを使用することができます。

Nearby接続のために Nearby接続 APIクライアントを初期化する

Nearby接続 APIにアクセスするには、まずそれを初期化する必要があります。 NearbyConnectionsオブジェクトを構築するための Builderクラスを使用することによって、これを行います。 次の例は、これを行う方法を示します。

gpg::AndroidPlatformConfiguration platform_configuration;
platform_configuration.SetActivity(app_->activity->clazz);

gpg::NearbyConnections::Builder nbcBuilder;
nearby_connection_ =
    nbcBuilder.SetDefaultOnLog(gpg::LogLevel::VERBOSE)
              .SetOnInitializationFinished([this](gpg::InitializationStatus status) {
                LOGI("NBCInitializationFinished: %d", (int)status);
                if (status == gpg::InitializationStatus::VALID) {
                  LOGI("InitializationFinished() returned VALID.");
                } else {
                  LOGE("InitializationFinished() returned an error.");
                }
              }).Create(platform_configuration);

ネットワーク接続を検証する

Nearby接続 APIを使用する前に、端末が WiFiネットワークに接続されていることを保証するためににチェックする必要があります。 もしそれらが接続されていなければ、プレイヤーに WiFiネットワークに接続するよう促します。

ネットワークの状態を検出するには、まずあなたのマニフェストに次のパーミッションを追加します:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

次の Javaコードの例は、端末が WiFiに接続されているかどうかを判断する方法を示しています:

ConnectivityManager cm =
    (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);

NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null &&
                      activeNetwork.isConnectedOrConnecting();
boolean isWiFi = activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;

以下のセクションで説明するように、あなたの端末を宣伝する、あるいはあなたの端末を発見モードにセットする前に、このメソッドを呼び出します。

あなたの端末を宣伝する

2つ以上の端末間の接続を確立するには、1つの端末がそのサービスを宣伝しなければならず、1つ以上の端末はそれに接続するためにリクエストしなければなりません。 宣伝している端末は、マルチプレイヤーゲームにて 'host' です、その間、接続している端末はクライアントです。

あなたのアプリが、Nearby接続 APIを用いて自身を宣伝することを有効にするには、あなたのマニフェストに次の行を追加します:

<application>
  <!-- Required for Nearby Connections API -->
  <meta-data android:name="com.google.android.gms.nearby.connection.SERVICE_ID"
            android:value="@string/service_id" />
  <activity>
      ...
  </activity>
</application>

service_idの値は、クライアント端末が、宣伝している端末を発見できるようにします。 値は、あなたのアプリを一意に識別しなければなりません。 ベストプラクティスとして、あなたのアプリのパッケージ名を使用します(例えば、com.google.example.mygame.service)。

次の例は、strings.xmlファイルにて service_idの値を宣言する方法を示しています:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    ...
    <!--
        This is the service identifier for Nearby Connections. Do NOT copy and paste this value
        into your own application.  Instead, choose a unique identifier that is appropriate for
        your application.
    -->
    <string name="service_id"><!-- your service ID goes here, e.g. com.google.example.mygame.service --></string>
    ...
</resources>

プレイヤーがゲームを開始し、ホストを選択したとき、端末を宣伝するために startAdvertising()を呼び出します。 次の例では、ホストプレイヤーの端末を宣伝する方法を示しています:

 std::vector<gpg::AppIdentifier> app_identifiers;
 gpg::AppIdentifier tmp;
 tmp.identifier = kAndroidPackageName; // Pass in the Android package name here.
 app_identifiers.push_back(tmp);
 nearby_connection_->StartAdvertising(
     "",  // Use an automatically-generated default name.
     app_identifiers,
     gpg::Duration::zero(),
     [this](int64_t client_id, gpg::StartAdvertisingResult const &result) {
         LOGV("StartAdvertisingResult(%lld, %s)", client_id, result.local_endpoint_name.c_str());
         switch (result.status) {
           case gpg::StartAdvertisingResult::StatusCode::SUCCESS:
               LOGI("Advertising succeeded!");
               break;
           default:
               LOGE("Advertising failed.");
               return;
         }
         EnableUI(true);
     },
     [this](int64_t client_id, gpg::ConnectionRequest const &request) {
         LOGI("ConnectionRequest(%lld)", client_id);
         LOGI("remote info: req.endpoint_id=%s, req.device_id=%s, req_name = %s",
             request.remote_endpoint_id.c_str(), request.remote_device_id.c_str(),
             request.remote_endpoint_name.c_str());
         std::string msg = "accepted";
         std::vector<uint8_t> payload(msg.begin(), msg.end());
         nearby_connection_->AcceptConnectionRequest(request.remote_endpoint_id, payload, msg_listener_);
         BroadcastNewConnection(request.remote_endpoint_id);
         // Adding this end point into the connected state:
         AddConnectionEndpoint(client_id, request.remote_endpoint_id, true, true);
         nbc_state_  |= nearby_connection_state::CONNECTED;
         nbc_state_  &= ~nearby_connection_state::IDLE;
         EnableUI(true);
         LOGI("Accepting Request sending out (for %s)", request.remote_device_id.c_str());
     });

端末に、切断後に再接続できるようにする最も簡単な方法は、ホストに、ゲームが終了するまで宣伝することを継続できるようにすることです。

他の端末を発見する

発見プロセスは、端末に、特定のサービス IDの接続を宣伝している近くの端末を見つけることができるようにします。 startDiscovery()に渡すサービス IDパラメータは、宣伝しているアプリのマニフェストにて提供される値と一致している必要があります。 アプリが Nearbyゲームを宣伝する方法については、あなたの端末を宣伝するを参照してください。

次の例では、あなたのアプリにて発見プロセスを開始する方法を示しています:

nearby_connection_->StartDiscovery(service_id_, gpg::Duration::zero(),  this);

次のセクションでは、ひとたびクライアントがホスト端末を発見した後、接続を処理する方法について学びます。

接続された端末を識別する

Nearby接続 APIが端末を識別する 2つの方法があります:

  • 端末 ID: これは端末を一意に識別し、端末を再起動したときでさえ同じままです。 ローカルな端末 IDは GetLocalDeviceIDを使用して取得します。
  • エンドポイント ID: これは、NearbyConnectionsインスタンスに対してローカルに端末を識別します。 ローカルなエンドポイント IDは GetLocalEndpointIDを使用して取得します。

エンドポイント IDは、接続を確立しメッセージを送信するために必要とされます。 例えば、もし端末 X上のプレイヤーが端末 Y上のプレイヤーに攻撃を指示しているならば、端末 Xからホストにメッセージを送信し、それは、端末 Xのエンドポイントを含んでいるので、端末 Yは、攻撃がどこから生じたかを知ることができます。

次のような場合に端末 IDを使用する可能性があります:

  • プレイヤーを識別し、彼らに適切なデータを復元する。
  • 発見中に、クライアントが以前に接続したことがあるホストに再接続する(例えば、翌日にあなたのクエストを継続するために、あなたの Android TVに再接続する)。
  • それとの接続を失った後、ホストに再接続することを試みる(例えば、電話の呼び出しを受信した後、マルチプレイヤーゲームに戻る)。

次のセクションでは、接続された端末にメッセージを送信する方法について学びます。

Nearby接続を通してメッセージを送信する

ひとたび接続が端末間で確立されれば、それらは、ゲームの状態を更新するため、および、ある端末から他の端末に。入力、データ、あるいはイベントを転送するために、メッセージを送信し受信することができます。 ホストは任意の数のクライアントにメッセージを送信することができ、クライアントとはホストにメッセージを送信することができます。 もしクライアントが他のクライアントと通信する必要があるならば、受信クライアントに情報をリレーするためにホストにメッセージを送信することができます。

メッセージを送信する

信頼できるメッセージを送信するには、sendReliableMessage()を呼び出し、適切なエンドポイント IDを渡します。 ペイロードパラメータはあなたのメッセージデータを保持します。 信頼できるメッセージは、それらが送信された順序で受信されることが保証され、システムは接続が終了するまでメッセージを送信することをリトライします。

次のコードは、ある端末から別の端末に信頼できるメッセージを送信する方法を示しています:

std::string msg_to_send = "message";
std::vector<uint8_t> payload(msg_to_send.begin(), msg_to_send.end());
nearby_connection_->SendReliableMessage(remote_endpoint_id, payload);

信頼性の低いメッセージを送信するには、sendUnreliableMessage()を呼び出します。 このメソッドは、システムに、メッセージをすぐに配信できるようにしますが、メッセージは失われたり順序が狂って送信されるかもしれません。 信頼性の低いメッセージは、マップ上に相手プレイヤーのキャラクタ位置を表示するといった、頻繁かつ重要度の低いゲームの通知に適しています。

メッセージを受信する

メッセージが送信された後、システムは IMessageListener::onMessageReceived()を通してメッセージを受信します。 あなたのゲームの要件に基づいて、プレイヤーのスコア、画面上の描画、あるいは現在のプレイヤーのターンといったゲームの情報を更新するために、このメソッドを実装することができます。

次のコードスニペットは、受信したメッセージを処理するためにこのメソッドを実装する方法を示しています:

void Engine::OnMessageReceivedCallback(int64_t receiver_id, std::string const &remote_endpoint,
    std::vector<uint8_t> const &payload, bool is_reliable) {
  std::string msg(payload.begin(), payload.end());
  if (msg.size() > 0) {
    switch (msg[0]) {
      case PAYLOAD_HEADER_FINAL_SCORE:
      {
        std::string  endpoint_id(msg.substr(1));
        endpoint_id.resize(remote_endpoint.size());
        msg = msg.substr(1 + remote_endpoint.size());   // score string
        UpdatePlayerScore(endpoint_id, msg, false);
        UpdateScoreBoardUI(true);
        return;
      }
      default:
      // Drop the message
      {
        LOGE("Unknown payload type: From(%s) with Payload(%s) in %s @ line %d",
            remote_endpoint.c_str(), msg.c_str(), __FILE__, __LINE__);
        return;
        }
    }
  }
}

Nearby接続を切断する

プレイヤーがゲームを終了したとき、あるいは、もしホストがプレイヤーに終了することを強制させたければ、Disconnect()を呼び出します。

次のスニペットは、切断したい特定の端末のエンドポイント IDを渡す方法を示しています:

nearby_connection_->Disconnect(remote_endpoint_id);

もしホストが宣伝を停止し、すべてのリモートのエンドポイントを切断することを決定したならば、Stop()を呼び出します。 より一般的に、接続を終了するとき、このメソッドを使用することはベストプラクティスです。 例えば:

  • クライアント端末に発見することを停止したい、あるいはホストから切断したいとき。
  • アクティビティを終了したとき。
  • アプリを終了したとき。
  • NearbyConnectionsオブジェクトを破壊したとき。

次の例では、Stop()を呼び出す方法を示します:

nearby_connection_->Stop();

NearbyConnectionsオブジェクトが端末上から切断され、Disconnect()、あるいは Stop()を呼び出したとき、システムは、それが接続されていた端末上の IMessageListener::onDisconnected()コールバックメソッドをトリガします。 切断を優雅に処理するためにこのメソッドを使用します。

Nearby接続を再接続する

プレイヤーとホストは、ネットワークの中断、あるいは単に WiFiネットワークの範囲外へ歩くことによって接続を失う可能性があります。 あなたのゲームのデザインに応じて、ゲームがまだ進行中の間は切断されたプレイヤーを再接続したい、あるいはゲームを一時停止し、プレイヤーが再接続しようとしたときにプレイヤーのターンを再開したいかもしれません。

あなたのアプリが端末を再接続することを可能にするには、プレイヤーの端末 IDとエンドポイント IDの両方を格納する必要があります。 位置あるいはスコアといったプレイヤーのデータを格納するとき、端末 IDによってプレイヤーを識別するので、データは、エンドポイント IDが変更したときでさえ、切断の後、復元されることができます。

もしホストがまだ宣伝していれば、プレイヤーはサービス IDを使用して再接続することができます。

接続を完了する

ひとたびあなたのアプリが、リクエストされたサービス IDを宣伝している他のアプリを発見したならば、端末間の接続を開始することができます。 次の例では、端末の発見を処理する方法を示します:

void Engine::OnEndpointFound(int64_t client_id, gpg::EndpointDetails const &endpoint_details) {
  std::vector<uint8_t> payload;
  std::string name;
  nearby_connection_->SendConnectionRequest(
      name,
      endpoint_details.endpoint_id,
      payload,
      [this](int64_t client_id, gpg::ConnectionResponse const& response) {
        OnConnectionResponse(client_id, response);
      },
      msg_listener_);
}

マルチプレイヤーゲームといったアプリでは、接続するホストのリストを提示することが適しています。 また、端末が Android TVに接続するときのように、プレイヤーをすぐに接続することを選択することもできます。

端末は一度に複数のエンドポイントに接続できないので、もし端末が既に接続されたものと別のものに接続することを試みたならば、結果は ERROR_ALREADY_CONNECTEDレスポンスになります。 接続された端末上で Disconnect(remote_endpoint_id)を呼び出し、それから接続を再試行することによって、この問題を解決することができます。

ホスト側では、接続している端末からの接続リクエストを処理する必要があります。 接続された端末を追跡し、メッセージを送信するには、あなたが選択したデータ構造内の端末のエンドポイント IDを格納します。 リクエストを受け入れるには、接続を受け入れたいエンドポイントのエンドポイント IDを用いて AcceptConnectionRequest()メソッドを呼び出します。 または、リクエストを拒否するために RejectConnectionRequest()メソッドを呼び出します。

次のコードスニペットは、端末からの接続リクエストを受け入れる方法を示しています:

std::string message = "accepted";
std::vector<uint8_t> payload(message.begin(), message.end());
nearby_connection_->AcceptConnectionRequest(request.remote_endpoint_id, payload, msg_listener_);

ひとたびゲームプレイが進行したならば、stopAdvertising()を呼び出すことによって、あなたの端末を宣伝することを停止することができます。

宣伝している端末との失われた接続を処理する

近くの端末の発見プロセス中に、プレイヤーは彼らのネットワーク接続にて中断に遭遇するかもしれません。 例えば、プレイヤーは WiFiネットワークの範囲外に移動するかもしれません。 もしあなたのアプリがユーザにホストのリストを提示するならば、システムがこのシナリオを検出したとき、リストからそのユーザを単に削除するために IEndpointDiscoveryListener::onEndpointLost()コールバックを使用することができます。

端末のスクリーンをアウェークに維持する

任意の端末に明示的に入力イベントを送信する必要がないので、接続された端末の画面はスリープモードに入るかもしれません。 あなたの Javaコードからのベストプラクティスとして、端末をアウェークに維持するために、あなたのアクティビティにて次のコードを呼び出します:

getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

詳細については、Keeping the Device Awakeおよび LayoutParamsのリファレンスドキュメントを参照してください。