Save/Load
クラッシュ耐性のあるアトミック書き込みとバージョンマイグレーションを備えた、スロットベースの JSON セーブ。
クラッシュ耐性のあるアトミック書き込みとバージョンマイグレーションを備えたスロットベースの JSON セーブ · 1 行のオフスイッチ:
NullSaveService
概要
セーブサービスはゲームの状態をディスクに書き込み、後で復元します — セーブの途中でゲームがクラッシュしても、破損はありません。"slot0" や "autosave" のような名前付きスロット(スロットは単に拡張子なしのファイル名です)を指定して、Save<T>(slot, data) または Load<T>(slot) を呼びます。残りはサービスが処理します: JSON シリアライズ、アトミックなファイル書き込み(一時ファイルに書いてから入れ替え)、そしてデータフォーマットが変わったときのスキーママイグレーション。セーブファイルが破損していたり、そのバージョンがもはやマイグレーションできない場合は、例外ではなく明示的な結果コードを受け取ります — リカバリー体験の主導権はあなたにあります。
簡単な使用例
using CommonGameSystem.Core;
using UnityEngine;
using Logger = CommonGameSystem.Core.Logger; // Unity has its own Logger type
[DefaultExecutionOrder(100)]
public class SaveLoadDemo : MonoBehaviour
{
private ISaveService _save;
void Awake()
{
_save = ServiceLocator.Resolve<ISaveService>();
}
public void SaveGame(string slot, PlayerData data)
{
var result = _save.Save(slot, data);
if (result.Status != SaveStatus.Ok)
Logger.Error(Logger.Categories.Save, $"Save failed: {result.Message}");
}
public void LoadGame(string slot)
{
var result = _save.Load<PlayerData>(slot);
if (result.Status == SaveStatus.Ok)
ApplyPlayerData(result.Value);
else
Logger.Warning(Logger.Categories.Save, $"Load failed: {result.Message}");
}
private void ApplyPlayerData(PlayerData data)
{
// Apply the loaded values to your game state here.
}
}
[System.Serializable]
public class PlayerData
{
public int level;
public float health;
}
セーブファイルはデフォルトで {Application.persistentDataPath}/saves/{slot}.json に置かれます。
API の全体像
書き込み
-
SaveResult Save<T>(string slot, T data) where T : class—dataをシリアライズし、名前付きスロットへアトミックに書き込みます。成功時はSaveStatus.Okを、ディスクエラー(ディスクフル、権限)時はIOFailureを返します — その場合、既存ファイルは完全に無傷のまま残ります。不正なスロット名にはArgumentNullException/ArgumentExceptionをスローします — それらは呼び出しコードのバグであって、ディスクの状態ではありません。 -
SaveResult Delete(string slot)— スロットファイルを削除します。成功時はOk、スロット(またはセーブフォルダ自体)が存在しなければNotFound— 2 回呼んでも安全で、例外なし — ファイルシステムが削除を拒めばIOFailureを返します。相棒の.bakバックアップファイルは意図的にそのまま残されます。その寿命は、そのスロットへの次のSaveが管理します。 -
void RegisterMigration(int fromVersion, ISaveMigration migration)— バージョンvからv + 1への 1 つのスキーママイグレーションステップを、純粋な JSON テキスト変換として登録します。fromVersionは 1 以上でなければならず、migration は null 不可です。マイグレーションは起動時に一度、最初のLoadの前に登録してください。2 つのマイグレーションが同じfromVersionを共有する場合、後から登録した方が勝ちます。
読み込み
-
SaveLoadResult<T> Load<T>(string slot) where T : class— スロットを読み取ってデシリアライズします。成功時は値付きのOkを返します。スロットが存在しなければNotFound、ファイルをパースできなければ(またはマイグレーションステップがスローしたら)Corrupt、ファイルを現在のバージョンまで引き上げられなければVersionUnsupportedを返します。ディスクやデータの状態に対しては決してスローしません — スローするのは不正なスロット名に対してだけです。 -
bool Exists(string slot)— スロットファイルがディスクに存在すればtrueを返します。ファイルが読み取り可能か、整形式かはチェックしません — それにはLoadを使ってください。 -
IReadOnlyList<string> ListSlots()— すべてのスロット名(拡張子なしのファイル名)をアルファベット順で列挙します。セーブがまだ 1 つもなければ空のリストを返します。副作用としてセーブフォルダを作ることは決してありません。
プロパティ
int CurrentSchemaVersion { get; }— すべての新しいセーブに刻印されるスキーマバージョン。すでにこのバージョンにあるファイルはマイグレーションなしでロードされます。デフォルトは 1 で、データフォーマットが変わったらSaveServiceOptionsを通じて引き上げます。
結果型
どちらの結果型も、public な読み取り専用フィールドを持つ軽量な struct です:
SaveResult—Status(SaveStatus)とMessage(診断用文字列。Okのときはnull。ログ向けで、プレイヤー向けではありません)。SaveLoadResult<T>—Status、Value(Status == Okのときにのみ非 null)、Message。
SaveStatus の値:
| ステータス | 意味 |
|---|---|
Ok | 操作は正常に完了しました。 |
NotFound | スロットファイルが存在しません。Load と Delete が返します。 |
Corrupt | ファイルは存在するがパースまたはデシリアライズできませんでした — 不正な JSON、フィールド欠落、型不一致、あるいはスローしたマイグレーションステップ。ファイルとその .bak はバイト単位で無変更のまま残るので、手作業での復旧は依然として可能です。 |
VersionUnsupported | 保存されているバージョンが CurrentSchemaVersion より新しい(セーブは決してダウングレードしません)、またはマイグレーションチェーンに欠落があってファイルを前へ進められない状態です。ファイルは無変更のまま保全されます。 |
IOFailure | 書き込みまたは削除中にファイルシステムエラーが発生しました。対象ファイルとその .bak は無変更のまま残り、根本の例外はスローされる代わりにログへ記録されます。 |
スキーママイグレーション
セーブデータのフォーマットを変えるときは、CurrentSchemaVersion を上げ、バージョンステップごとに 1 つの ISaveMigration を登録します。各ステップは、生のペイロード JSON テキストをバージョン v から v + 1 へ変換します — ファイルアクセスなし、副作用なし、string を受け取って string を返すだけです。Load では、データが現在のバージョンに達するまでステップを順に適用し、それからデシリアライズします。
using CommonGameSystem.Core;
// v1 stored "oldField"; v2 renamed it to "newField".
public sealed class V1ToV2Migration : ISaveMigration
{
public string Migrate(string payloadJson)
=> payloadJson.Replace("\"oldField\"", "\"newField\"");
}
// Once at startup, before the first Load:
_save.RegisterMigration(1, new V1ToV2Migration());
マイグレーションステップがスローすると、Load は Corrupt を返し、ディスク上のファイルは無傷のままです。下方向へのマイグレーションはサポートされません: あなたのゲームのより新しいバージョンが書いたファイルは VersionUnsupported を返します。
カスタムの保存先・拡張子・シリアライザー
デフォルトのサービスは Bootstrap が標準オプションで登録します。フォルダ名、ファイル拡張子、バックアップの挙動、スキーマバージョンを変えるには、自分でインスタンスを構築して登録を差し替えます:
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class CustomSaveInstaller : MonoBehaviour
{
void Awake()
{
var options = new SaveServiceOptions(
saveDirectoryName: "mygame_saves", // folder inside persistentDataPath (default "saves")
fileExtension: ".sav", // default ".json"
backupExtension: ".sav.bak", // default ".bak"; must differ from fileExtension
currentSchemaVersion: 2, // default 1; must be >= 1
createBackup: true); // default true — keep a backup of the prior save
ServiceLocator.Replace<ISaveService>(
new SaveService(new JsonUtilitySaveSerializer(), options));
}
}
コンストラクターはすべての引数を検証し、不正な値には ArgumentException をスローします(パス区切り文字を含むディレクトリ名、先頭のドットがない拡張子、ファイル拡張子とバックアップ拡張子の一致、1 未満のバージョン)。
Newtonsoft JSON や System.Text.Json、暗号化を使うには、ISaveSerializer を実装し、JsonUtilitySaveSerializer の代わりに SaveService のコンストラクターへ渡してください。
オフにする
ServiceLocator.Replace<ISaveService>(new NullSaveService());
この 1 行で、呼び出し側のコードに一切触れずに永続化を無効化できます。Save は(何もせずに)Ok を返し、Load は NotFound、Exists は false、ListSlots() は空のリストを返します。ディスクアクセスゼロ、アロケーションゼロ。開発中やヘッドレスなサーバービルドに便利です。なお、Configuration サービスはデフォルトで設定をセーブサービス経由で保存します — そのルーティングは起動時に一度だけ決まるため、セッション中それより前に書かれた設定は、後から NullSaveService へ差し替えても影響を受けません。
挙動とエッジケース
-
メインスレッド専用。 すべてのメソッドは、Unity のメインスレッドで実行されているかをチェックします(このチェックは Release ビルドでは除去されます)。他のスレッドからの呼び出しは
InvalidOperationExceptionをスローします。 -
不正なスロット名は即座にスローします。 スロットはプレーンなファイル名の語幹でなければなりません:
nullでなく、空や空白でもなく、パス区切り文字なし、..なし、:なし、そしてファイルシステムがファイル名に禁じる文字もなしです。違反はディスクアクセスの前にArgumentNullExceptionまたはArgumentExceptionをスローします — 結果コードにはラップされません。実際のディスク状態(ファイル欠落、破損、バージョンギャップ)は、代わりに結果コードとして返ります。 -
アトミック書き込み。 サービスはまず
.tmpファイルに書き、それからFile.Replaceで入れ替えます。入れ替えが(たとえば電源断で)中断されても、元のスロットは無傷のままです。次のSaveは残った.tmpファイルを単に上書きします。さらに、直前のセーブの.bakバックアップも自動で作られます。ディスクの書き込み量を抑えたければSaveServiceOptions.CreateBackupで無効化できます(スキーマバージョンを上げている最中はお勧めしません — マイグレーション書き込みが失敗したとき、復旧に使える.bakがなくなります)。 -
失敗がデータを破壊することはありません。
Ok以外のすべての結果 —Corrupt、VersionUnsupported、IOFailure— は、スロットファイルとその.bakをバイト単位で無変更のまま残します。サービスが、読めなかったファイルを「後片付け」することは決してありません。 -
IL2CPP / link.xml。 サービス自身の型はすでに IL2CPP のコードストリッピングから保護されています。あなた自身のセーブデータクラス(上の
PlayerDataなど)は、あなたのプロジェクトのlink.xmlで保護しなければ、ストリップされたビルドでのデシリアライズに失敗します。 -
シリアライズの制限。 デフォルトのシリアライザーは Unity の
JsonUtilityを使います: public フィールドと[SerializeField]フィールドのみ、自動プロパティ不可、UnityEngine.Object参照不可、参照循環不可です。フィールドが静かに保存されない場合、たいていの犯人は自動プロパティです — public フィールドに切り替えてください。
関連ページ
- Bootstrap — サービスが自動的に起動する仕組み
- Configuration — このサービスの上に構築された設定の永続化
- Logger —
Logger.Categories.Saveの下でのセーブ診断 - Service Locator — サービスを独自実装に差し替える
- マニュアル: トラブルシューティング —
CorruptとVersionUnsupportedからの復旧