GPG Tutorial – Saved Games (Code)

This post contains all the code that’s been written in the YouTube video.

You can read it at your own pace, although watching the video prior to looking at this code example is recommended.

PlayGamesScript.cs

using GooglePlayGames;
using GooglePlayGames.BasicApi;
using GooglePlayGames.BasicApi.SavedGame;
using System.Text;
using UnityEngine;

public class PlayGamesScript : MonoBehaviour
{

    public static PlayGamesScript Instance { get; private set; }

    const string SAVE_NAME = "Tutorial";
    bool isSaving;
    bool isCloudDataLoaded = false;

    // Use this for initialization
    void Start()
    {
        Instance = this;
        //setting default value, if the game is played for the first time
        if (!PlayerPrefs.HasKey(SAVE_NAME))
            PlayerPrefs.SetString(SAVE_NAME, "0");
        //tells us if it's the first time that this game has been launched after install - 0 = no, 1 = yes 
        if (!PlayerPrefs.HasKey("IsFirstTime"))
            PlayerPrefs.SetInt("IsFirstTime", 1);

        LoadLocal(); //we want to load local data first because loading from cloud can take quite a while, if user progresses while using local data, it will all
                     //sync in our comparating loop in StringToGameData(string, string)

        PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder()
            .EnableSavedGames().Build();
        PlayGamesPlatform.InitializeInstance(config);
        PlayGamesPlatform.Activate();

        SignIn();
    }

    void SignIn()
    {
        //when authentication process is done (successfuly or not), we load cloud data
        Social.localUser.Authenticate(success => { LoadData(); });
    }

    #region Saved Games
    //making a string out of game data (highscores...)
    string GameDataToString()
    {
        return CloudVariables.Highscore.ToString();
    }

    //this overload is used when user is connected to the internet
    //parsing string to game data (stored in CloudVariables), also deciding if we should use local or cloud save
    void StringToGameData(string cloudData, string localData)
    {
        //if it's the first time that game has been launched after installing it and successfuly logging into Google Play Games
        if (PlayerPrefs.GetInt("IsFirstTime") == 1)
        {
            //set playerpref to be 0 (false)
            PlayerPrefs.SetInt("IsFirstTime", 0);
            if (int.Parse(cloudData) > int.Parse(localData)) //cloud save is more up to date
            {
                //set local save to be equal to the cloud save
                PlayerPrefs.SetString(SAVE_NAME, cloudData);
            }
        }
        //if it's not the first time, start comparing
        else
        {
            //comparing integers, if one int has higher score in it than the other, we update it
            if (int.Parse(localData) > int.Parse(cloudData))
            {
                //update the cloud save, first set CloudVariables to be equal to localSave
                CloudVariables.Highscore = int.Parse(localData);
                //also send the more up to date high score to leaderboard
                AddScoreToLeaderboard(GPGSIds.leaderboard_leaderboard, CloudVariables.Highscore);
                isCloudDataLoaded = true;
                //saving the updated CloudVariables to the cloud
                SaveData();
                return;
            }
        }
        //if the code above doesn't trigger return and the code below executes,
        //cloud save and local save are identical, so we can load either one
        CloudVariables.Highscore = int.Parse(cloudData);
        isCloudDataLoaded = true;
    }

    //this overload is used when there's no internet connection - loading only local data
    void StringToGameData(string localData)
    {
        CloudVariables.Highscore = int.Parse(localData);
    }

    //used for loading data from the cloud or locally
    public void LoadData()
    {
        //basically if we're connected to the internet, do everything on the cloud
        if (Social.localUser.authenticated)
        {
            isSaving = false;
            ((PlayGamesPlatform)Social.Active).SavedGame.OpenWithManualConflictResolution(SAVE_NAME,
                DataSource.ReadCacheOrNetwork, true, ResolveConflict, OnSavedGameOpened);
        }
        //this will basically only run in Unity Editor, as on device,
        //localUser will be authenticated even if he's not connected to the internet (if the player is using GPG)
        else
        {
            LoadLocal();
        }
    }

    private void LoadLocal()
    {
        StringToGameData(PlayerPrefs.GetString(SAVE_NAME));
    }

