Unityで学ぶセーブ機構:JSONでゲームデータを一括保存・読み込み(入門〜実践)

なぜJSON保存?

PlayerPrefsは「少量の設定値」を保存するのに便利ですが、データが増えるとキーの管理が大変になります。

JSON保存にすると——

  • ひとつのファイルに構造化データとしてまとめられる
  • フィールド追加・削除など拡張がしやすい
  • デバッグ時に中身が読める(テキスト)

セーブ先の場所

モバイル/PC問わず安全な保存先は Application.persistentDataPath

例)/Users/…/AppData/LocalLow/<Company>/<Product>/(Windows)

using System.IO;
using UnityEngine;

public static class SavePaths
{
    public static string Dir  => Application.persistentDataPath;
    public static string File => Path.Combine(Dir, "save.json");
}

1. データ構造(POCOクラス)を定義

Unity標準の JsonUtility は publicフィールド を対象にします(プロパティは非対応)。

将来の互換性のために バージョン を持たせておくと便利です。

[System.Serializable]
public class GameData
{
    public int    version = 1;     // マイグレーション用
    public int    level   = 1;     // 1〜99
    public float  score   = 0f;
    public string item    = "なし";
}

2. セーブ/ロードの実装(JsonUtility版)

セーブ(アトミック書き込み)

一時ファイルに書いてから置き換えると、クラッシュ時でも破損しにくくなります。

using System.IO;
using UnityEngine;
using System.Text;

public static class JsonSaveSystem
{
    public static void Save(GameData data)
    {
        if (data.level < 1 || 99 < data.level)
            throw new System.Exception("レベルは1~99で指定してください");

        Directory.CreateDirectory(SavePaths.Dir);

        string json = JsonUtility.ToJson(data, prettyPrint: true);

        // アトミックっぽく: temp -> move
        string tmp = SavePaths.File + ".tmp";
        File.WriteAllText(tmp, json, new UTF8Encoding(encoderShouldEmitUTF8Identifier:false));
        if (File.Exists(SavePaths.File)) File.Delete(SavePaths.File);
        File.Move(tmp, SavePaths.File);

        Debug.Log($"Saved: {SavePaths.File}\n{json}");
    }

    public static GameData LoadOrDefault()
    {
        if (!File.Exists(SavePaths.File))
        {
            Debug.Log("セーブが見つからないため初期データを返します。");
            return new GameData(); // デフォルト
        }

        try
        {
            string json = File.ReadAllText(SavePaths.File, Encoding.UTF8);
            var data = JsonUtility.FromJson<GameData>(json);
            if (data == null) throw new System.Exception("JSONのパースに失敗");

            // ここで必要ならマイグレーション
            MigrateIfNeeded(data);

            return data;
        }
        catch (System.Exception e)
        {
            Debug.LogWarning($"ロード失敗: {e.Message}\n初期データにフォールバックします。");
            return new GameData();
        }
    }

    // バージョン差分に応じてフィールド補完などを行う
    static void MigrateIfNeeded(GameData data)
    {
        switch (data.version)
        {
            case 1:
                // v1→現行 何もしない
                break;
            // 例: case 0: data.item ??= "なし"; data.version = 1; break;
        }
    }

    public static void Delete()
    {
        if (File.Exists(SavePaths.File)) File.Delete(SavePaths.File);
    }
}

3. 実際の呼び出し例(MonoBehaviour)

using UnityEngine;

public class GameDataExample : MonoBehaviour
{
    void Start()
    {
        // セーブ例
        var save = new GameData
        {
            level = 10,
            score = 1234.5f,
            item  = "勇者の剣"
        };
        JsonSaveSystem.Save(save);

        // ロード例
        GameData loaded = JsonSaveSystem.LoadOrDefault();
        Debug.Log($"Loaded → level:{loaded.level}, score:{loaded.score}, item:{loaded.item}");
    }
}

4. PlayerPrefs との比較(使い分けの目安)

目的PlayerPrefsJSONファイル
少量の設定(音量、難易度など)
多数の値/ネスト構造△(キー管理が煩雑)
デバッグのしやすさ◎(テキストで読める)
破損時の復旧×(キー単位)○(バックアップ/アトミック書込可)
セキュリティ×(平文)△(暗号化を自前で実装すれば○)

5. よくある落とし穴・ベストプラクティス

  • ファイルI/Oはメインスレッドで:小さいJSONならOK。大きい場合は Task.Run 等で非同期化(※Unity 6/.NET 4.x互換で可)。
  • 例外ハンドリング:JSON破損や権限エラーに備えて try-catch で初期データにフォールバック。
  • バージョン管理:GameData.version を上げ、MigrateIfNeeded に移行処理を追加。
  • バックアップ:上書き前に save.json.bak を作る方法も有効。
  • 暗号化:チート対策が必要なら、JSONテキストをAES等で暗号化してから保存(ハッシュで改ざん検知も)。
  • クラウド同期:プラットフォームのクラウド(iCloud/Google Play Games/Steam Cloud等)にファイルを同期させる設計も可能。

6. 代替:Newtonsoft.Json / System.Text.Json を使う

JsonUtility は 配列/リスト以外の複雑な型や辞書 が苦手です。

柔軟にやるなら Newtonsoft.Json(パッケージ導入)や System.Text.Json(Unity 6 以降で選択肢)を検討できます。

// Newtonsoft.Json 例
using Newtonsoft.Json;
// Install via UPM or asmdefで参照
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
var back  = JsonConvert.DeserializeObject<GameData>(json);

辞書やprivate setter、プロパティを扱いたい場合に有効です。


7. UIとの連携(簡易例)

ロードしたデータをUIへ反映・保存ボタンで更新するだけで、設定画面/セーブスロットが作れます。

// 例:Startでロード→UI反映、保存ボタンでSave
public class SettingsScreen : MonoBehaviour
{
    GameData _data;

    void Start()
    {
        _data = JsonSaveSystem.LoadOrDefault();
        // TODO: UIへ反映(Text、Sliderなど)
    }

    public void OnClickSave()
    {
        // TODO: UIから値を反映
        JsonSaveSystem.Save(_data);
    }
}

8. 仕上げチェックリスト

  • persistentDataPath を使っている
  • 例外時は初期データにフォールバック
  • JSONのバージョンを持たせた
  • 書き込みは一時ファイル→置換で安全性UP
  • 将来のフィールド追加を想定して実装

まとめ

  • PlayerPrefsは“ちょい保存”、JSONは“ちゃんと保存”。
  • POCOクラスJsonUtilityで手早く始め、複雑化してきたら Newtonsoft/System.Text.Json へ。
  • アトミック書き込み/マイグレーション/例外処理 を押さえると、実運用に耐えるセーブ機構になる。

次の発展案

  • セーブスロット(save1.json, save2.json …)
  • 自動バックアップ復元メニュー
  • 暗号化改ざん検知(HMAC)
  • 非同期I/Oでフリーズ防止
  • ScriptableObjectと併用した設定プロファイル

訪問数 3 回, 今日の訪問数 3回

C#,JSON,Unity

Posted by hidepon