mcpField
U
unity-save-system

unity-save-system

github.com/nice-wolf-studio開発4072 回インストール6 週間前に更新
リポジトリ

unity-save-system について

unity-save-systemは、Unityゲームのセーブ・ロード設計に関するリファレンススキルです。シリアライズ、保存先、移行、クラウド同期、自動保存の方法を比較し、データ損失やプラットフォーム固有の保存問題への対処を扱います。

主な機能

  1. 01シリアライズ形式の選択
  2. 02セーブデータとDTOの設計
  3. 03保存先とPlayerPrefsの指針
  4. 04バージョン管理と移行パターン
  5. 05ローカル優先のクラウド同期
  6. 06自動保存とモバイルでの永続化

活用シーン

  1. 01ゲームのセーブデータ形式を選択
  2. 02ゲームシステムごとにセーブファイルを整理
  3. 03データ構造の変更後に旧セーブを移行
  4. 04ローカルセーブをクラウドと同期

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/nice-wolf-studio/unity-claude-skills --skill unity-save-system

インストール後、Claude Code で/unity-save-systemを入力して読み込まれたか確認します。

同梱ファイルreferences/save-load-implementation.md

詳細

セーブ/ロードシステム — 判断パターン

前提となるスキル: unity-lifecycle(OnApplicationPause、OnApplicationQuit、終了シーケンス)、unity-data-driven(JSON シリアル化、バージョン管理)、unity-packages-services(Cloud Save API)、unity-async-patterns(BackgroundThreadAsync)

これらのパターンは、セーブシステムで最もよくある問題に対処します。Claude が何にでも PlayerPrefs を使い、バージョン管理のない壊れやすいシリアル化を行い、モバイル固有の永続化要件を無視する問題です。


パターン:シリアル化形式の選択

タイミング:セーブデータをディスクにシリアル化する方法を選ぶとき

判断:

  • JSON(JsonUtility) — 人が読みやすく、デバッグしやすく、セーブファイルが小さくなります。辞書とポリモーフィズムには対応していません。ほとんどのゲームで最適なデフォルトです。
  • JSON(Newtonsoft) — JSON の全機能(辞書、ポリモーフィズム、LINQ)を利用できます。依存関係が少し大きくなります。セーブデータが複雑な場合に最適です。
  • バイナリ(カスタムまたはMessagePack) — パフォーマンスが重要な場合、大きなセーブデータ、チート対策(編集が困難)に適しています。人が読むことはできません。対戦ゲームや非常に広大なワールドに最適です。
  • BinaryFormatter — 絶対に使用しないでください。セキュリティ上の脆弱性があり、デシリアル化時に任意のコードが実行される可能性があります。Unity と Microsoft は非推奨にしています。

スキャフォールド(JSON のセーブ/ロード):

using System.IO;
using UnityEngine;

public static class SaveFileIO
{
    public static void SaveJson<T>(T data, string fileName)
    {
        string path = GetSavePath(fileName);
        string json = JsonUtility.ToJson(data, prettyPrint: true);

        // Write to temp file first, then rename (atomic write)
        string tempPath = path + ".tmp";
        File.WriteAllText(tempPath, json);
        if (File.Exists(path))
            File.Replace(tempPath, path, path + ".bak");
        else
            File.Move(tempPath, path);
    }

    public static T LoadJson<T>(string fileName) where T : new()
    {
        string path = GetSavePath(fileName);
        if (!File.Exists(path)) return new T();

        string json = File.ReadAllText(path);
        return JsonUtility.FromJson<T>(json);
    }

    public static bool SaveExists(string fileName) =>
        File.Exists(GetSavePath(fileName));

    public static void DeleteSave(string fileName)
    {
        string path = GetSavePath(fileName);
        if (File.Exists(path)) File.Delete(path);
        if (File.Exists(path + ".bak")) File.Delete(path + ".bak");
    }

    static string GetSavePath(string fileName) =>
        Path.Combine(Application.persistentDataPath, fileName);
}

注意点:BinaryFormatter はセキュリティ上の欠陥です。デシリアル化によって任意のコードが実行される可能性があります。Unity と Microsoft は非推奨にしています。既存のコードで見つけた場合は、すぐに置き換えてください。JsonUtility は辞書をシリアル化できません。List<SerializableKeyValue<K,V>> に変換するか、Newtonsoft を使用してください。アトミック書き込みパターン(一時ファイル .tmp に書き込んでから名前を変更)を使うと、書き込み中にアプリがクラッシュした場合のデータ破損を防げます。


パターン:セーブデータのアーキテクチャ(DTO パターン)

タイミング:何をシリアル化するか、どのように構造化するかを決めるとき

