ペット育成ゲームの作り方#6|ペットの状態をJSONでまとめて保存しよう

前回はPlayerPrefsで、おなか・きげん・コイン・時刻を1つずつ保存しました。でも、このあとショップのアイテムや進化の段階など、保存したいものはどんどん増えていきます・・・。1つずつキーを増やしていくのは大変ですよね。今回はペット育成ゲームのシリーズの6回目として、保存したいものを1つのクラスにまとめて、JSONのファイルに保存・読み込みするようにしてみたいと思います!

目次

今回作るもの

  • 保存したいものを、1つのクラス(SaveData)にまとめる
  • JSONにして、1つのファイル(petdata.json)に保存・読み込みする
  • #5でPlayerPrefsに保存したデータを、JSONのファイルに引っ越す
  • セーブデータを消して最初からやり直すメニューを作る(確認用)
スクリプト今回やること
SaveData保存したいものをまとめたクラス(新しく作る)
SaveSystemJSONのファイルに保存・読み込み・削除する(新しく作る)
OfflineProgressPlayerPrefsの代わりに、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を付けています

保存したいものが増えたら、このクラスに変数を足していくだけです。

ファイルに保存・読み込みしよう(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にしている
  • 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ならファインダー)で、そのファイルの場所を開く
Assets/PetRaisingにEditorフォルダが増え、ScriptsにSaveData・SaveSystemが並んでいる

動かして確認しよう

引っ越しを確認する(#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の、最初の状態から始まる
左:セーブデータを消したあとのConsoleのログ。右:そのあと再生した、ゲージが8割・コインが0の最初の状態

おかえりパネルも確認する

  • 一度再生して止める(ファイルに保存される)
  • GameManagerのOffline Progressの右クリックメニューから「5時間前に閉じたことにする」
  • 再生すると、#5と同じように「おかえり! 5時間ぶりだね」が出る

保存のしかたを変えても、今までと同じように遊べていれば成功です!

あとがき

今回はペットの状態をJSONにまとめて、1つのファイルに保存するようにしました。見た目は何も変わりませんが、これで保存するものがいくら増えても大丈夫です。次回はいよいよコインの使い道、ショップを作ります。ScriptableObjectでアイテムのデータを作って、買ったアイテムをペットにあげられるようにしたいと思います!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (1件)

コメントする

目次