前回はPlayerPrefsで、おなか・きげん・コイン・時刻を1つずつ保存しました。でも、このあとショップのアイテムや進化の段階など、保存したいものはどんどん増えていきます・・・。1つずつキーを増やしていくのは大変ですよね。今回はペット育成ゲームのシリーズの6回目として、保存したいものを1つのクラスにまとめて、JSONのファイルに保存・読み込みするようにしてみたいと思います!
- シリーズの目次:ペット育成ゲームの作り方
- 前回:#5 閉じている間も時間を進めよう
今回作るもの
- 保存したいものを、1つのクラス(SaveData)にまとめる
- JSONにして、1つのファイル(petdata.json)に保存・読み込みする
- #5でPlayerPrefsに保存したデータを、JSONのファイルに引っ越す
- セーブデータを消して最初からやり直すメニューを作る(確認用)
| スクリプト | 今回やること |
|---|---|
| SaveData | 保存したいものをまとめたクラス(新しく作る) |
| SaveSystem | JSONのファイルに保存・読み込み・削除する(新しく作る) |
| OfflineProgress | PlayerPrefsの代わりに、SaveSystemで保存するように変える |
| SaveDataMenu | 「セーブデータを消す」メニュー(エディタ拡張、新しく作る) |
前回の続きから
- Unityのバージョンは6.3 LTS
- 記事作成時点では6000.3.22f1で確認しています
- テンプレートはUniversal 2D
- #5の状態:閉じるときにPlayerPrefsに保存して、次に開いたときに閉じていた時間の分だけ進む
JSONとファイルへの保存の基本は、12-6 データを保存しよう:PlayerPrefsとJSONで紹介しています。
なぜJSONにまとめるの?
12-6の記事でも紹介しましたが、PlayerPrefsは「少ない値を手軽に保存する」もの。セーブデータには、JSONのファイルの方が向いています。
| PlayerPrefs(#5) | JSONのファイル(#6) | |
|---|---|---|
| 保存のしかた | 値ごとにキーを付けて1つずつ | クラスをまるごと1つのファイルに |
| 保存するものが増えたら | キーと保存・読み込みの行を増やす | クラスに変数を1行足すだけ |
| アイテムの一覧(リスト) | 保存しにくい | そのまま保存出来る(#7で使います) |
| 中身を見る | Windowsではレジストリの中で見にくい | メモ帳で開ける |
#7ではショップで買ったアイテムの一覧を保存します。その前に、JSONにまとめておきましょう。
保存したいものをまとめよう(SaveData)
まずは、保存したいものを1つのクラスにまとめます。
- PetRaising/Scriptsに「SaveData」を作る
using System;
// セーブデータ。ファイルに保存したいものを、このクラスに全部まとめる
[Serializable]
public class SaveData
{
public PetStatus petStatus = new PetStatus(); // おなか・きげん・コイン
public long lastTimeBinary; // 最後に保存した時刻(UTC の DateTime を ToBinary した数字)
}
- [Serializable]
- JsonUtilityでJSONにするクラスには必要。#3で作ったPetStatusにも付いているので、中身ごとJSONに出来る
- petStatus
- #3で作ったPetStatusをそのまま入れる。おなか・きげん・コインが入っている
- lastTimeBinary
- #5と同じく、時刻は
ToBinary()で数字(long)にして保存する - JsonUtilityはDateTimeをそのまま保存出来ないので、数字にしておく
- DateTimeと間違えないように、名前にBinaryを付けています
- #5と同じく、時刻は
保存したいものが増えたら、このクラスに変数を足していくだけです。
ファイルに保存・読み込みしよう(SaveSystem)
次に、SaveDataをJSONのファイルに保存・読み込みするスクリプトです。
- PetRaising/Scriptsに「SaveSystem」を作る
using System;
using System.IO;
using UnityEngine;
// SaveData を JSON のファイルに保存・読み込み・削除する
public static class SaveSystem
{
private const string FileName = "petdata.json";
// セーブデータのファイルの場所(Windows・Mac・スマホで、それぞれ保存してよい場所になる)
public static string FilePath => Path.Combine(Application.persistentDataPath, FileName);
public static void Save(SaveData data)
{
string json = JsonUtility.ToJson(data, true);
File.WriteAllText(FilePath, json);
}
// セーブデータを読み込む。なければ null を返す(初めて遊ぶとき)
public static SaveData Load()
{
if (!File.Exists(FilePath))
{
return LoadFromPlayerPrefs(); // #5 で PlayerPrefs に保存したデータがあれば、それを使う
}
try
{
string json = File.ReadAllText(FilePath);
return JsonUtility.FromJson<SaveData>(json);
}
catch (Exception e)
{
// ファイルが壊れていたら、最初からにする(ゲームが止まらないように)
Debug.LogWarning($"セーブデータを読み込めませんでした:{e.Message}");
return null;
}
}
public static void Delete()
{
if (File.Exists(FilePath))
{
File.Delete(FilePath);
}
}
// #5 まで PlayerPrefs に保存していたデータを読み込み、PlayerPrefs からは消す(引っ越し)
private static SaveData LoadFromPlayerPrefs()
{
if (!PlayerPrefs.HasKey("LastTime"))
{
return null;
}
SaveData data = new SaveData();
data.petStatus.hunger = PlayerPrefs.GetFloat("Hunger", data.petStatus.hunger);
data.petStatus.mood = PlayerPrefs.GetFloat("Mood", data.petStatus.mood);
data.petStatus.coins = PlayerPrefs.GetInt("Coins", data.petStatus.coins);
data.lastTimeBinary = long.Parse(PlayerPrefs.GetString("LastTime"));
PlayerPrefs.DeleteKey("Hunger");
PlayerPrefs.DeleteKey("Mood");
PlayerPrefs.DeleteKey("Coins");
PlayerPrefs.DeleteKey("LastTime");
PlayerPrefs.Save();
return data;
}
}
- static class
- GameObjectに付けずに、どこからでも
SaveSystem.Save(…)のように呼べるクラス - 保存と読み込みは、ゲームの中でどこからでも使いたいので、staticにしている
- GameObjectに付けずに、どこからでも
- JsonUtility.ToJson(data, true)
- SaveDataをJSONの文字にする。trueにすると、改行付きの読みやすい形になる
- Application.persistentDataPath
- アプリがデータを保存してよい場所。WindowsでもMacでもスマホでも、それぞれに合った場所になる
- Load()がnullを返す
- ファイルがないとき(初めて遊ぶとき)はnull。受け取った側で「nullなら最初から」と判断する
- try / catch
- ファイルが壊れていると、FromJsonでエラーになる。catchで受け止めて、最初から遊べるようにしている
- LoadFromPlayerPrefs(引っ越し)
- ファイルがなくても、#5でPlayerPrefsに保存したデータがあれば、それを読み込む
- 読み込んだらPlayerPrefsからは消す。次からはJSONのファイルだけを使う
- #5の記事を読んで遊んでいた人のデータが消えないようにするためのもの
スライム実際のゲームでも、アップデートで保存のしかたを変えるときは、こういう「引っ越し」の処理を入れます。入れ忘れると、アップデートした人のデータが全部消えちゃう・・・
PlayerPrefsから切り替えよう(OfflineProgress)
#5で作ったOfflineProgressの、保存と読み込みの部分をSaveSystemに置き換えます。閉じていた時間の計算とおかえりパネルは、そのままです。
using System;
using TMPro;
using UnityEngine;
// 閉じるときにペットの状態と時刻を保存し、次に開いたときに閉じていた時間の分だけ進める
public class OfflineProgress : MonoBehaviour
{
[SerializeField] private PetController pet;
[SerializeField] private float maxOfflineHours = 24f; // 閉じていても、進めるのはこの時間まで
[Header("おかえりパネル")]
[SerializeField] private GameObject welcomePanel;
[SerializeField] private TMP_Text welcomeText;
[SerializeField] private float minMinutesToWelcome = 1f; // これより短いときはパネルを出さない
private void Start()
{
welcomePanel.SetActive(false);
// ---- ここから変更 ----
SaveData data = SaveSystem.Load();
if (data == null)
{
return; // 初めて遊ぶとき
}
// 閉じたときの状態に戻す
PetStatus status = pet.Status;
status.hunger = data.petStatus.hunger;
status.mood = data.petStatus.mood;
status.coins = data.petStatus.coins;
// 閉じていた時間を計算する
DateTime lastTime = DateTime.FromBinary(data.lastTimeBinary);
// ---- ここまで変更 ----
TimeSpan away = DateTime.UtcNow - lastTime;
if (away < TimeSpan.Zero)
{
away = TimeSpan.Zero; // 端末の時計が戻されていたら、時間はたっていないことにする
}
if (away > TimeSpan.FromHours(maxOfflineHours))
{
away = TimeSpan.FromHours(maxOfflineHours); // 進めるのは上限まで
}
// 閉じていた時間の分だけ、まとめて進める
pet.PassTime((float)away.TotalSeconds);
if (away.TotalMinutes >= minMinutesToWelcome)
{
ShowWelcome(away);
}
}
// スマホでホームに戻ったとき
private void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
Save();
}
}
// ゲームを終了したとき(エディタでは再生を止めたとき)
private void OnApplicationQuit()
{
Save();
}
// ---- ここから変更 ----
private void Save()
{
SaveData data = new SaveData();
data.petStatus = pet.Status;
data.lastTimeBinary = DateTime.UtcNow.ToBinary();
SaveSystem.Save(data);
}
// ---- ここまで変更 ----
private void ShowWelcome(TimeSpan away)
{
// 閉じていた時間を「5時間」「30分」のような文字にする
string awayText = away.TotalHours >= 1
? $"{(int)away.TotalHours}時間"
: $"{(int)away.TotalMinutes}分";
welcomeText.text = $"おかえり!\n{awayText}ぶりだね";
welcomePanel.SetActive(true);
}
// おかえりパネルの OK ボタンの On Click に登録する
public void CloseWelcome()
{
welcomePanel.SetActive(false);
}
// ---- ここから変更 ----
// 確認用:コンポーネントの右クリックメニューから「5時間前に閉じたことにする」
// 再生していないときに使う。保存した時刻だけを書き換える(ペットの状態は、前回閉じたときのまま)
[ContextMenu("5時間前に閉じたことにする")]
private void PretendClosedFiveHoursAgo()
{
SaveData data = SaveSystem.Load();
if (data == null)
{
Debug.Log("セーブデータがありません。一度再生して止めてから使ってください");
return;
}
data.lastTimeBinary = DateTime.UtcNow.AddHours(-5).ToBinary();
SaveSystem.Save(data);
Debug.Log("5時間前に閉じたことにしました。再生するとおかえりパネルが出ます");
}
// ---- ここまで変更 ----
}
- PlayerPrefsのキー(定数)がなくなった
- 保存するものはSaveDataにまとまっているので、キーを1つずつ決める必要がない
- Start
SaveSystem.Load()がnullなら初めて遊ぶとき。それ以外は、読み込んだデータで状態を戻す
- Save
- 新しいSaveDataを作って、今の状態と時刻を入れて保存するだけ
- 保存するものが増えても、ここに1行足せばOK
セーブデータを消すメニューを作ろう(SaveDataMenu)
動作を確認していると、「最初からやり直したい」ことがよくあります。Unityのメニューに「セーブデータを消す」を追加しましょう。
- PetRaisingの中に「Editor」フォルダを作る
- Editorフォルダに「SaveDataMenu」を作る
using UnityEditor;
using UnityEngine;
// Unity のメニューに「PetRaising」を追加する(エディタでだけ使う確認用)
public static class SaveDataMenu
{
[MenuItem("PetRaising/セーブデータを消す")]
private static void DeleteSaveData()
{
if (EditorApplication.isPlaying)
{
Debug.LogWarning("再生を止めてから消してください(止めたときに保存し直されるため)");
return;
}
SaveSystem.Delete();
Debug.Log($"セーブデータを消しました:{SaveSystem.FilePath}");
}
[MenuItem("PetRaising/セーブデータの場所を開く")]
private static void OpenSaveFolder()
{
EditorUtility.RevealInFinder(SaveSystem.FilePath);
}
}
- Editorフォルダ
- Editorという名前のフォルダに入れたスクリプトは、エディタでだけ使われて、ゲームのビルドには入らない
using UnityEditor;を使うスクリプトは、必ずEditorフォルダに入れる(入れないとビルドでエラーになる)
- [MenuItem(“PetRaising/セーブデータを消す”)]
- Unityの上のメニューに「PetRaising」が増えて、その中に項目が出る
- 再生中は消さない
- 再生中に消しても、再生を止めたとき(OnApplicationQuit)に保存し直されてしまうため
- EditorUtility.RevealInFinder
- エクスプローラー(Macならファインダー)で、そのファイルの場所を開く


