Saved games support in Android games

This guide shows you how to implement saved games game using the snapshots API provided by Google Play Games Services. The APIs can be found in the com.google.android.gms.games.snapshot and com.google.android.gms.games packages.

Before you begin

If you haven't already done so, you might find it helpful to review the Saved Games game concepts.

Get the snapshots client

To start using the snapshots API, your game must first obtain a SnapshotsClient object. You can do this by calling the Games.getSnapshotsClient() method and passing in the activity and the GoogleSignInAccount for the current player. To learn how to retrieve the player account information, see Sign-in in Android Games.

Specify the Drive scope

The snapshots API relies on the Google Drive API for saved games storage. To access the Drive API, your app must specify the Drive.SCOPE_APPFOLDER scope when building the Google sign-in client.

Here’s an example of how to do this in the onResume() method for your sign-in activity:

private GoogleSignInClient mGoogleSignInClient;

@Override
protected void onResume() {
  super.onResume();
  signInSilently();
}

private void signInSilently() {
  GoogleSignInOptions signInOption =
      new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
          // Add the APPFOLDER scope for Snapshot support.
          .requestScopes(Drive.SCOPE_APPFOLDER)
          .build();

  GoogleSignInClient signInClient = GoogleSignIn.getClient(this, signInOption);
  signInClient.silentSignIn().addOnCompleteListener(this,
      new OnCompleteListener<GoogleSignInAccount>() {
        @Override
        public void onComplete(@NonNull Task<GoogleSignInAccount> task) {
          if (task.isSuccessful()) {
            onConnected(task.getResult());
          } else {
            // Player will need to sign-in explicitly using via UI
          }
        }
      });
}

Display saved games

You can integrate the snapshots API wherever your game provides players with the option to save or restore their progress. Your game might display such an option at designated save/restore points or allow players to save or restore progress at any time.

Once players select the save/restore option in your game, your game can optionally bring up a screen that prompts players to enter information for a new saved game or to select an existing saved game to restore.

To simplify your development, the snapshots API provides a default saved games selection user interface (UI) that you can use out-of-the-box. The saved games selection UI allows players to create a new saved game, view details about existing saved games, and load previous saved games.

To launch the default Saved Games UI:

  1. Call SnapshotsClient.getSelectSnapshotIntent() to get an Intent for launching the default saved games selection UI.
  2. Call startActivityForResult() and pass in that Intent. If the call is successful, the game displays the saved game selection UI, along with the options you specified.

Here’s an example of how to launch the default saved games selection UI:

private static final int RC_SAVED_GAMES = 9009;

private void showSavedGamesUI() {
  SnapshotsClient snapshotsClient =
      Games.getSnapshotsClient(this, GoogleSignIn.getLastSignedInAccount(this));
  int maxNumberOfSavedGamesToShow = 5;

  Task<Intent> intentTask = snapshotsClient.getSelectSnapshotIntent(
      "See My Saves", true, true, maxNumberOfSavedGamesToShow);

  intentTask.addOnSuccessListener(new OnSuccessListener<Intent>() {
    @Override
    public void onSuccess(Intent intent) {
      startActivityForResult(intent, RC_SAVED_GAMES);
    }
  });
}

If the player selects to create a new saved game or load an existing saved game, the UI sends a request to Google Play Games Services. If the request is successful, Google Play Games Services returns information to create or restore the saved game through the onActivityResult() callback. Your game can override this callback to check if any errors occurred during request.

The following code snippet shows a sample implementation of onActivityResult():

private String mCurrentSaveName = "snapshotTemp";

/**
 * This callback will be triggered after you call startActivityForResult from the
 * showSavedGamesUI method.
 */
@Override
protected void onActivityResult(int requestCode, int resultCode,
                                Intent intent) {
  if (intent != null) {
    if (intent.hasExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA)) {
      // Load a snapshot.
      SnapshotMetadata snapshotMetadata =
          intent.getParcelableExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA);
      mCurrentSaveName = snapshotMetadata.getUniqueName();

      // Load the game data from the Snapshot
      // ...
    } else if (intent.hasExtra(SnapshotsClient.EXTRA_SNAPSHOT_NEW)) {
      // Create a new snapshot named with a unique string
      String unique = new BigInteger(281, new Random()).toString(13);
      mCurrentSaveName = "snapshotTemp-" + unique;

      // Create the new snapshot
      // ...
    }
  }
}

Write saved games

