10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

グーグルプレイSavedGames(=クラウドセーブ)の実装と落とし穴

Last updated at Posted at 2018-04-17

#はじめに
GooglePlayには無料で利用できるSaved Gamesという名前のクラウドセーブ機能が準備されています。容量の上限は3MBです。(2018.4.16現在)
日本語情報が少なく、Unityでの実装に苦労したので共有したいと思い記事を書きました。

developers.google.comのSaved Gamesのページ
#Unityアプリ側
##動作

  • バックアップボタンを押すだけの操作
  • =>セーブデータがなければバックアップ
  • =>ローカルのプレイよりプレイ時間の短いデータしかクラウドにないならクラウドにセーブ
  • =>ローカルのプレイよりプレイ時間の長いデータがクラウドにあるならクラウドからロード

##実装コード

  • GooglePlayServicesの導入に関してはここでは触れません
  • このスクリプトはゲーム開始時に存在し破壊されないオブジェクトのコンポーネントであることを前提にしています。
  • stringをクラウドとやりとりするコードを以下に記します。セーブデータを1つのstringにする処理はここでは触れません(自分はJsonUtility等でやっています)
CloudManager.cs
using UnityEngine;
using System;
using GooglePlayGames;
using GooglePlayGames.BasicApi;
using GooglePlayGames.BasicApi.SavedGame;
using System.Text;
using UnityEngine.SocialPlatforms;

public class CloudManager : MonoBehaviour {
    public static CloudManager Instance;

    public DateTime openedSavedGame_commitDate;

    bool isSaving;
    static string saveFileName = "SaveFileName_AsYouLike";
    static string saveString = "";

    bool hasInternetConnection { get { return  Application.internetReachability != NetworkReachability.NotReachable; } }

    bool isAuthed { get { return Social.Active.localUser.authenticated; } }


#region lifeCycle

    void Awake() {
        Instance = this;
    }

void Start(){
//ログイン処理例
            PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder()
                .EnableSavedGames()
                .Build();            
            PlayGamesPlatform.InitializeInstance(config);
            PlayGamesPlatform.Activate();
}
#endregion

#region input
//バックアップボタンから呼ぶメソッド
    public void PressCloudSyncButton() {
        OpenSavedGame();
    }

#endregion

    //まずクラウド上のファイルをOPEN
    public void OpenSavedGame() {
        if (!hasInternetConnection) {
            Debug.Log("No Internet Connection");
            return;
        }

        if (!isAuthed) {
            Debug.Log("Not Authed");
            return;
        }
        isSaving = false;
        Debug.Log("Opening savedGame in the cloud");

        ((PlayGamesPlatform)Social.Active).SavedGame.OpenWithAutomaticConflictResolution(
            saveFileName,
            DataSource.ReadCacheOrNetwork,
            ConflictResolutionStrategy.UseLongestPlaytime,
            OnSavedGameOpened
        );
    }

    public void SaveToCloud() {
        if (!hasInternetConnection) {
            Debug.Log("No Internet Connection");
            return;
        }

        if (isAuthed) {
            isSaving = true;
            //first, open the file
            ((PlayGamesPlatform)Social.Active).SavedGame.OpenWithAutomaticConflictResolution(
                saveFileName,
                DataSource.ReadCacheOrNetwork,
                ConflictResolutionStrategy.UseLongestPlaytime,
                OnSavedGameOpened
            );
        } else {
            Debug.Log("Not Authed");
            return;
        }
    }

#region savedGames Callbacks

    ISavedGameMetadata currentOpenedSavedGameMetadata;

    // OPENできたらwrite/readする
    void OnSavedGameOpened(SavedGameRequestStatus status, ISavedGameMetadata gameMetadata) {

        if (status == SavedGameRequestStatus.Success) {

            openedSavedGame_commitDate = gameMetadata.LastModifiedTimestamp;
            currentOpenedSavedGameMetadata = gameMetadata;

            if (isSaving) {
                BuildBackupDate();

                byte[] data = ToBytes(saveString);
                var builder = new SavedGameMetadataUpdate.Builder();
                var updateMetadata = builder
                    .WithUpdatedPlayedTime(GameController.statTotalBattleTime)
                    .WithUpdatedDescription("SAVED AT:" + DateTime.UtcNow.ToLocalTime().ToShortDateString())
                    .Build();


                ((PlayGamesPlatform)Social.Active).SavedGame.CommitUpdate(gameMetadata, updateMetadata, data, OnSavedGameWritten);
            } else {

                SAVEDGAME_STATUS stat = SAVEDGAME_STATUS.Default;

                if (gameMetadata.Description == null || gameMetadata.Description == "") {
                    stat = SAVEDGAME_STATUS.NoData;
                }
                if (gameMetadata.TotalTimePlayed > GameController.statTotalBattleTime) {
                    stat = SAVEDGAME_STATUS.LongerPlaytime;
                } else {
                    if (gameMetadata.TotalTimePlayed == GameController.statTotalBattleTime) {
                        stat = SAVEDGAME_STATUS.Default;      
                    } else {
                        stat = SAVEDGAME_STATUS.ShorterPlaytime;
                    }
                }

                switch (stat) {
                case SAVEDGAME_STATUS.Default:
                    Debug.Log("バックアップデータの更新は不要です");
                    break;
                case SAVEDGAME_STATUS.NoData:
                case SAVEDGAME_STATUS.LongerPlaytime:
                case SAVEDGAME_STATUS.ShorterPlaytime:
                    OpenDialog(stat);
                    break;
                }
            }
        }
    }