    //used for saving data to the cloud or locally
    public void SaveData()
    {
        //if we're still running on local data (cloud data has not been loaded yet), we also want to save only locally
        if (!isCloudDataLoaded)
        {
            SaveLocal();
            return;
        }
        //same as in LoadData
        if (Social.localUser.authenticated)
        {
            isSaving = true;
            ((PlayGamesPlatform)Social.Active).SavedGame.OpenWithManualConflictResolution(SAVE_NAME,
                DataSource.ReadCacheOrNetwork, true, ResolveConflict, OnSavedGameOpened);
        }
        else
        {
            SaveLocal();
        }
    }

    private void SaveLocal()
    {
        PlayerPrefs.SetString(SAVE_NAME, GameDataToString());
    }

    private void ResolveConflict(IConflictResolver resolver, ISavedGameMetadata original, byte[] originalData,
        ISavedGameMetadata unmerged, byte[] unmergedData)
    {
        if (originalData == null)
            resolver.ChooseMetadata(unmerged);
        else if (unmergedData == null)
            resolver.ChooseMetadata(original);
        else
        {
            //decoding byte data into string
            string originalStr = Encoding.ASCII.GetString(originalData);
            string unmergedStr = Encoding.ASCII.GetString(unmergedData);
            
            //parsing
            int originalNum = int.Parse(originalStr);
            int unmergedNum = int.Parse(unmergedStr);

            //if original score is greater than unmerged
            if (originalNum > unmergedNum)
            {
                resolver.ChooseMetadata(original);
                return;
            }
            //else (unmerged score is greater than original)
            else if (unmergedNum > originalNum)
            {
                resolver.ChooseMetadata(unmerged);
                return;
            }
            //if return doesn't get called, original and unmerged are identical
            //we can keep either one
            resolver.ChooseMetadata(original);
        }
    }

    private void OnSavedGameOpened(SavedGameRequestStatus status, ISavedGameMetadata game)
    {
        //if we are connected to the internet
        if (status == SavedGameRequestStatus.Success)
        {
            //if we're LOADING game data
            if (!isSaving)
                LoadGame(game);
            //if we're SAVING game data
            else
                SaveGame(game);
        }
        //if we couldn't successfully connect to the cloud, runs while on device,
        //the same code that is in else statements in LoadData() and SaveData()
        else
        {
            if (!isSaving)
                LoadLocal();
            else
                SaveLocal();
        }
    }

    private void LoadGame(ISavedGameMetadata game)
    {
        ((PlayGamesPlatform)Social.Active).SavedGame.ReadBinaryData(game, OnSavedGameDataRead);
    }

    private void SaveGame(ISavedGameMetadata game)
    {
        string stringToSave = GameDataToString();
        //saving also locally (can also call SaveLocal() instead)
        PlayerPrefs.SetString(SAVE_NAME, stringToSave);

        //encoding to byte array
        byte[] dataToSave = Encoding.ASCII.GetBytes(stringToSave);
        //updating metadata with new description
        SavedGameMetadataUpdate update = new SavedGameMetadataUpdate.Builder().Build();
        //uploading data to the cloud
        ((PlayGamesPlatform)Social.Active).SavedGame.CommitUpdate(game, update, dataToSave,
            OnSavedGameDataWritten);
    }

    //callback for ReadBinaryData
    private void OnSavedGameDataRead(SavedGameRequestStatus status, byte[] savedData)
    {
        //if reading of the data was successful
        if (status == SavedGameRequestStatus.Success)
        {
            string cloudDataString;
            //if we've never played the game before, savedData will have length of 0
            if (savedData.Length == 0)
            //in such case, we want to assign "0" to our string
                cloudDataString = "0";
            //otherwise take the byte[] of data and encode it to string
            else
                cloudDataString =  Encoding.ASCII.GetString(savedData);
            
            //getting local data (if we've never played before on this device, localData is already
            //"0", so there's no need for checking as with cloudDataString)
            string localDataString = PlayerPrefs.GetString(SAVE_NAME);
            
            //this method will compare cloud and local data
            StringToGameData(cloudDataString, localDataString);
        }
    }

    //callback for CommitUpdate
    private void OnSavedGameDataWritten(SavedGameRequestStatus status, ISavedGameMetadata game)
    {

    }
    #endregion /Saved Games

    #region Achievements
    public static void UnlockAchievement(string id)
    {
        Social.ReportProgress(id, 100, success => { });
    }