判断:

  • 単一のモノリシックなセーブデータ — すべてを含む SaveData クラスを1つ用意します。ロード/セーブはアトミックです。ロード/セーブが高速な小規模ゲームに最適です。
  • システムごとのセーブファイル — PlayerSave.json、InventorySave.json、WorldSave.json。一部のみロードでき、書き込みサイズが小さく、個々のシステムを移行しやすくなります。

スキャフォールド(ISaveable インターフェース):

// Save Data Transfer Object -- plain C# class, NOT MonoBehaviour
[System.Serializable]
public class GameSaveData
{
    public int version = 1;
    public PlayerSaveData player;
    public InventorySaveData inventory;
    public WorldSaveData world;
}

[System.Serializable]
public class PlayerSaveData
{
    public float[] position = new float[3]; // Vector3 as array for JSON compat
    public int health;
    public int level;
    public int xp;

    public void FromPlayer(Transform t, int hp, int lvl, int xp)
    {
        position[0] = t.position.x;
        position[1] = t.position.y;
        position[2] = t.position.z;
        health = hp; level = lvl; this.xp = xp;
    }

    public Vector3 GetPosition() => new(position[0], position[1], position[2]);
}

// Systems implement ISaveable to participate in save/load
public interface ISaveable
{
    void GatherSaveData(GameSaveData data);  // Write state to DTO
    void ApplySaveData(GameSaveData data);   // Read state from DTO
}

注意点:セーブ用 DTO は [System.Serializable] を付けた通常の C# クラスでなければなりません。MonoBehaviour や ScriptableObject は使用しないでください。含められるのはシリアル化可能な型(プリミティブ型、配列、リスト、ネストされた [Serializable] クラス)のみです。Vector3 などの Unity 型は JsonUtility では問題なくシリアル化できますが、カスタムコンバーターなしでは Newtonsoft でシリアル化できません。移植性を高めるには float[] を使用してください。


パターン:セーブファイルの保存場所

タイミング:セーブファイルの書き込み先を選ぶとき

判断:

  • Application.persistentDataPath(正しいデフォルト)— 書き込み可能で、アプリのアップデート後も保持され、プラットフォームに適しています。すべてのセーブデータに使用してください。
  • Application.streamingAssetsPath — ビルドでは読み取り専用です。ここには書き込まないでください。バンドルされた読み取り専用データにのみ使用します。
  • PlayerPrefs — キーと値のみを保存できます。設定や環境設定に使用します。ゲームの状態には使用しないでください。

スキャフォールド:

// Always use Path.Combine -- never concatenate with "/"
string savePath = Path.Combine(Application.persistentDataPath, "saves", "slot1.json");

// Ensure directory exists before writing
string dir = Path.GetDirectoryName(savePath);
if (!Directory.Exists(dir))
    Directory.CreateDirectory(dir);

注意点:プラットフォームによって persistentDataPath の場所は異なります。Windows では AppData/LocalLow/<company>/<product>、macOS では ~/Library/Application Support/<bundleID>、Android/iOS では内部ストレージです。WebGL では IndexedDB を使用します(非同期で、プライベートブラウジングでは失敗する場合があります)。必ず Path.Combine を使用してください。macOS/Linux ではスラッシュが使えますが、Path.Combine ならクロスプラットフォームで安全です。


パターン:PlayerPrefs のスコープ設定

使用場面:設定や少量の永続データに PlayerPrefs を使用する場合

判断:

  • PlayerPrefs を直接呼び出す ― 設定が10個未満の小規模なプロジェクト向けです。シンプルです。
  • ラッパークラス ― 設定が10個以上ある場合や、型安全性、変更イベント、デフォルト値が必要な場合に適しています。キーの衝突を防げます。

スキャフォールド(設定ラッパー):

public static class GameSettings
{
    // Prefixed keys prevent collision between systems
    private const string Prefix = "settings.";

    public static float MasterVolume
    {
        get => PlayerPrefs.GetFloat(Prefix + "masterVolume", 1f);
        set { PlayerPrefs.SetFloat(Prefix + "masterVolume", value); OnSettingsChanged?.Invoke(); }
    }

    public static bool Fullscreen
    {
        get => PlayerPrefs.GetInt(Prefix + "fullscreen", 1) == 1;
        set { PlayerPrefs.SetInt(Prefix + "fullscreen", value ? 1 : 0); OnSettingsChanged?.Invoke(); }
    }

    public static int QualityLevel
    {
        get => PlayerPrefs.GetInt(Prefix + "quality", QualitySettings.GetQualityLevel());
        set { PlayerPrefs.SetInt(Prefix + "quality", value); OnSettingsChanged?.Invoke(); }
    }

