Adding Nearby Connections to Your Game

This guide shows you how to use the Nearby Connections API in your C++ application. This API enables your app to easily discover other devices on a local network, connect to them, and exchange messages with them in real time. This functionality is especially useful for two types of user experiences:

  • Local multiplayer gaming: Allows one player to set up a local multiplayer game that other players on the network can join. Your app can also allow a player to start an in-game mission when enough nearby participants join.
  • Multi-screen gaming: Allows players to use their phone or tablet as a game controller to play games displayed on a nearby large-screen Android device, such as Android TV. Your app can also enable players to see a customized game screen on their personal device while all nearby participants see a shared common view on a table-top Android device.

This API is part of the Play Games C++ SDK, and and lets you build games that can join multiple nearby Android devices together on a WiFi network. One device advertises on the network as the host, while other devices act as clients and send connection requests to the host.

Before you begin

Before you start to code using the Nearby Connections API:

  • Install the Google Play Services SDK.
  • Download and review the Nearby Connections API code sample.
  • Connect multiple Android devices to the same WiFi network with multicast enabled.

Once the system builds and configures a NearbyConnections object, your game can use the Nearby Connections API.

Initializing the Nearby Connections API client for nearby connections

To access the Nearby Connections API, you first need to initialize it. Do this by using the Builder class to construct a NearbyConnections object. The following example shows you how to do this.

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);

Validating network connectivity

Before using the Nearby Connections API, you should check to ensure the devices are connected to a WiFi network. If they are not connected, prompt players to connect to a WiFi network.

To detect the network state, first add the following permission to your manifest:

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

The following Java code example shows you how to determine whether a device is connected to 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;

Call this method before you start to advertise your device or set your device to discovery mode as described in the sections below.

Advertising your device

To establish a connection between two or more devices, one device must advertise its service and one or more devices must request to connect to it. The advertising device is the 'host' in a multiplayer game, while the connecting device is the client.

To enable your app to advertise itself with the Nearby Connections API, add the following to your manifest:

<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>

The service_id value allows client devices to discover the advertising device. The value must uniquely identify your app. As a best practice, use the package name of your app (for example, com.google.example.mygame.service).

The following example shows you how to declare the service_id value in the strings.xml file:

<?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>

When a player starts the game and chooses to host, call startAdvertising() to advertise the device. The following example shows you how you can advertise the host player’s device:

 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());
     });

The simplest way to allow devices to reconnect after disconnecting is to allow the host to continue advertising until a game ends.

Discovering other devices

The discovery process allows a device to find nearby devices that are advertising connections for a specific service ID. The service ID parameter you pass into startDiscovery() should match the value provided in the manifest of the advertising app. See Advertising your device for information on how apps advertise nearby games.

The following example shows how to initiate the discovery process in your app:

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

In the following section, you learn how to handle the connections once the client device discovers the host device.

Identifying connected devices

There are two ways that the Nearby Connections API identifies devices:

  • Device ID: This identifies the device uniquely and remains the same even when the device is rebooted. Obtain a local device ID using GetLocalDeviceID
  • Endpoint ID: This identifies the device locally to a NearbyConnections instance. Obtain a local endpoint ID using GetLocalEndpointID.

The endpoint ID is required to establish connections and send messages. For example, if the player on device X is directing an attack at the player on device Y, you would send the message from device X to the host and include device X’s endpoint ID so that device Y can know where the attack originated.

You can use the device ID for cases such as:

  • Identifying the player and restoring their appropriate data.
  • Reconnecting to a host that the client has previously connected to during discovery (for example, reconnecting to your Android TV to continue your quest the following day).
  • Trying to reconnect to a host after losing connectivity with it (for example, returning to a multiplayer game after receiving a phone call).

In the next section, you learn how to send messages to connected devices.

Sending messages through nearby connections

Once connections are established between devices, they can send and receive messages to update game state and transfer input, data, or events from one device to another. Hosts can send messages to any number of clients, and clients can send messages to the host. If a client needs to communicate with other clients, you can send a message to the host to relay the information to the recipient client.

Sending the message

To send a reliable message, call sendReliableMessage() and pass in the appropriate endpoint ID. The payload parameter holds your message data. Reliable messages are guaranteed to be received in the order they were sent, and the system retries sending the message until the connection ends.

The following code snippet shows how you can send reliable messages from one device to another:

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);

To send an unreliable message, call sendUnreliableMessage(). This method allows the system to deliver messages quickly, but the messages may be lost or sent out of order. Unreliable messaging is suitable for frequent and less important game notifications, such as showing an opposing player’s character position on a map.

Receiving the message

After the message is sent, the system receives the message through IMessageListener::onMessageReceived(). Based on the requirements of your game, you can implement this method to update game information such as player scores, on-screen drawings, or the current player's turn.

The following code snippet shows how to implement this method to handle received messages:

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;
        }
    }
  }
}

Disconnecting nearby connections

When a player quits the game, or if the host wants to force a player to quit, call Disconnect().

The following snippet shows you how to pass in the endpoint ID of the specific device that you wish to disconnect:

nearby_connection_->Disconnect(remote_endpoint_id);

If the host decides to stop advertising and disconnect all remote endpoints, call Stop(). More generally, it is a best practice to use this method when you are finished with a connection. For example, when:

  • You want a client device to stop discovery or disconnect from the host.
  • Ending the activity.
  • Exiting the app.
  • Destructing the NearbyConnections object.

The following example shows how you might call Stop():

nearby_connection_->Stop();

When the NearbyConnections object is disconnected on a device, and you call Disconnect(), or Stop(), the system triggers the IMessageListener::onDisconnected() callback method on the device it was connected to. Use this method to gracefully handle disconnection.

Reconnecting nearby connections

Players and hosts can lose connectivity because of network interruptions, or by simply walking outside of the WiFi network range. Depending on your game design, you may want to reconnect a disconnected player while the game is still ongoing or pause the game and restart a player's turn when the player attempts to reconnect.

To enable your app to reconnect devices, you should store both a player's device ID and endpoint ID. When storing player data such as a position or score, identify the player by device ID so that the data can be restored after a disconnection even if the endpoint ID changes.

If the host is still advertising, the player can reconnect using the service ID.

Completing the connection

Once your app discovers another app that is advertising the requested service ID, you can initiate a connection between the devices. The following example shows you how to handle discovery of a device:

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_);
}

For apps such as multiplayer games, it is suitable to present a list of hosts to connect to. Alternatively, you could also choose to immediately connect the player, such as when a device connects to an Android TV.

Because devices cannot connect to more than one endpoint at a time, if a device tries to connect to another one that is already connected, the result will be an ERROR_ALREADY_CONNECTED response. You can solve this problem by calling Disconnect(remote_endpoint_id) on the connected device, and then re-attempting the connection.

On the host side, you need to handle connection requests from the connecting devices. To keep track of and send messages to connected devices, store the device endpoint IDs in a data construct of your choosing. To accept a request, call the AcceptConnectionRequest() method with the endpoint ID of the endpoint you wish to accept a connection to. Alternatively, call the RejectConnectionRequest() method to reject a request.

The following code snippet shows how you might accept connection requests from devices:

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

Once gameplay is underway, you can stop advertising your device by calling stopAdvertising().

Handling lost connection with advertising device

During the nearby device discovery process, players may encounter disruptions in their network connection. For example, a player might move out of the range of the WiFi network. If your app presents a list of hosts to the user, you can use the IEndpointDiscoveryListener::onEndpointLost() callback to simply remove that user from the list when the system detects this scenario.

Keeping the device screens awake

Screens of the connected devices may go into sleep mode because you do not explicitly need to send input events to any of the devices. As a best practice from your Java code, call the following code in your activity to keep the devices awake:

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

For more information, see Keeping the Device Awake and the reference documentation for LayoutParams.

Send feedback about...

Play Games Services for C++
Play Games Services for C++