LoginSignup
3
2

[Unity] jsonを用いた外部へのデータの保存と読み込み

Last updated at Posted at 2022-12-07

二回目の投稿です。よろしくお願いします。

目次

  1. 概要
  2. 実装概要
  3. 作業手順
  4. 実装方法
  5. 最後に

概要

簡単なサンプルを用いて以下のことを解説します:

  1. 外部へのデータの保存 (セーブ)
  2. 外部からのデータの読み出し (ロード)
  3. 外部データの削除 (デリート)
こんな感じのシステムが作れます:

ezgif.com-gif-maker.gif
(動画での手順: Add 5回 -> Clear -> Add 4回 -> Save -> Clear -> Load -> ゲーム再起動 -> Load -> Delete -> Clear -> Load
(これにより、ゲームを再起動してもデータが残っていることと、データの削除ができていることがわかる))
また、セーブデータは以下のような形式で保存されています:
image.png

注意
解説にスコアカウンターのシステムを用いています。その実装方法は本題から大きく離れるため解説いたしません。ボタンやテキストはこれを見ていい感じに配置し、ボタンを割り当てて下さい (きちんと配置したい人は自動レイアウトで検索)。
スコアカウンターに関するスクリプトは以下においておきます:

ScoreManager.cs (tap)
using UnityEngine;
using UnityEngine.UI;

namespace App.SaveSystem
{
    /// <summary>
    /// スコアの可算や表示、最大値の更新を担うクラス
    /// </summary>
    public class ScoreManager : MonoBehaviour
    {
        public Text maxScoreText;
        public Text scoreText;

        private int maxScore;
        private int score;

        private void Start()
        {
            UpdateScoreTexts();
        }

        private void UpdateScoreTexts()
        {
            maxScoreText.text = maxScore.ToString();
            scoreText.text = score.ToString();
        }

        public void AddScore() // only button
        {
            score++;
            if (score > maxScore) // scoreがmaxScoreを超えるごとにmaxScoreを更新
            {
                maxScore = score;
            }
            UpdateScoreTexts();
        }

        public void ClearScore() // only button
        {
            maxScore = 0;
            score = 0;
            UpdateScoreTexts();
        }
    }
}

実装概要

  1. セーブの概要
  2. ロードの概要
  3. デリートの概要

セーブの概要

要点は「セーブしたいデータを含むクラスをjson形式に変換し、保存先を指定し、上書き(新規)保存すること」なので、最低限以下をすればよい:

  1. 保存したいデータ(maxScore)を含むクラス(SaveData)を用意する。
  2. SaveDataクラスの経由地となるstaticクラス(SaveDataStore)を用意する。
    (staticにすることで参照しやすくなり、かつ、シーン間で保持される)
  3. SaveDataStoreのSaveDataをjson形式に変換する。
  4. 保存先のパスを指定し、上書き(もしくは新規)保存する。

ちなみに、今回使用するセーブ用のクラスは以下である:

SaveData.cs (tap)
namespace App.SaveSystem
{
    [System.Serializable]
    public class SaveData
    {
        public int MaxScore
        {
            get => maxScore;
            set => maxScore = value;
        }
        public int maxScore; // 外部で参照させない
    }
}

注意
データをjson形式にシリアル化するので、保存したいクラス(今回はSaveDataクラス)をSystem.Serializable属性でシリアル化可能にしておく必要がある。
(シリアル化とは、階層をもたないフラットな(直線的な)データ構造に変換すること。(wiki))

ロードの概要

要点は「保存しているjson形式のデータを読み出してクラスに変換し、SaveDataStoreのSaveDataに代入する」なので、最低限以下をすればよい:

  1. 保存先で指定していたパスを用いて、セーブデータを読み出す。
  2. このjson形式のデータをクラスに変換する(元に戻す)。
  3. 読みだしたデータをSaveDataStoreのSaveDataに代入する。