    public static event Action OnSettingsChanged;

    public static void Save() => PlayerPrefs.Save();

    public static void ResetToDefaults()
    {
        PlayerPrefs.DeleteKey(Prefix + "masterVolume");
        PlayerPrefs.DeleteKey(Prefix + "fullscreen");
        PlayerPrefs.DeleteKey(Prefix + "quality");
        OnSettingsChanged?.Invoke();
    }
}

注意点:PlayerPrefs のキーはグローバルな文字列であり、名前空間はありません。衝突を防ぐため、プレフィックス付きのキー("audio.masterVolume"、"video.fullscreen")を使用してください。PlayerPrefs に bool 型はありません。int(0/1)を使用してください。Windows では、PlayerPrefs は Windows レジストリに保存されるため、大きな値は避けてください。PlayerPrefs.Save() は OnApplicationQuit で自動的に実行されますが、モバイルでは重要な変更の後に手動で呼び出してください(アプリが終了処理なしに終了させられる場合があります)。


パターン:セーブデータのバージョン管理と移行

使用場面:ゲームのアップデートによってセーブ形式が変わる場合(フィールドの追加、名前変更、構造変更)

判断:

  • バージョンフィールド+順次マイグレーター ― 互換性のない変更に適しています。セーブデータに int version を保存します。読み込み時に、v1 -> v2 -> v3 -> 現在のバージョンという移行チェーンを実行します。
  • 寛容なリーダー ― 追加のみの変更に適しています。新しいフィールドにはデフォルト値が設定され、削除されたフィールドは無視されます。JsonUtility で自動的に処理されます。

スキャフォールド(移行チェーン):

public static class SaveMigrator
{
    private static readonly int CurrentVersion = 3;

    public static GameSaveData LoadAndMigrate(string json)
    {
        // Parse version from raw JSON first
        var versionCheck = JsonUtility.FromJson<VersionOnly>(json);
        int version = versionCheck.version;

        if (version == CurrentVersion)
            return JsonUtility.FromJson<GameSaveData>(json);

        // Sequential migration using Newtonsoft for raw JSON manipulation
        var jObj = Newtonsoft.Json.Linq.JObject.Parse(json);

        if (version < 2) MigrateV1ToV2(jObj);
        if (version < 3) MigrateV2ToV3(jObj);

        jObj["version"] = CurrentVersion;
        return Newtonsoft.Json.JsonConvert.DeserializeObject<GameSaveData>(jObj.ToString());
    }

    static void MigrateV1ToV2(Newtonsoft.Json.Linq.JObject data)
    {
        // v2 moved "playerX/Y/Z" into "player.position" array
        var player = (Newtonsoft.Json.Linq.JObject)data["player"];
        if (player != null && player["playerX"] != null)
        {
            float x = (float)player["playerX"];
            float y = (float)player["playerY"];
            float z = (float)player["playerZ"];
            player["position"] = new Newtonsoft.Json.Linq.JArray(x, y, z);
            player.Remove("playerX"); player.Remove("playerY"); player.Remove("playerZ");
        }
    }

    static void MigrateV2ToV3(Newtonsoft.Json.Linq.JObject data)
    {
        // v3 added inventory system (default to empty)
        if (data["inventory"] == null)
            data["inventory"] = Newtonsoft.Json.Linq.JObject.FromObject(new InventorySaveData());
    }

    [System.Serializable]
    private class VersionOnly { public int version; }
}

注意点:古い型定義がコードに存在しなくなっている可能性があるため、移行は新しい C# 型にデシリアライズする前の生の JSON に対して行う必要があります。構造変更には Newtonsoft の JObject を使用してください。各移行ステップは、バージョン N から N+1 への関数です。これらを順番に組み合わせてください。各バージョンのセーブ済み JSON を使って移行をテストしてください(テスト用フィクスチャを保存します)。


パターン:クラウドセーブの統合

使用場面:セーブデータをデバイス間で同期したり、ローカルストレージを超えて保存したりする場合

判断:

  • Unity Cloud Save(UGS) ― 管理型サービスで、キーと値、またはファイルのストレージを利用できます。プレイヤーごとに200KBまで無料です。API については unity-packages-services を参照してください。ほとんどのインディーゲームや中規模ゲームに最適です。
  • カスタムバックエンド ― 完全な制御、無制限のストレージ、カスタムの競合解決が可能です。構築と保守は自分で行います。

スキャフォールド(ローカル優先のクラウド同期):