動かして確認しよう
引っ越しを確認する(#5から続けている人)
- 再生(▶)する
- #5で保存したおなか・きげん・コインが、そのまま引き継がれていればOK
- このときはまだファイルがないので、PlayerPrefsから読み込んでいる
- 再生を止める
- ここで初めて、JSONのファイルに保存される
保存されたファイルを見てみる
- PetRaising>セーブデータの場所を開く
- エクスプローラーでpetdata.jsonが選ばれた状態で開くので、メモ帳で開いてみる
中身は、下のようなJSONになっています。
{
"petStatus": {
"hunger": 58.0638313293457,
"mood": 73.55758666992188,
"coins": 30
},
"lastTimeBinary": 5250942850135970750
}
- SaveDataの変数の名前と値が、そのまま並んでいる
- hungerやmoodの小数が長いのは、floatの値をそのまま書き出しているため。気にしなくてOK
- lastTimeBinaryは、DateTimeをToBinaryした大きな数字。見ても時刻は分かりませんが、FromBinaryで正しく元の時刻に戻せます



メモ帳で数字を書き換えて保存すると、次に開いたときにその値になるよ。コインを増やしてみたくなるけど、それはほどほどにね!
最初からやり直す
- 再生を止めた状態で、PetRaising>セーブデータを消す
- Consoleに「セーブデータを消しました」と出る
- 再生すると、おなか80・きげん80・コイン0の、最初の状態から始まる


おかえりパネルも確認する
- 一度再生して止める(ファイルに保存される)
- GameManagerのOffline Progressの右クリックメニューから「5時間前に閉じたことにする」
- 再生すると、#5と同じように「おかえり! 5時間ぶりだね」が出る
保存のしかたを変えても、今までと同じように遊べていれば成功です!
あとがき
今回はペットの状態をJSONにまとめて、1つのファイルに保存するようにしました。見た目は何も変わりませんが、これで保存するものがいくら増えても大丈夫です。次回はいよいよコインの使い道、ショップを作ります。ScriptableObjectでアイテムのデータを作って、買ったアイテムをペットにあげられるようにしたいと思います!
コメント
コメント一覧 (1件)
[…] 前回:#6 ペットの状態をJSONでまとめて保存しよう […]