注意
上記手順でデータを読み出せるが、反映(今回の場合はMaxScoreとして表示)はされないので、イベントを用いてロード時に表示する必要がある。

デリートの概要

System.IO.File.Delete関数を用いて、削除したいデータのパス(保存先で指定していたパス)を指定し、実行するだけ。

作業手順

1. セーブの実装
 1.1. 保存したいデータを含むクラス(SaveData)と、その経由地となるクラス(SaveDataStore)の作成
 1.2. スコアの最大値が更新されたら、SaveDataStore.SaveDataのMaxScoreを更新する。
 1.3. SaveDataStoreのSaveDataをjson形式に変換し、指定したフォルダ内に保存する。
 1.4. セーブ機能を持つボタン用関数の作成
 1.5. セーブの実装確認
2. ロードの実装
 2.1. セーブデータを読み出し、クラスに変換する。
 2.2. ロード機能を持つボタン用関数の作成
 2.3. ロード時にテキストを更新
 2.4. ロードの実装確認
3. デリートの実装
 3.1. セーブデータを削除する機能を追加
 3.2. デリート機能を持つボタン用関数の作成
 3.3. デリートの実装確認

実装方法

1. セーブの実装

1.1. 保存したいデータを含むクラス(SaveData)と、その経由地となるクラス(SaveDataStore)の作成

以下のようにSaveData.csとSaveDataStore.csを作成する:

SaveData.cs (tap)
namespace App.SaveSystem
{
    [System.Serializable]
    public class SaveData
    {
        public int MaxScore
        {
            get => maxScore;
            set => maxScore = value;
        }
        public int maxScore; // 外部に参照させない
    }
}

ポイント:

  • maxScoreを直接参照させないようにプロパティを用いている。
  • 後にjson形式にシリアル化するので、クラスにSerializable属性をつけている。

(2022/12/10追記)
maxScoreをpublic変数にしてセーブデータとして扱えるようにしているが、外部に参照させたくないので、private変数にして[UnityEngine.SerializeField]を用いた方が良い。

SaveDataStore.cs (tap)
using UnityEngine.Events;

namespace App.SaveSystem
{
    /// <summary>
    /// セーブデータを読み込んだもの
    /// </summary>
    public static class SavedDataStore
    {
        public static SaveData SaveData
        {
            get => saveData;
            set => saveData = value;
        }
        private static SaveData saveData = new SaveData();
    }
}

ポイント:

  • saveDataを直接参照させないようにプロパティを用いている。
  • static変数でラップすることで参照しやすくしている。 (staticなのでシーン間で値を保持)
1.2. スコアの最大値が更新されたら、SaveDataStore.SaveDataのMaxScoreを更新する。
ScoreManager.cs (tap)
using UnityEngine;
using UnityEngine.UI;

namespace App.SaveSystem
{
    /// <summary>
    /// スコアの可算や表示、最大値の更新を担うクラス
    /// </summary>
    public class ScoreManager : MonoBehaviour
    {
        public Text maxScoreText;
        public Text scoreText;

        private int maxScore;
        private int score;

        private void Start()
        {
            UpdateScoreTexts();
        }

        private void UpdateScoreTexts()
        {
            maxScoreText.text = maxScore.ToString();
            scoreText.text = score.ToString();
        }

        public void AddScore() // only button
        {
            score++;
            if (score > maxScore) // scoreがmaxScoreを超えるごとにmaxScoreを更新
            {
                maxScore = score;
+                SavedDataStore.SaveData.MaxScore = maxScore;
            }
            UpdateScoreTexts();
        }

        public void ClearScore() // only button
        {
            maxScore = 0;
            score = 0;
            UpdateScoreTexts();
        }
    }
}
1.3. SaveDataStoreのSaveDataをjson形式に変換し、指定したフォルダ内に保存する。

以下のようにSaveAndLoadSystem.csを作成する:

SaveAndLoadSystem.cs (tap)
using System;
using System.IO;
using UnityEngine;