    public void LoadSavedGameData() {
        ((PlayGamesPlatform)Social.Active).SavedGame.ReadBinaryData(currentOpenedSavedGameMetadata, OnSavedGameDataRead);
    }

    void OnSavedGameDataRead(SavedGameRequestStatus status, byte[]data) {
        if (status == SavedGameRequestStatus.Success) {
            ProcessCloudData(data);
        } else {
            Debug.LogWarning("Error: Reading game:" + status);
        }
    }

    void OnSavedGameWritten(SavedGameRequestStatus status, ISavedGameMetadata gameMetadata) {
        if (status == SavedGameRequestStatus.Success) {
            GameController.Instance.gameProgress.cloudSaveNum++;
            GameController.Instance.SaveGameProgress();
            Debug.Log("Game " + gameMetadata.Description + " written");
        } else {
            Debug.LogWarning("Error: saving on the cloud " + status);
        }
    }

#endregion

    void ProcessCloudData(byte[]cloudData) {
        DistributeLoadedData(cloudData);
    }

    string FromBytes(byte[] bytes) {
        return Encoding.UTF8.GetString(bytes);
    }

    byte[] ToBytes(string data) {
        return Encoding.UTF8.GetBytes(data);
    }

    enum SAVEDGAME_STATUS {
        Default,
        NoData,
        ShorterPlaytime,
        LongerPlaytime
    }
//ダイアログ表示,YES NOを選択すると対応するメソッドを呼ぶ。具体的な内容にはこの記事では触れない
    void OpenDialog(SAVEDGAME_STATUS stts) {
        Debug.Log("OPEN DIALOG:" + stts);
        switch (stts) {
        default:
            break;
        case SAVEDGAME_STATUS.NoData:
            Debug.Log("バックアップデータがありません、バックアップしますか?");
            break;
        case SAVEDGAME_STATUS.ShorterPlaytime:
            Debug.Log("バックアップしますか?");
            break;
        case SAVEDGAME_STATUS.LongerPlaytime:
            Debug.Log("バックアップしたデータをロードしますか?");
            break;
        }
    }

    void QuitApp() {
        Debug.Log("QuitApp");
        Application.Quit();
    }

    void BuildBackupData() {        
        //バックアップしたいデータをここで代入する
        saveString = "(バックアップしたいデータ)";
    }

//ロードしたデータをゲームに反映する処理
    void DistributeLoadedData(byte[] dataFromCloud) {
        if (dataFromCloud == null) {
            Debug.Log("No data saved on the cloud yet");
            return;
        }
        Debug.Log("Decoding cloud data from bytes.");
        string stringFromCloud = FromBytes(dataFromCloud);
        if (stringFromCloud == "") {
            Debug.Log("No data saved on the cloud yet");
            return;
        }

        //データを反映する
        HogeHoge(SaveString);
//念の為アプリを落とす
        QuitApp();
    }
}

-プレイ時間とクラウドにセーブ成功した回数を記録しておくクラスの例

GameController.cs
public class GameController : MonoBehaviour {
    public static GameController Instance;
    public static double statTotalBattleTime;
    public class GameProgress{
        public int cloudSaveNum;
    }
    public GameProgress gameProgress;
}

#FireBase側設定

  • FireBaseの導入に関してはここでは触れません

##1. GooglePlayConsole=>ゲームサービス=>(アプリ名)=>ゲームの詳細⇒保存済みゲーム(オンを選択)
2f0155ca-bb71-11e5-9d05-d9257613882f.png
##2. APIコンソール プロジェクトでGooglePlayGameServicesとDriveAPIにチェックマークがあることを確認(なければリンクから設定)
キャプチャ-1.jpg
##3. ゲームサービス=>(アプリ名)=>公開=>ゲームに対する変更の公開=>反映を待つ(24時間程度?)

  • 自分はここで落とし穴に落ちました。この項目の存在に気づかず、公開しなくてもベータテスターはSavedGamesの機能が利用できるため、うまく出来ていると思いこんでそのままアプリをリリースしてしまいました…。状況を認識するのに1週間以上かかりました。

  • 公開前状態ではリリースのAPKからこんなLOGがLOGCATに出てきます

Exception in com/google/android/gms/games/snapshot/Snapshots.load: java.lang.IllegalStateException: Cannot use snapshots without enabling the 'Saved Game' feature in the Play console.

  • NullReferenceなんですよねこの後。口に酸っぱいもの上がってきますよねこんなの。
  • 公開から一晩寝かせておくと、上記Errorは綺麗に消えていました。

##「公開」が必須手順なら未公開の変更があるって警告表示出してほしい
##このUIは罠すぎるので腹立ち紛れに記事を投稿することにしました

10
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?