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.
- Make sure to enable saved games support for your game in the Google Play Console.
- Download and review the saved games code sample in the Android samples page.
- Familiarize yourself with the recommendations described in Quality Checklist.
Getting 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.
Specifying 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 } } }); }
Displaying 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:
- Call
SnapshotsClient.getSelectSnapshotIntent()
to get anIntent
for launching the default saved games selection UI. - Call
startActivityForResult()
and pass in thatIntent
. 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 // ... } } }
Writing saved games
To store content to a saved game:
- Asynchronously open a snapshot via
SnapshotsClient.open()
. Then, retrieve theSnapshot
object from the task's result by callingSnapshotsClient.DataOrConflict.getData()
. - Retrieve a
SnapshotContents
instance viaSnapshotsClient.SnapshotConflict
. - Call
SnapshotContents.writeBytes()
to store the player's data in byte format. - 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 aSnapshotMetaDataChange
object, which your game creates usingSnapshotMetadataChange.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.
Loading saved games
To retrieve saved games for the currently signed-in player:
- Asynchronously open a snapshot via
SnapshotsClient.open()
. Then, retrieve theSnapshot
object from the task's result by callingSnapshotsClient.DataOrConflict.getData()
. Alternatively, your game can also retrieve a specific snapshot through the saved games selection UI, as described in Displaying Saved Games. - Retrieve the
SnapshotContents
instance viaSnapshotsClient.SnapshotConflict
. - 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. // ... } }); }
Handling 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:
- Call
SnapshotsClient.open()
. The task result contains aSnapshotsClient.DataOrConflict
class. - Call the
SnapshotsClient.DataOrConflict.isConflict()
method. If the result is true, you have a conflict to resolve. - Call
SnapshotsClient.DataOrConflict.getConflict()
to retrieve aSnaphotsClient.snapshotConflict
instance. - 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. - Call
SnapshotsClient.SnapshotConflict.getConflictingSnapshot()
to get the local version. - Call
SnapshotsClient.SnapshotConflict.getSnapshot()
to get the server version. - 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"); } } }); }
Modifying 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:
- Call
SnapshotsClient.open()
. - Call
SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent()
to get a newSnapshotContents
object. - Merge the data from
SnapshotsClient.SnapshotConflict.getConflictingSnapshot()
andSnapshotsClient.SnapshotConflict.getSnapshot()
into theSnapshotContents
object from the previous step. - Optionally, create a
SnapshotMetadataChange
instance if there are any changes to the metadata fields. - Call
SnapshotsClient.resolveConflict()
. In your method call, pass inSnapshotsClient.SnapshotConflict.getConflictId()
as the first argument, and theSnapshotMetadataChange
andSnapshotContents
objects that you modified earlier as the second and third arguments respectively. - If the
SnapshotsClient.resolveConflict()
call is successful, the API stores theSnapshot
object to the server and attempts to open the Snapshot object on your local device.- If there is a conflict,
SnapshotsClient.DataOrConflict.isConflict()
returnstrue
. In this case, your game should return to step 2 and repeat the steps to modify the snapshot until conflicts are resolved. - If there’s no conflict,
SnapshotsClient.DataOrConflict.isConflict()
returnsfalse
and theSnapshot
object is open for your game to modify.
- If there is a conflict,