    public static void IncrementAchievement(string id, int stepsToIncrement)
    {
        PlayGamesPlatform.Instance.IncrementAchievement(id, stepsToIncrement, success => { });
    }

    public static void ShowAchievementsUI()
    {
        Social.ShowAchievementsUI();
    }
    #endregion /Achievements

    #region Leaderboards
    public static void AddScoreToLeaderboard(string leaderboardId, long score)
    {
        Social.ReportScore(score, leaderboardId, success => { });
    }

    public static void ShowLeaderboardsUI()
    {
        Social.ShowLeaderboardUI();
    }
    #endregion /Leaderboards

}

 

CloudVariables.cs

using UnityEngine;

public class CloudVariables : MonoBehaviour {
    public static int Highscore { get; set; }
}

 

ManagerScript.cs

using UnityEngine;

public class ManagerScript : MonoBehaviour {

    public static ManagerScript Instance { get; private set; }
    public static int Counter { get; private set; }

	// Use this for initialization
	void Start () {
        Instance = this;
        UIScript.Instance.UpdateHighscoreText();
	}

    public void IncrementCounter()
    {
        Counter++;
        UIScript.Instance.UpdatePointsText();
    }

    public void RestartGame()
    {
        PlayGamesScript.AddScoreToLeaderboard(GPGSIds.leaderboard_leaderboard, Counter);

        if (Counter > CloudVariables.Highscore)
        {
            CloudVariables.Highscore = Counter;
            PlayGamesScript.Instance.SaveData();
            UIScript.Instance.UpdateHighscoreText();
        }

        Counter = 0;
        UIScript.Instance.UpdatePointsText();
    }
	
}

 

UIScript.cs

using UnityEngine;
using UnityEngine.UI;

public class UIScript : MonoBehaviour {

    public static UIScript Instance { get; private set; }

	// Use this for initialization
	void Start () {
        Instance = this;
	}

    [SerializeField]
    private Text pointsTxt;
    [SerializeField]
    private Text highscoreTxt;

    public void GetPoint()
    {
        ManagerScript.Instance.IncrementCounter();
    }

    public void Restart()
    {
        ManagerScript.Instance.RestartGame();
    }

    public void Increment()
    {
        PlayGamesScript.IncrementAchievement(GPGSIds.achievement_incremental_achievement, 5);
    }

    public void Unlock()
    {
        PlayGamesScript.UnlockAchievement(GPGSIds.achievement_standard_achievement);
    }

    public void ShowAchievements()
    {
        PlayGamesScript.ShowAchievementsUI();
    }

    public void ShowLeaderboards()
    {
        PlayGamesScript.ShowLeaderboardsUI();
    }

    public void UpdatePointsText()
    {
        pointsTxt.text = ManagerScript.Counter.ToString();
    }

    public void UpdateHighscoreText()
    {
        highscoreTxt.text = CloudVariables.Highscore.ToString();
    }
}

 

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a freelancer and most importantly developer educator, he is set on helping other people succeed in their Flutter app development career.

You may also like

  • Thank you VERY MUCH for the Tutorial!!! It is GREAT!!

    Immediately after playing my game in Unity the Console shows the following error:

    NullReferenceException: Object reference not set to an instance of an object
    ManagerScript.Start () (at Assets/Scripts/ManagerScript.cs:14)

    and line 14 refers to: UIScript.Instance.UpdateHighscoreText(); in the void Start()

    it looks like the function returns “null”…

    Another silly thing is that my Highscore does not get back if I stop and start again neither in Unity nor on device. I also doubt if the highscore ever gets saved on the cloud.

    What would you recommend to troubleshoot?

  • sir, will u please make tutorial on how to save multiple Playerprefs to google Cloud, i am having great trouble in saving my games… I am new to unity i cannot find a suitable tutorial or documentation explaining how to save multiple PlayerPrefs to google play service cloud. like i want to save coins , unlock state of levels stored in PlayerPrefs…

    Thanks in advance….

  • hi

    great tutorial, but i have a problem.
    my app (in internal testing), if i just add EnableSavedGames(), crashes after login.
    i already enable savegames in the google console.

    do you have any ideas? i’ve search all over the internet and i can’t find a answer.

  • When trying to save or load data from Google cloud getting this exception

    AndroidJavaException: java.lang.NoSuchMethodError: no non-static method with name=’getStatusCode’ signature='()I’

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >