This guide shows you how to use the Saved Games API in an Android application.
The API can be found in the com.google.android.gms.games.snapshot package.
Before you begin
If you haven't already done so, you might find it helpful to review the Saved Games game concepts.
Before you start to code using the Saved Games API:
- Install the Google Play Services SDK.
- Download and review the Saved Games code sample.
- Enable the Saved Games service in the Google Play Developer Console.
- Specify the
Drive.SCOPE_APPFOLDERscope andDrive.APIAPI when you create yourGoogleApiClient:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create the Google API Client with access to Plus, Games, and Drive
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(Games.API).addScope(Games.SCOPE_GAMES)
.addApi(Drive.API).addScope(Drive.SCOPE_APPFOLDER)
.build();
}
- Follow the sign-in checklist recommendations.
Once the player is signed in and the GoogleApiClient is
connected, your game can start using the Saved Games API.
Displaying Saved Games
You can integrate the Saved Games 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 should bring up a screen that prompts the players to enter information for a new saved game or select an existing saved game to restore. To simplify your development, the Saved Games 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 bring up the default Saved Games UI:
- Call
getSelectSnapshotIntent()to get anIntentfor launching the default Saved Games selection UI. In the method call, you can set boolean values in theallowAddButtonandallowDeleteparameters to indicate if your game wants the UI to provide buttons to create a new saved game or delete existing saved games. - 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.
The following snippet shows how to bring up the default Saved Games selection UI:
private static final int RC_SAVED_GAMES = 9009;
private void showSavedGamesUI() {
int maxNumberOfSavedGamesToShow = 5;
Intent savedGamesIntent = Games.Snapshots.getSelectSnapshotIntent(mGoogleApiClient,
"See My Saves", true, true, maxNumberOfSavedGamesToShow);
startActivityForResult(savedGamesIntent, 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 a
Snapshot object representing the saved game to your 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(Snapshots.EXTRA_SNAPSHOT_METADATA)) {
// Load a snapshot.
SnapshotMetadata snapshotMetadata = (SnapshotMetadata)
intent.getParcelableExtra(Snapshots.EXTRA_SNAPSHOT_METADATA);
mCurrentSaveName = snapshotMetadata.getUniqueName();
// Load the game data from the Snapshot
// ...
} else if (intent.hasExtra(Snapshots.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, your game must obtain a reference to a
Snapshot object then call open() to get access to modify its
contents. You can store a player's data in byte format by calling
writeBytes().
Once all your modifications are made to the saved game's content or metadata,
call commitAndClose() to send your changes to Google's servers. In the method call, your game can associate 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 PendingResult<Snapshots.CommitSnapshotResult> 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();
// Commit the operation
return Games.Snapshots.commitAndClose(mGoogleApiClient, snapshot, metadataChange);
}
If the player's device is not connected to a network when your app calls
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 all saved games for the currently signed-in player, call
the load() method.
Your game can also retrieve a specific saved game through the player's UI
selection, as described in Displaying Saved Games.
The returned saved game is represented as a Snapshot, which
your game can then open to read its content and metadata.
To improve your game's performance, you are encouraged to perform saved game
loading as a background operation rather than in the main thread. One way to do
this is by using an AsyncTask
and override its doInBackground() method to open the saved game.
The following snippet shows how you might implement the AsyncTask to load a specific saved game:
private byte[] mSaveGameData;
void loadFromSnapshot() {
// Display a progress dialog
// ...
AsyncTask<Void, Void, Integer> task = new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... params) {
// Open the saved game using its name.
Snapshots.OpenSnapshotResult result = Games.Snapshots.open(mGoogleApiClient,
mCurrentSaveName, true).await();
// Check the result of the open operation
if (result.getStatus().isSuccess()) {
Snapshot snapshot = result.getSnapshot();
// Read the byte content of the saved game.
try {
mSaveGameData = snapshot.getSnapshotContents().readFully();
} catch (IOException e) {
Log.e(TAG, "Error while reading Snapshot.", e);
}
} else{
Log.e(TAG, "Error while loading: " + result.getStatus().getStatusCode());
}
return result.getStatus().getStatusCode();
}
@Override
protected void onPostExecute(Integer status) {
// Dismiss progress dialog and reflect the changes in the UI.
// ...
}
};
task.execute();
}
Handling saved game conflicts
When using the Saved Games service 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 Saved Games services 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, it notifies your game
during a saved game open operation by returning a status code of
STATUS_SNAPSHOT_CONFLICT.
In this event, the OpenSnapshotResult
provides two versions of the saved game:
- The most-up-to-date version known by Google Play games services to be accurate; and
- 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, follow these steps:
- Call
Snapshots.open(). IfSTATUS_SNAPSHOT_CONFLICTis returned, you have a conflict to resolve. - Call
OpenSnapshotResult.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
getConflictingSnapshot()to get the modified version. - Call
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
Snapshots.resolveConflict()method.
The following snippet shows and example of how your game might handle a saved game conflict by selecting the newest saved game as the final version to save:
private static final int MAX_SNAPSHOT_RESOLVE_RETRIES = 3;
/**
* Conflict resolution for when Snapshots are opened. Must be run in an AsyncTask or in a
* background thread,
*/
Snapshot processSnapshotOpenResult(Snapshots.OpenSnapshotResult result, int retryCount) {
Snapshot mResolvedSnapshot = null;
retryCount++;
int status = result.getStatus().getStatusCode();
Log.i(TAG, "Save Result status: " + status);
if (status == GamesStatusCodes.STATUS_OK) {
return result.getSnapshot();
} else if (status == GamesStatusCodes.STATUS_SNAPSHOT_CONTENTS_UNAVAILABLE) {
return result.getSnapshot();
} else if (status == GamesStatusCodes.STATUS_SNAPSHOT_CONFLICT) {
Snapshot snapshot = result.getSnapshot();
Snapshot conflictSnapshot = result.getConflictingSnapshot();
// Resolve between conflicts by selecting the newest of the conflicting snapshots.
mResolvedSnapshot = snapshot;
if (snapshot.getMetadata().getLastModifiedTimestamp() <
conflictSnapshot.getMetadata().getLastModifiedTimestamp()) {
mResolvedSnapshot = conflictSnapshot;
}
Snapshots.OpenSnapshotResult resolveResult = Games.Snapshots.resolveConflict(
mGoogleApiClient, result.getConflictId(), mResolvedSnapshot).await();
if (retryCount < MAX_SNAPSHOT_RESOLVE_RETRIES) {
// Recursively attempt again
return processSnapshotOpenResult(resolveResult, retryCount);
} else {
// Failed, log error and show Toast to the user
String message = "Could not resolve snapshot conflicts";
Log.e(TAG, message);
Toast.makeText(getBaseContext(), message, Toast.LENGTH_LONG).show();
}
}
// Fail, return null.
return null;
}
Modifying a saved game for conflict resolution
If you want to merge data from the two Saved Games or modify an existing
Snapshot to save to the server as the resolved final version, follow these steps:
- Pick a
Snapshotobject fromgetConflictingSnapshot()orgetSnapshot()as your base. - Next, call
Snapshots.resolveConflict()and pass in theSnapshotthat you selected in the previous step. This stores theSnapshotto the server. - Call
open()to retrieve theSnapshotthat you just stored in the previous step. Now make your modifications to the returnedSnapshot, then callSnapshots.commitAndClose()to upload the modified save game to the server.
Migrating from the AppState API
If your game uses the Cloud Save (AppState) API to store player
data to Google Play games services, you should migrate your code to use the Saved
Games API as soon as possible. The following table lists the code changes that
you should be aware of when migrating your game:
| Operation | Cloud Save | Saved Games |
|---|---|---|
| Setup | In your Android.xml manifest, declare
com.google.android.gms.appstate.APP_ID. |
|
| Load | Call AppStateManager.load(). |
Call Snapshots.open(). |
| Save | Call AppStateManager.update(). |
Call Snapshot.writeBytes() followed by
Snapshots.commitAndClose(). |
| Resolve conflict | Call AppStateManager.resolve(). |
Call Snapshots.resolveConflict(). |