二回目の投稿です。よろしくお願いします。
目次
概要
簡単なサンプルを用いて以下のことを解説します:
- 外部へのデータの保存 (セーブ)
- 外部からのデータの読み出し (ロード)
- 外部データの削除 (デリート)
こんな感じのシステムが作れます:
(動画での手順: Add 5回 -> Clear -> Add 4回 -> Save -> Clear -> Load -> ゲーム再起動 -> Load -> Delete -> Clear -> Load
(これにより、ゲームを再起動してもデータが残っていることと、データの削除ができていることがわかる))
また、セーブデータは以下のような形式で保存されています:
注意
解説にスコアカウンターのシステムを用いています。その実装方法は本題から大きく離れるため解説いたしません。ボタンやテキストはこれを見ていい感じに配置し、ボタンを割り当てて下さい (きちんと配置したい人は自動レイアウトで検索)。
スコアカウンターに関するスクリプトは以下においておきます:
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();
}
}
}
実装概要
セーブの概要
要点は「セーブしたいデータを含むクラスをjson形式に変換し、保存先を指定し、上書き(新規)保存すること」なので、最低限以下をすればよい:
- 保存したいデータ(maxScore)を含むクラス(SaveData)を用意する。
- SaveDataクラスの経由地となるstaticクラス(SaveDataStore)を用意する。
(staticにすることで参照しやすくなり、かつ、シーン間で保持される) - SaveDataStoreのSaveDataをjson形式に変換する。
- 保存先のパスを指定し、上書き(もしくは新規)保存する。
ちなみに、今回使用するセーブ用のクラスは以下である:
SaveData.cs (tap)
using UnityEngine;
namespace App.SaveSystem
{
[System.Serializable]
public class SaveData
{
public int MaxScore
{
get => maxScore;
set => maxScore = value;
}
[SerializeField]
private int maxScore;
}
}
注意
データをjson形式にシリアル化するので、保存したいクラス(今回はSaveDataクラス)をSystem.Serializable属性でシリアル化可能にしておく必要がある。
(シリアル化とは、階層をもたないフラットな(直線的な)データ構造に変換すること。(wiki))
ロードの概要
要点は「保存しているjson形式のデータを読み出してクラスに変換し、SaveDataStoreのSaveDataに代入する」なので、最低限以下をすればよい:
- 保存先で指定していたパスを用いて、セーブデータを読み出す。
- このjson形式のデータをクラスに変換する(元に戻す)。
- 読みだしたデータを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)
using UnityEngine;
namespace App.SaveSystem
{
[System.Serializable]
public class SaveData
{
public int MaxScore
{
get => maxScore;
set => maxScore = value;
}
[SerializeField]
private 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回の状態でセーブを押したときに、生成されるデータ
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. ロード時にテキストを更新
イベントに実行する処理を予め登録しておき、ロード時にイベントを実行すればよい:
- SavedDataStore.csにイベントを格納する変数(onLoadEvent)を用意
- ScoreManager.csでロード時に実行する処理を登録
- 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/