public class SaveManager : MonoBehaviour
{
    public async Awaitable Save(GameSaveData data)
    {
        // Always save locally first (fast, reliable)
        SaveFileIO.SaveJson(data, "savegame.json");

        // Then sync to cloud in background (best-effort)
        try
        {
            var cloudData = new Dictionary<string, object>
            {
                { "savegame", JsonUtility.ToJson(data) }
            };
            await Unity.Services.CloudSave.CloudSaveService.Instance
                .Data.Player.SaveAsync(cloudData);
        }
        catch (Exception e)
        {
            Debug.LogWarning($"Cloud save failed (local save preserved): {e.Message}");
        }
    }
}

注意点:クラウドセーブではオフラインプレイへの対応が必要です。必ず最初にローカルへ保存し、バックグラウンドでクラウドに同期してください。CloudSaveException は適切に処理してください。ネットワーク障害によってローカルプレイが妨げられたり、ゲームがクラッシュしたりしてはなりません。競合解決には「最終書き込み優先」が最もシンプルです。マージ処理が必要な場合はタイムスタンプを使用してください。


パターン:オートセーブ戦略

使用場面:セーブを実行するタイミングを決める場合

判断:

  • チェックポイント方式 ― レベルクリア、休憩地点、手動セーブなど、特定のゲームイベントで保存します。明示的で、デザイナーが制御できます。従来から使われている方法です。
  • 定期的なオートセーブ ― N秒ごと、またはシーン遷移時に保存します。クラッシュに備える安全策です。チェックポイント方式との併用も可能です。
  • イベント駆動方式 ― アイテムの取得やクエストの完了など、重要な状態が変化するたびに保存します。粒度は細かいものの、I/O が増えます。

スキャフォールド(オートセーブ+モバイル向け安全対策):

public class AutoSaveManager : MonoBehaviour
{
    [SerializeField] private float autoSaveInterval = 60f; // seconds
    private float _timeSinceLastSave;

    void Update()
    {
        _timeSinceLastSave += Time.unscaledDeltaTime;
        if (_timeSinceLastSave >= autoSaveInterval)
        {
            _timeSinceLastSave = 0f;
            _ = PerformAutoSave(); // Fire-and-forget with error handling
        }
    }

    // Mobile: save on app background (OnApplicationQuit may not fire)
    void OnApplicationPause(bool paused)
    {
        if (paused)
            _ = PerformAutoSave();
    }

    void OnApplicationQuit()
    {
        // Synchronous save on quit (async may not complete)
        var data = GatherSaveData();
        SaveFileIO.SaveJson(data, "autosave.json");
    }

    async Awaitable PerformAutoSave()
    {
        try
        {
            await Awaitable.BackgroundThreadAsync();
            var data = GatherSaveData();
            SaveFileIO.SaveJson(data, "autosave.json");
            await Awaitable.MainThreadAsync();
        }
        catch (Exception e)
        {
            Debug.LogError($"Auto-save failed: {e.Message}");
        }
    }

    GameSaveData GatherSaveData()
    {
        var data = new GameSaveData();
        // Gather from all ISaveable systems
        return data;
    }
}

注意点:セーブは I/O に依存する処理です。モバイルでは OnApplicationPause(true) を使用してください。OS がアプリを終了させる場合、OnApplicationQuit は呼び出されないことがあります。OnApplicationQuit では同期 I/O を使用してください(プロセス終了前に非同期処理が完了しない可能性があります)。Update 内で同期的に保存してはいけません。BackgroundThreadAsync またはタイマーを使用してください。関連項目:終了シーケンスについては unity-lifecycle を参照してください。


アンチパターン

アンチパターン問題修正方法
BinaryFormatterセキュリティ上の脆弱性があり、非推奨ですJsonUtility または Newtonsoft を使用します
PlayerPrefs にゲームの状態を保存する構造がなく、バージョン管理もできず、キーが衝突しますJSON ファイルと persistentDataPath を使用します
MonoBehaviour を直接保存するシリアライズできず、セーブデータがシーンに依存します[Serializable] を付けたプレーンな C# DTO を使用します
セーブデータにバージョンフィールドがない形式が変わったときに移行できません常に int version を含めます
Update 内で同期的にセーブする特にモバイルでフレームの停止が発生しますBackgroundThreadAsync またはタイマーを使用します
バックアップ/アトミック書き込みがない書き込み中のクラッシュでセーブデータが破損します.tmp に書き込んでから、アトミックに名前を変更します

関連スキル

  • unity-lifecycle -- OnApplicationPause、OnApplicationQuit、終了シーケンスのタイミング
  • unity-packages-services -- Unity Cloud Save API(重複して扱わないでください)
  • unity-async-patterns -- 非同期 I/O のための BackgroundThreadAsync
  • unity-data-driven -- JsonUtility と Newtonsoft の使い分け、設定データのバージョン管理パターン

追加リソース

コメント