To store content to a saved game:

  1. Asynchronously open a snapshot via SnapshotsClient.open(). Then, retrieve the Snapshot object from the task's result by calling SnapshotsClient.DataOrConflict.getData().
  2. Retrieve a SnapshotContents instance via SnapshotsClient.SnapshotConflict.
  3. Call SnapshotContents.writeBytes() to store the player's data in byte format.
  4. Once all your changes are written, call SnapshotsClient.commitAndClose() to send your changes to Google's servers. In the method call, your game can optionally provide additional information to tell Google Play Games Services how to present this saved game to players. This information is represented in a SnapshotMetaDataChange object, which your game creates using SnapshotMetadataChange.Builder.

The following snippet shows how your game might commit changes to a saved game:

private Task<SnapshotMetadata> writeSnapshot(Snapshot snapshot,
                                             byte[] data, Bitmap coverImage, String desc) {

  // Set the data payload for the snapshot
  snapshot.getSnapshotContents().writeBytes(data);

  // Create the change operation
  SnapshotMetadataChange metadataChange = new SnapshotMetadataChange.Builder()
      .setCoverImage(coverImage)
      .setDescription(desc)
      .build();

  SnapshotsClient snapshotsClient =
      Games.getSnapshotsClient(this, GoogleSignIn.getLastSignedInAccount(this));

  // Commit the operation
  return snapshotsClient.commitAndClose(snapshot, metadataChange);
}

If the player's device is not connected to a network when your app calls SnapshotsClient.commitAndClose(), Google Play Games Services stores the saved game data locally on the device. Upon device re-connection, Google Play Games Services syncs the locally cached saved game changes to Google's servers.

Load saved games

To retrieve saved games for the currently signed-in player:

  1. Asynchronously open a snapshot via SnapshotsClient.open(). Then, retrieve the Snapshot object from the task's result by calling SnapshotsClient.DataOrConflict.getData(). Alternatively, your game can also retrieve a specific snapshot through the saved games selection UI, as described in Display saved games.
  2. Retrieve the SnapshotContents instance via SnapshotsClient.SnapshotConflict.
  3. Call SnapshotContents.readFully() to read the contents of the snapshot.

The following snippet shows how you might load a specific saved game:

Task<byte[]> loadSnapshot() {
  // Display a progress dialog
  // ...

  // Get the SnapshotsClient from the signed in account.
  SnapshotsClient snapshotsClient =
      Games.getSnapshotsClient(this, GoogleSignIn.getLastSignedInAccount(this));

  // In the case of a conflict, the most recently modified version of this snapshot will be used.
  int conflictResolutionPolicy = SnapshotsClient.RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED;

  // Open the saved game using its name.
  return snapshotsClient.open(mCurrentSaveName, true, conflictResolutionPolicy)
      .addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception e) {
          Log.e(TAG, "Error while opening Snapshot.", e);
        }
      }).continueWith(new Continuation<SnapshotsClient.DataOrConflict<Snapshot>, byte[]>() {
        @Override
        public byte[] then(@NonNull Task<SnapshotsClient.DataOrConflict<Snapshot>> task) throws Exception {
          Snapshot snapshot = task.getResult().getData();

          // Opening the snapshot was a success and any conflicts have been resolved.
          try {
            // Extract the raw data from the snapshot.
            return snapshot.getSnapshotContents().readFully();
          } catch (IOException e) {
            Log.e(TAG, "Error while reading Snapshot.", e);
          }

          return null;
        }
      }).addOnCompleteListener(new OnCompleteListener<byte[]>() {
        @Override
        public void onComplete(@NonNull Task<byte[]> task) {
          // Dismiss progress dialog and reflect the changes in the UI when complete.
          // ...
        }
      });
}

Handle saved game conflicts

When using the snapshots API in your game, it is possible for multiple devices to perform reads and writes on the same saved game. In the event that a device temporarily loses its network connection and later reconnects, this might cause data conflicts whereby the saved game stored on a player's local device is out-of-sync with the remote version stored in Google's servers.

The snapshots API provides a conflict resolution mechanism that presents both sets of conflicting saved games at read-time and lets you implement a resolution strategy that is appropriate for your game.

When Google Play Games Services detects a data conflict, the SnapshotsClient.DataOrConflict.isConflict() method returns a value of true In this event, the SnapshotsClient.SnapshotConflict class provides two versions of the saved game:

  • Server version: The most-up-to-date version known by Google Play Games Services to be accurate for the player’s device; and
  • Local version: A modified version detected on one of the player's devices that contains conflicting content or metadata. This may not be the same as the version that you tried to save.