namespace App.SaveSystem
{
    public static class SaveAndLoadSystem<T> where T : class, new()
    {
        private const string FolderName = "SaveData";

        public static void Save(T data)
        {
            var jsonData = JsonUtility.ToJson(data); // json形式に変換

            using (var sw = new StreamWriter(GetPath(data), false)) // 上書き(データがないなら新規作成)
            {
                try
                {
                    sw.Write(jsonData); // セーブ
                    Debug.Log($"Saved {data.GetType().Name} : \n{jsonData}"); // debugonly
                }
                catch (Exception e)
                {
                    Debug.LogError(e);
                }
            }
        }

        private static string GetPath(T data)
        {
            var directoryPath = Application.persistentDataPath + "\\" + FolderName;
            Directory.CreateDirectory(directoryPath); // 指定したパスにフォルダがないなら, フォルダを新規作成
            return directoryPath + $"\\{data.GetType().Name}" + ".json";
        }
    }
}

ポイント:

  • JsonUtility.ToJson()でjson形式に変換
  • StreamWriterにパスを指定して、そこに保存させる。
  • persistentDataPathとは"C: \Users\(ユーザーフォルダ)\AppData\LocalLow\{Company Name}\{Project Name}"で、そこにセーブデータが保存される。
    (Company NameはUnityエディター上のEdit/ProjectSetting/Playerで見れる)
  • ジェネリック型を用いることで、様々なクラスに対応させている。
    (今回はSaveData型しか使わないので、内のTをSaveDataとしても動作する(汎用性に欠けるが。))
1.4. セーブ機能を持つボタン用関数の作成

以下のようにSaveAndLoadButton.csを作成し、Save関数をボタンに付与する:

SaveAndLoadButton.cs (tap)
using UnityEngine;

namespace App.SaveSystem
{
    /// <summary>
    /// データのセーブとロード、デリート機能を持つボタン用関数群
    /// </summary>
    public class SaveAndLoadButton : MonoBehaviour
    {
        public void Save()
        {
            var saveData = SavedDataStore.SaveData;
            SaveAndLoadSystem<SaveData>.Save(saveData);
        }
    }
}
1.5. セーブの実装確認

スコアカウンターを何回か押し、続けてセーブボタンを押す。そして、画面に表示されているMaxScoreの値を保持するjson形式のデータ(SaveData.json)が"C: \Users\(ユーザーフォルダ)\AppData\LocalLow\{Company Name}\{Project Name}"にあればok。
↓ MaxScoreが4回の状態でセーブを押したときに、生成されるデータ
image.png

2. ロードの実装

2.1. セーブデータを読み出し、クラスに変換する。

SaveAndLoadSystem.csにデータをロードする関数を追記する:

SaveAndLoadSystem.cs (tap)
using System;
using System.IO;
using UnityEngine;

namespace App.SaveSystem
{
    public static class SaveAndLoadSystem<T> where T : class, new()
    {
        private const string FolderName = "SaveData";

        public static void Save(T data)
        {
            var jsonData = JsonUtility.ToJson(data);

            using (var sw = new StreamWriter(GetPath(data), false)) // 上書き
            {
                try
                {
                    sw.Write(jsonData);
                    Debug.Log($"Saved {data.GetType().Name} : \n{jsonData}"); // debugonly
                }
                catch (Exception e)
                {
                    Debug.LogError(e);
                }
            }
        }

+        public static T Load()
+        {
+            var data = new T();
+            try
+            {
+                using (var fs = new FileStream(GetPath(data), FileMode.OpenOrCreate))
+                {
+                    using (var sr = new StreamReader(fs))
+                    {
+                        var jsonData = sr.ReadToEnd();
+                        Debug.Log($"Loaded {data.GetType().Name} : \n{jsonData}"); // debugonly
+                        data = JsonUtility.FromJson<T>(jsonData);
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                Debug.LogError(e);
+                return null;
+            }
+            return data;
+        }

        private static string GetPath(T data)
        {
            var directoryPath = Application.persistentDataPath + "\\" + FolderName;
            Directory.CreateDirectory(directoryPath); // 指定したパスにフォルダがないなら, フォルダを新規作成
            return directoryPath + $"\\{data.GetType().Name}" + ".json";
        }
    }
}

ポイント:

  • JsonUtility.FromJson()でjsonをクラスに変換
  • FileStreamにパスを指定して、それを読み出す。
2.2. ロード機能を持つボタン用関数の作成
SaveAndLoadButton.cs (tap)
using UnityEngine;

namespace App.SaveSystem
{
    /// <summary>
    /// データのセーブとロード、デリート機能を持つボタン用関数群
    /// </summary>
    public class SaveAndLoadButton : MonoBehaviour
    {
        public void Save()
        {
            var saveData = SavedDataStore.SaveData;
            SaveAndLoadSystem<SaveData>.Save(saveData);
        }

+        public void Load()
+        {
+            var loadedData = SaveAndLoadSystem<SaveData>.Load() ?? new SaveData();
+            SavedDataStore.SaveData = loadedData;
+        }
    }
}

ポイント:

  • ロード時にデータがなければ初期値を取得
  • SavedDataStore.SaveDataに読みだしたデータを代入
2.3. ロード時にテキストを更新

イベントに実行する処理を予め登録しておき、ロード時にイベントを実行すればよい:

  1. SavedDataStore.csにイベントを格納する変数(onLoadEvent)を用意
  2. ScoreManager.csでロード時に実行する処理を登録
  3. SaveAndLoadButton.csのLoad()内でロード後にonLoadEventを実行
SavedDataStore.cs (tap)
using UnityEngine.Events;

namespace App.SaveSystem
{
    /// <summary>
    /// セーブデータを読み込んだもの
    /// </summary>
    public static class SavedDataStore
    {
+        public static UnityEvent onLoadEvent = new UnityEvent(); // ロード時に呼ばれるイベント

        public static SaveData SaveData
        {
            get => saveData;
            set => saveData = value;
        }
        private static SaveData saveData = new SaveData();
    }
}
ScoreManager.cs (tap)
using UnityEngine;
using UnityEngine.UI;

namespace App.SaveSystem
{
    /// <summary>
    /// スコアの可算や表示、最大値の更新を担うクラス
    /// </summary>
    public class ScoreManager : MonoBehaviour
    {
        public Text maxScoreText;
        public Text scoreText;

        private int maxScore;
        private int score;

        private void Start()
        {
+            SavedDataStore.onLoadEvent.AddListener(() => 
+            {
+                maxScore = SavedDataStore.SaveData.MaxScore;
+                UpdateScoreTexts();
+            }); // ロード時に保存していたデータを表示
            UpdateScoreTexts();
        }

        private void UpdateScoreTexts()
        {
            maxScoreText.text = maxScore.ToString();
            scoreText.text = score.ToString();
        }

        public void AddScore()
        {
            score++;
            if (score > maxScore)
            {
                maxScore = score;
                SavedDataStore.SaveData.MaxScore = maxScore;
            }
            UpdateScoreTexts();
        }

        public void ClearScore()
        {
            maxScore = 0;
            score = 0;
            UpdateScoreTexts();
        }
    }
}
SaveAndLoadButton.cs (tap)
using UnityEngine;

namespace App.SaveSystem
{
    /// <summary>
    /// データのセーブとロード、デリート機能を持つボタン用関数群
    /// </summary>
    public class SaveAndLoadButton : MonoBehaviour
    {
        public void Save()
        {
            var saveData = SavedDataStore.SaveData;
            SaveAndLoadSystem<SaveData>.Save(saveData);
        }

        public void Load()
        {
            var loadedData = SaveAndLoadSystem<SaveData>.Load() ?? new SaveData();
            SavedDataStore.SaveData = loadedData;
+            SavedDataStore.onLoadEvent?.Invoke();
        }
    }
}

ポイント:

  • onLoadEventをstaticにすることで唯一性を保証
  • イベント続行時に"?.Invoke()"を用いてnull確認
2.4. ロードの実装確認

セーブデータが存在する状態でロードボタンを押した時、データに記されているMaxScoreが読みだされ、表示されればok。(参考)

3. デリートの実装

3.1. セーブデータを削除する機能を追加
SaveAndLoadSystem.cs (tap)
using System;
using System.IO;
using UnityEngine;

namespace App.SaveSystem
{
    public static class SaveAndLoadSystem<T> where T : class, new()
    {
        private const string FolderName = "SaveData";

        public static void Save(T data)
        {
            var jsonData = JsonUtility.ToJson(data);

            using (var sw = new StreamWriter(GetPath(data), false)) // 上書き
            {
                try
                {
                    sw.Write(jsonData);
                    Debug.Log($"Saved {data.GetType().Name} : \n{jsonData}"); // debugonly
                }
                catch (Exception e)
                {
                    Debug.LogError(e);
                }
            }
        }

        public static T Load()
        {
            var data = new T();
            try
            {
                using (var fs = new FileStream(GetPath(data), FileMode.OpenOrCreate))
                {
                    using (var sr = new StreamReader(fs))
                    {
                        var jsonData = sr.ReadToEnd();
                        Debug.Log($"Loaded {data.GetType().Name} : \n{jsonData}"); // debugonly
                        data = JsonUtility.FromJson<T>(jsonData);
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogError(e);
                return null;
            }
            return data;
        }

+       public static void Delete()
+       {
+           File.Delete(GetPath(new T())); // 指定したパスのファイルを削除
+           Debug.Log($"Delete SaveData");
+       }

        private static string GetPath(T data)
        {
            var directoryPath = Application.persistentDataPath + "\\" + FolderName;
            Directory.CreateDirectory(directoryPath); // 指定したパスにフォルダがないなら, フォルダを新規作成
            return directoryPath + $"\\{data.GetType().Name}" + ".json";
        }
    }
}
3.2. デリート機能を持つボタン用関数の作成
SaveAndLoadButton.cs (tap)
using UnityEngine;

namespace App.SaveSystem
{
    /// <summary>
    /// データのセーブとロード、デリート機能を持つボタン用関数群
    /// </summary>
    public class SaveAndLoadButton : MonoBehaviour
    {
        public void Save()
        {
            var saveData = SavedDataStore.SaveData;
            SaveAndLoadSystem<SaveData>.Save(saveData);
        }

        public void Load()
        {
            var loadedData = SaveAndLoadSystem<SaveData>.Load() ?? new SaveData();
            SavedDataStore.SaveData = loadedData;
            SavedDataStore.onLoadEvent?.Invoke();
        }

+        public void Delete()
+        {
+            SaveAndLoadSystem<SaveData>.Delete();
+        }
    }
}
3.3. デリートの実装確認

セーブデータがある状態でデリートボタンを押し、セーブデータがフォルダ(C: \Users\(ユーザーフォルダ)\AppData\LocalLow\{Company Name}\{Project Name})からなくなっていればok。(参考)

最後に

どうでしたか、わかりやすかったでしょうか。今回は前回よりは難しいと思います。ただ、セーブはゲームに限らずアプリ制作でほぼ必須となる技術なので、苦労して習得する価値は大いにあります。
今回はボタンを用いて実装しましたが、トリガーを用いれば自動セーブが実装できたりと、いろいろできます。ちなみに、ScriptableObjectを用いる場合はMonoBehaviorを継承していることからシリアル化できないので、一工夫必要です(一旦別のクラスにデータを移して、それを保存するなど)。
また気が向いたら何か書きます。それでは。

宣伝 : CUPLEXという時間差アクションパズルゲームを作成しています。私は主にプログラムとプロジェクト管理を担当しています。よかったら、覗いてみてください↓
https://store.steampowered.com/app/2499100/CUPLEX/

3
2
0

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
3
2