Unity Cloud Save β を用いてプレイヤーデータをクラウドサーバ上で保存・読込・削除する【Unity Gaming Services】

Unity Cloud Save β

クラウドサーバ上でプレイヤーデータを保存・読込・削除ができる Unity 公式 BaaS です.
つまりクラウドサーバ版 PlayerPrefs と捉えても構いません.

unity.com

サンプルプロジェクト

サンプルプロジェクトを作成したので,よければ参考にしてみてください.

github.com

検証環境

  • Unity 2021.2.0f1
  • Unity Authentication 1.0.0-pre.6
  • Unity Cloud Save 1.0.0-pre.3
  • UniRx 7.1.0
  • UniTask 2.2.5
  • Zenject 9.2.0

動作の様子

保存可能なデータフォーマット

PlayerPrefs の場合は,stringintbool のみの制限がありますが,
Unity Cloud Save の場合は,JSONシリアライズが可能なデータであれば型の制限はなさそうです.
が,プリミティブ型と JSON Serializable クラスで少し読込方法が変わります(後述します).

解説

インストール

Add Package by Name に com.unity.services.cloudsave を追加することで完了します.

f:id:xrdnk:20211027212101p:plain

あるいは manifest.json に以下を追加すればできます.

"dependencies": {
    "com.unity.services.cloudsave": "1.0.0-pre.3",
 }

Cloud Save をインストールすると,Authentication 1.0.0-pre.6 も一緒に入ります.

Dashboard 設定

Cloud Save を利用できるようにするために,Dashboard 上で Cloud Save を Enabled にする必要があります.

dashboard.unity3d.com

f:id:xrdnk:20211027212306p:plain

Unity Authentication でまずは認証

Unity Gaming Services は基本的には Unity Authentication で認証を通してからでないと使えません.
なので,一回 Unity Authentication でサインイン処理を行っています.簡易のため今回前回記事の匿名認証を利用します.

xrdnk.hateblo.jp

サインイン成功後に表示されるプレイヤーIDは後程利用します.

保存処理

わかりにくくて申し訳ありませんが,最初のフォームでは名前を,二つ目のフォームでは年齢を入力することを想定しています.
そして,右にあるプルダウンはどういうデータ形式で保存するかを選択しています.
Name であれば名前のみ保存,Age であれば年齢のみ保存,User であれば全データを保存という意味で捉えて頂ければ.

以降,Unity Cloud Save の機能を利用したコアのスクリプトは CloudSaveUserDataRepository.cs になります.

保存処理は以下で行っています.

        public async UniTask Save(string key, object data)
        {
            // PlayerPrefsKey と保存データで辞書化
            var savingData = new Dictionary<string, object>{ { key, data } };
            try
            {
                // Cloud Save 上で保存処理
                // NOTE: この時,渡すデータがシリアライズデータの場合,内部で一緒にシリアライズ処理も行ってくれる
                await SaveData.ForceSaveAsync(savingData);
                Debug.Log($"Saved: [{key},{data}]");
            }
            // バリデーションエラー
            catch (CloudSaveValidationException csve)
            {
                Debug.LogError($"{csve.ErrorCode} : {csve.Reason}");
            }
            // Cloud Save の汎用エラー
            catch (CloudSaveException cse)
            {
                Debug.LogError($"{cse.ErrorCode} : {cse.Reason}");
            }
        }

第一引数は PlayerPrefs でいう key,第二引数は保存するデータ内容になります.
保存するデータがプリミティブ型の場合は特にそのまま問題ないんですが,
シリアライズデータの場合,内部で勝手に一緒にJSONシリアライズ処理を行っています.
なのでこちら側でシリアライズ処理を行う必要はありません.

保存データ確認

Save Data ボタンを押下後にデータ保存がされますが,実際に保存されているかを確認します.
Unity Dashboard で Cloud Save → Find Player でプレイヤの検索画面になります.
ここで,サインイン時に出力されたプレイヤ ID を入力しますと,保存したデータが表示されます.

f:id:xrdnk:20211027213313p:plain

読込処理

プリミティブ型とシリアライズデータの場合で読込処理が異なります.