Your game must decide how to resolve the conflict by picking one of the provided versions or merging the data of the two saved game versions.

To detect and resolve saved game conflicts:

  1. Call SnapshotsClient.open(). The task result contains a SnapshotsClient.DataOrConflict class.
  2. Call the SnapshotsClient.DataOrConflict.isConflict() method. If the result is true, you have a conflict to resolve.
  3. Call SnapshotsClient.DataOrConflict.getConflict() to retrieve a SnaphotsClient.snapshotConflict instance.
  4. Call SnapshotsClient.SnapshotConflict.getConflictId() to retrieve the conflict ID that uniquely identifies the detected conflict. Your game needs this value to send a conflict resolution request later.
  5. Call SnapshotsClient.SnapshotConflict.getConflictingSnapshot() to get the local version.
  6. Call SnapshotsClient.SnapshotConflict.getSnapshot() to get the server version.
  7. To resolve the saved game conflict, select a version that you want to save to the server as the final version, and pass it to the SnapshotsClient.resolveConflict() method.

The following snippet shows and example of how your game might handle a saved game conflict by selecting the most recently modified saved game as the final version to save:

private static final int MAX_SNAPSHOT_RESOLVE_RETRIES = 10;

Task<Snapshot> processSnapshotOpenResult(SnapshotsClient.DataOrConflict<Snapshot> result,
                                         final int retryCount) {

  if (!result.isConflict()) {
    // There was no conflict, so return the result of the source.
    TaskCompletionSource<Snapshot> source = new TaskCompletionSource<>();
    source.setResult(result.getData());
    return source.getTask();
  }

  // There was a conflict.  Try resolving it by selecting the newest of the conflicting snapshots.
  // This is the same as using RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED as a conflict resolution
  // policy, but we are implementing it as an example of a manual resolution.
  // One option is to present a UI to the user to choose which snapshot to resolve.
  SnapshotsClient.SnapshotConflict conflict = result.getConflict();

  Snapshot snapshot = conflict.getSnapshot();
  Snapshot conflictSnapshot = conflict.getConflictingSnapshot();

  // Resolve between conflicts by selecting the newest of the conflicting snapshots.
  Snapshot resolvedSnapshot = snapshot;

  if (snapshot.getMetadata().getLastModifiedTimestamp() <
      conflictSnapshot.getMetadata().getLastModifiedTimestamp()) {
    resolvedSnapshot = conflictSnapshot;
  }

  return Games.getSnapshotsClient(theActivity, GoogleSignIn.getLastSignedInAccount(this))
      .resolveConflict(conflict.getConflictId(), resolvedSnapshot)
      .continueWithTask(
          new Continuation<
              SnapshotsClient.DataOrConflict<Snapshot>,
              Task<Snapshot>>() {
            @Override
            public Task<Snapshot> then(
                @NonNull Task<SnapshotsClient.DataOrConflict<Snapshot>> task)
                throws Exception {
              // Resolving the conflict may cause another conflict,
              // so recurse and try another resolution.
              if (retryCount < MAX_SNAPSHOT_RESOLVE_RETRIES) {
                return processSnapshotOpenResult(task.getResult(), retryCount + 1);
              } else {
                throw new Exception("Could not resolve snapshot conflicts");
              }
            }
          });
}

Modify saved games for conflict resolution

If you want to merge data from multiple saved games or modify an existing Snapshot to save to the server as the resolved final version, follow these steps:

  1. Call SnapshotsClient.open() .
  2. Call SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent() to get a new SnapshotContents object.
  3. Merge the data from SnapshotsClient.SnapshotConflict.getConflictingSnapshot() and SnapshotsClient.SnapshotConflict.getSnapshot() into the SnapshotContents object from the previous step.
  4. Optionally, create a SnapshotMetadataChange instance if there are any changes to the metadata fields.
  5. Call SnapshotsClient.resolveConflict(). In your method call, pass in SnapshotsClient.SnapshotConflict.getConflictId() as the first argument, and the SnapshotMetadataChange and SnapshotContents objects that you modified earlier as the second and third arguments respectively.
  6. If the SnapshotsClient.resolveConflict() call is successful, the API stores the Snapshot object to the server and attempts to open the Snapshot object on your local device.