Saved Games - StansAssets/com.stansassets.android-native GitHub Wiki

This guide shows you how to implement saved games using the snapshots API provided by Google Play games services.

If you haven't already done so, you might find it helpful to review the Saved Games game concepts. Familiarize yourself with the recommendations described in Quality Checklist.

Important Notes

Before using the Saved Games service, you must first enable it from the Google Play Console. After performing the steps above, it may take up to 24 hours for Google Play games services to activate the Saved Games feature for your game.

If you want to test the Saved Games feature immediately, manually clear the data in the Google Play Services app installed on your test device.

Warning!!!

When saving a game snapshot, you should provide an AN_SnapshotMetadataChange object. Make sure you set ProgressValue and PlayedTime are always ABOVE ZERO. Otherwise, your whole snapshot may not be saved.

Getting the snapshots client

To start using the snapshots API, your game must first obtain a AN_SnapshotsClient object. You can do this by calling the AN_Games.GetSnapshotsClient() method.

Specifying the Drive scope

Your app must specify the AN_Drive.SCOPE_APPFOLDER scope when building the Google sign-in client. Here’s an example of how to do this in your Sign-in flow. Also, remember that you need to always do the Non-interactive sing-in when your app resumes. You can read more about it in Android Games Sing-in guide.

using SA.Android.GMS.Auth;
using SA.Android.GMS.Games;
using SA.Android.GMS.Drive;
...

private void SignInFlow() {
    AN_Logger.Log("Play Services Sign In started....");
    AN_GoogleSignInOptions.Builder builder = new AN_GoogleSignInOptions.Builder(AN_GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN);

    //Google play documentation syas that
    // you don't need to use this, however, we recommend you still
    // add those option to your Sing In builder. Some version of play service lib
    // may retirn a signed account with all fileds empty if you will not add this.
    // However according to the google documentation this step isn't required
    // So the decision is up to you.
    builder.RequestId();
    builder.RequestEmail();
    builder.RequestProfile();

    // Add the APPFOLDER scope for Snapshot support.
    builder.RequestScope(AN_Drive.SCOPE_APPFOLDER);

    AN_GoogleSignInOptions gso = builder.Build();
    AN_GoogleSignInClient client = AN_GoogleSignIn.GetClient(gso);

    AN_Logger.Log("Let's try Silent SignIn first");
    client.SilentSignIn((result) => {
        if (result.IsSucceeded) {
            AN_Logger.Log("SilentSignIn Succeeded");
            UpdateUIWithAccount(result.Account);
        } else {

            // Player will need to sign-in explicitly using via UI

            AN_Logger.Log("SilentSignIn Failed with code: " + result.Error.Code);
            AN_Logger.Log("Starting the default Sign in flow");

            //Starting the interactive sign-in
            client.SignIn((signInResult) => {
                AN_Logger.Log("Sign In StatusCode: " + signInResult.StatusCode);
                if (signInResult.IsSucceeded) {
                    AN_Logger.Log("SignIn Succeeded");
                    UpdateUIWithAccount(signInResult.Account);
                } else {
                    AN_Logger.Log("SignIn filed: " + signInResult.Error.FullMessage);
                }
            });

        }
    });
}

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.

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

using SA.Android.GMS.Games;
...

var client = AN_Games.GetSnapshotsClient();
client.ShowSelectSnapshotIntent("Hello World!", result => {
    if(result.IsSucceeded) {
        switch (result.State) {
            case AN_SnapshotUIResult.UserInteractionState.EXTRA_SNAPSHOT_METADATA:
                Debug.Log("User choosed to load the Snapshot");
                LoadSnapshot(result.Metadata);
                break;
            case AN_SnapshotUIResult.UserInteractionState.EXTRA_SNAPSHOT_NEW:
                Debug.Log("User choosed to create the Snapshot");
                CreateSnapshot();
                break;
        }
    } else {
        Debug.LogError($"Snapshots UI Failed: {result.Error.FullMessage}");
    }
});

Loading Snapshots

If instead of using the default Google Play UI, you would like to build your own snapshots in-game UI, you can load the snapshots metadata. See the snippet below:

using SA.Android.GMS.Games;
...

var client = AN_Games.GetSnapshotsClient();
client.Load((result) => {
    if(result.IsSucceeded) {
        Debug.Log($"Load Snapshots Succeeded, count: {result.Snapshots.Count}");
        foreach(var meta in result.Snapshots) {
            Debug.Log($"meta.CoverImageUri: {meta.CoverImageUri}");
            Debug.Log($"meta.Title: {meta.Title}");
            Debug.Log($"meta.Description: {meta.Description}");
            Debug.Log($"meta.DeviceName: {meta.DeviceName}");
            Debug.Log($"meta.PlayedTime: {meta.PlayedTime}");
            Debug.Log($"meta.ProgressValue: {meta.ProgressValue}");
        }
    } else {
        Debug.LogError($"Load Snapshots Failed: {result.Error.FullMessage}");
    }
});