プリミティブ型

        /// <summary>
        /// プリミティブ型の読込処理
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public async UniTask<object> Read(string key)
        {
            Dictionary<string, string> savedData;
            try
            {
                // PlayerPrefsKey を持つ辞書データの読み込み
                savedData = await SaveData.LoadAsync(new HashSet<string> { key });
            }
            // バリデーションエラー
            catch (CloudSaveValidationException csve)
            {
                Debug.LogError($"{csve.ErrorCode} : {csve.Reason}");
                return null;
            }
            // Cloud Save の汎用エラー
            catch (CloudSaveException cse)
            {
                Debug.LogError($"{cse.ErrorCode} : {cse.Reason}");
                return null;
            }

            // 読み込んだ辞書データから Key に対応する生データを読み込む
            var rawData = savedData[key];
            Debug.Log($"RawData Read: [{key}, {rawData}]");
            return rawData;
        }
savedData = await SaveData.LoadAsync(new HashSet<string> { key });

LoadAsync の引数に key の HashSet を渡すと,その key に対応する key と 保存データの辞書セットが読み込まれます.
保存データを読み込むために savedData[key] をしている感じです.
ここでは単一データ読込の方法でしたが,勿論複数データの読み込みも可能ではあります.

シリアライズデータ

        /// <summary>
        /// シリアライズデータの読込処理
        /// </summary>
        /// <param name="key"></param>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public async UniTask<object> Read<T>(string key) where T : class
        {
            Dictionary<string, string> savedData;
            try
            {
                // PlayerPrefsKey を持つ辞書データの読み込み
                savedData = await SaveData.LoadAsync(new HashSet<string> { key });
            }
            // バリデーションエラー
            catch (CloudSaveValidationException csve)
            {
                Debug.LogError($"{csve.ErrorCode} : {csve.Reason}");
                return null;
            }
            // Cloud Save の汎用エラー
            catch (CloudSaveException cse)
            {
                Debug.LogError($"{cse.ErrorCode} : {cse.Reason}");
                return null;
            }

            // 読み込んだ辞書データから Key に対応する JSON データを読み込む
            var jsonData = savedData[key];
            Debug.Log($"JsonData Read: [{key}, {jsonData}]");
            // デシリアライズ処理を行い,データを取得する
            // NOTE: Read の場合は自分でデシリアライズする必要がある
            var data = JsonUtility.FromJson<T>(jsonData);
            Debug.Log($"RawData Read: [{key}, {data}]");
            return data;
        }

保存処理の場合は内部で勝手にシリアライズ処理を行ってくれましたが,
読込処理の場合は自分でデシリアライズ処理を行う必要があることに注意ください.
JsonUtility.FromJson(jsonData); の実行のため,型も渡す必要があるので,classジェネリック型制約を行ってます.
(シリアライズデータがclassの場合を想定しているため,structにする場合は各自よしなに対応ください.)

削除処理

        public async UniTask Delete(string key)
        {
            try
            {
                // Cloud Save 上で削除処理
                await SaveData.ForceDeleteAsync(key);
                Debug.Log($"Deleted: [{key}]");
            }
            // バリデーションエラー
            catch (CloudSaveValidationException csve)
            {
                Debug.LogError($"{csve.ErrorCode} : {csve.Reason}");
            }
            // Cloud Save の汎用エラー
            catch (CloudSaveException cse)
            {
                Debug.LogError($"{cse.ErrorCode} : {cse.Reason}");
            }
        }

削除したい key を await SaveData.ForceDeleteAsync(key); に渡すだけでできます.
ダッシュボードを見ると消えているのが確認できます.

例外処理

CloudSaveException

Cloud Save の汎用エラー.
プロジェクトID、プレーヤーID、またはアクセストークンのいずれかが不足などで発生したりします.

エラーコードとエラーメッセージ,エラー原因も出してくれます.CloudSaveExceptionReason は以下の通りになります.

f:id:xrdnk:20211027215136p:plain

CloudSaveValidationException

CloudSave のバリデーションエラー.型チェックミスの場合など.CloudSaveValidationExceptionDetail で原因もわかります.

f:id:xrdnk:20211027215026p:plain

参考資料

こちらもドキュメントが丁寧なので,参考にするとよいです.
暗号化のケアを考慮した上でクラウド保存用に Unity Cloud Save を利用するのは全然ありだと感じました.
一応開発・ステージング・本番環境の切り替えは可能です.

コメントするならば,Dashboard の所の保存データ検索をもう少し使いやすくしてほしさはあります.

docs.unity.com