As you may notice the AN_SnapshotMetadata object contains CoverImageUri property. The URL will be formatted similarly to the example below:

content://com.google.android.gms.games.background/images/b4d1b8fd/2956

This means you can't use UnityWebRequest to download the image using such URL. Since google play stores all the images locally. In order to obtain an image that you can use in your game, you need to use Image Manager.

Reading and Writing Saved Games

To store content to a saved game:

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

using SA.Android.GMS.Games;
...

var client = AN_Games.GetSnapshotsClient();
var createIfNotFound = true;
var conflictPolicy = AN_SnapshotsClient.ResolutionPolicy.LAST_KNOWN_GOOD;
client.Open(name, createIfNotFound, conflictPolicy, result => {
    if(result.IsSucceeded) {
        Debug.Log("We have snapshot, reading data...");
        var snapshot = result.Data.GetSnapshot();
        byte[] data = snapshot.ReadFully();
        long progress = snapshot.GetMetadata().ProgressValue;
        var base64Text = Convert.ToBase64String(data);
        Debug.Log($"Snapshot data: {base64Text}");
        Debug.Log($"Snapshot progress: {snapshot.GetMetadata().ProgressValue}");
        Debug.Log($"Snapshot played time: {snapshot.GetMetadata().PlayedTime}");
        Debug.Log("Writing data...");
        var mydata = "My game data";
        data = mydata.ToBytes();
        snapshot.WriteBytes(data);
        SA_ScreenUtil.TakeScreenshot(512, screenshot => {
            var changeBuilder = new AN_SnapshotMetadataChange.Builder();
            changeBuilder.SetDescription("Hello Description");
            changeBuilder.SetPlayedTimeMillis(10000);
            changeBuilder.SetProgressValue(progress + 1);
            changeBuilder.SetCoverImage(screenshot);
            AN_SnapshotMetadataChange changes = changeBuilder.Build();
            client.CommitAndClose(snapshot, changes, commitResult => {
                if (commitResult.IsSucceeded) {
                    PrintMeta(commitResult.Metadata);
                } else {
                    Debug.Log("CommitAndClose Snapshots Failed: " + result.Error.FullMessage);
                }
            });
        });
    } else {
        Debug.LogError($"Open Snapshots Failed: {result.Error.FullMessage}");
    }
});

If the player's device is not connected to a network when your app calls AN_SnapshotsClient.CommitAndClose(), Google Play games services stores the saved game data locally on the device. Upon device re-connection, Google Play games services sync the locally cached saved game changes to Google's servers.

Handling saved game conflicts

Android native does not support manual conflicts resolution so far. If for some reason you need this feature, please do not hesitate to request it via contacting us directly at https://stansassets.com/. But Google Play provides auto resolution policy when you try to open the snapshot. Here is the following audio resolution option you have with the Android Native plugin.

LONGEST_PLAYTIME - In the case of a conflict, the snapshot with the longest playing time will be used. In the case of a tie, the last known good snapshot will be chosen instead. This policy is a good choice if the length of play time is a reasonable proxy for the "best" save game. Note that you must use AN_SnapshotMetadataChange.Builder.SetPlayedTimeMillis(long) when saving games for this policy to be meaningful.

LAST_KNOWN_GOOD - In the case of a conflict, the last known good version of this snapshot will be used. This policy is a reasonable choice if your game requires stability from the snapshot data. This policy ensures that only writes which are not contested are seen by the player, which guarantees that all clients converge.

MOST_RECENTLY_MODIFIED - In the case of a conflict, the most recently modified version of this snapshot will be used. This policy is a reasonable choice if your game can tolerate players on multiple devices clobbering their own changes. Because this policy blindly chooses the most recent data, it is possible that a player's changes may get lost.

POLICY_HIGHEST_PROGRESS - In the case of a conflict, the snapshot with the highest progress value will be used. In the case of a tie, the last known good snapshot will be chosen instead. This policy is a good choice if your game uses the progress value of the snapshot to determine the best-saved game. Note that you must use AN_SnapshotMetadataChange.Builder.SetProgressValue(long) when saving games for this policy to be meaningful.

Delete Saved Game

A user always can delete the game save if you present snapshots UI with the allowDelete parameter set to true. But using the Android Native API you can always manage snapshots yourself, or build a custom UI where the user may delete the saved game snapshot. The code example below ill demonstrate how you can delete the first snapshot in the list:

using SA.Android.GMS.Games;
...

var client = AN_Games.GetSnapshotsClient();
client.Load(result => {
    if (result.IsSucceeded) {
        if(result.Snapshots.Count == 0) {
            Debug.LogError("There are no spanpshot's. Can't test delete action");
            return;
        }
        AN_SnapshotMetadata meta = result.Snapshots[0];
        client.Delete(meta, deleteResult => {
            if(deleteResult.IsSucceeded) {
                Debug.Log($"deleteResult.SnapshotId: {deleteResult.SnapshotId}");
            } else {
                Debug.LogError($"Delete Snapshots Failed: {deleteResult.Error.FullMessage}")
            }
        });
    } 
});