unity-save-system
Skill32 starsChanged 59 days agoMITView on GitHub
Unity save/load system architecture. Serialization format selection, save data DTOs, versioning and migration, cloud sync, PlayerPrefs scoping, auto-save strategies, mobile persistence. DECISION format: WHEN/DECISION/SCAFFOLD/GOTCHA. Based on Unity 6.3 LTS.
In this repository34 other files it gives its agents.
- unity-2dSkill
- unity-3d-mathSkill
- unity-ai-navigationSkill
- unity-animationSkill
- unity-async-patternsSkill
- unity-audioSkill
- unity-cinemachineSkill
- unity-data-drivenSkill
- unity-ecs-dotsSkill
- unity-editor-toolsSkill
- unity-foundationsSkill
- unity-game-architectureSkill
- unity-game-loopSkill
- unity-graphicsSkill
- unity-input-correctnessSkill
- unity-inputSkill
- unity-level-designSkill
- unity-lifecycleSkill
- unity-lighting-vfxSkill
- unity-multiplayerSkill
- unity-npc-behaviorSkill
- unity-packages-servicesSkill
- unity-performanceSkill
- unity-physics-queriesSkill
- unity-physicsSkill
- unity-platformsSkill
- unity-procedural-genSkill
- unity-scene-assetsSkill
- unity-scriptingSkill
- unity-state-machinesSkill
- unity-testingSkill
- unity-ui-patternsSkill
- unity-uiSkill
- unity-xrSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 7 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 21
WHEN: Choosing how to serialize save data to disk
- Distinctive: line 80
WHEN: Deciding what to serialize and how to structure it
- Distinctive: line 131
WHEN: Choosing where to write save files
- Distinctive: line 155
WHEN: Using PlayerPrefs for settings and small persistent values
- Distinctive: line 206
WHEN: Save format changes between game updates (fields added, renamed, restructured)
- Distinctive: line 269
WHEN: Save data should sync across devices or persist beyond local storage
- Distinctive: line 308
WHEN: Deciding when to trigger saves
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Save/Load Systems -- Decision Patterns
Prerequisite skills:
unity-lifecycle(OnApplicationPause, OnApplicationQuit, quit sequence),unity-data-driven(JSON serialization, versioning),unity-packages-services(Cloud Save API),unity-async-patterns(BackgroundThreadAsync)
These patterns address the most common save system failure: Claude uses PlayerPrefs for everything, produces brittle serialization with no versioning, and ignores mobile-specific persistence requirements.
PATTERN: Serialization Format Selection
WHEN: Choosing how to serialize save data to disk
DECISION:
- JSON (
JsonUtility) -- Human-readable, debuggable, small save files. No dictionary/polymorphism. Best default for most games. - JSON (Newtonsoft) -- Full JSON features (dictionaries, polymorphism, LINQ). Slightly larger dependency. Best when save data is complex.
- Binary (custom or MessagePack) -- Performance-critical, large saves, anti-cheat (harder to edit). Not human-readable. Best for competitive games or very large worlds.
- BinaryFormatter -- NEVER USE. Security vulnerability (arbitrary code execution on deserialization). Deprecated by Unity and Microsoft.
SCAFFOLD (JSON save/load):
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);
}GOTCHA: BinaryFormatter is a security hole -- deserialization can execute arbitrary code. Unity and Microsoft have deprecated it. If you find it in existing code, replace it immediately. JsonUtility cannot serialize dictionaries -- convert to List<SerializableKeyValue<K,V>> or use Newtonsoft. The atomic write pattern (write to .tmp, rename) prevents data corruption if the app crashes mid-write.
PATTERN: Save Data Architecture (DTO Pattern)
WHEN: Deciding what to serialize and how to structure it
DECISION:
- Single monolithic save -- One
SaveDataclass with everything. Load/save is atomic. Best for small games with fast save/load. - Per-system save files --
PlayerSave.json,InventorySave.json,WorldSave.json. Partial load, smaller writes, easier to migrate individual systems.
SCAFFOLD (ISaveable interface):
// 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
}GOTCHA: Save DTOs must be plain C# classes with [System.Serializable], NOT MonoBehaviours or ScriptableObjects. They contain only serializable types (primitives, arrays, lists, nested [Serializable] classes). Unity types like Vector3 serialize fine with JsonUtility but NOT with Newtonsoft without a custom converter -- use float[] for portability.
PATTERN: Save File Location
WHEN: Choosing where to write save files
DECISION:
Application.persistentDataPath(correct default) -- Writable, survives app updates, platform-appropriate. Use for all save data.Application.streamingAssetsPath-- READ-ONLY in builds. Never write here. For bundled read-only data only.PlayerPrefs-- Key-value only. For settings/preferences. NOT for game state.
SCAFFOLD:
// 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);GOTCHA: persistentDataPath locations differ per platform: AppData/LocalLow/<company>/<product> (Windows), ~/Library/Application Support/<bundleID> (macOS), internal storage (Android/iOS). On WebGL, it uses IndexedDB (async, may fail in private browsing). Always use Path.Combine -- forward slashes work on macOS/Linux but Path.Combine is cross-platform safe.
PATTERN: PlayerPrefs Scoping
WHEN: Using PlayerPrefs for settings and small persistent values
DECISION:
- Direct PlayerPrefs calls -- Under 10 settings, minimal project. Simple.
- Wrapper class -- 10+ settings, need type safety, change events, defaults. Prevents key collision.
SCAFFOLD (Settings wrapper):
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();
}
}GOTCHA: PlayerPrefs keys are global strings -- no namespacing. Use prefixed keys ("audio.masterVolume", "video.fullscreen") to prevent collisions. PlayerPrefs has no bool type -- use int (0/1). On Windows, PlayerPrefs stores to the Windows Registry -- avoid large values. PlayerPrefs.Save() is automatic on OnApplicationQuit but call it manually after important changes on mobile (app may be killed without quit).
PATTERN: Save Versioning and Migration
WHEN: Save format changes between game updates (fields added, renamed, restructured)
DECISION:
- Version field + sequential migrators -- For breaking changes. Store
int versionin save. On load, run migration chain: v1 -> v2 -> v3 -> current. - Tolerant reader -- For additive-only changes. New fields get defaults, removed fields are ignored. Works with
JsonUtilityautomatically.
SCAFFOLD (Migration chain):
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; }
}GOTCHA: Migration must work on raw JSON (before deserializing to the new C# type) because the old type definition may no longer exist in code. Use Newtonsoft JObject for structural changes. Each migration step is a function from version N to N+1 -- compose them sequentially. Test migrations with saved JSON from each version (store test fixtures).
PATTERN: Cloud Save Integration
WHEN: Save data should sync across devices or persist beyond local storage
DECISION:
- Unity Cloud Save (UGS) -- Managed service, key-value or file storage, 200KB free per player. See
unity-packages-servicesfor API. Best for most indie/mid-size games. - Custom backend -- Full control, unlimited storage, custom conflict resolution. You build and maintain it.
SCAFFOLD (Local-first with cloud sync):
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}");
}
}
}GOTCHA: Cloud saves must handle offline play. Always save locally first, sync to cloud in the background. Handle CloudSaveException gracefully -- network failures should never prevent local play or crash the game. For conflict resolution, "last-write-wins" is simplest; use timestamps if you need merge logic.
PATTERN: Auto-Save Strategy
WHEN: Deciding when to trigger saves
DECISION:
- Checkpoint-based -- Save at specific game events (level complete, rest point, manual save). Explicit, designer-controlled. Traditional approach.
- Periodic auto-save -- Save every N seconds or on scene transition. Safety net against crashes. Can be combined with checkpoints.
- Event-driven -- Save whenever significant state changes (item acquired, quest completed). Granular but more I/O.
SCAFFOLD (Auto-save + mobile safety):
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;
}
}GOTCHA: Saving is I/O-bound. On mobile, use OnApplicationPause(true) -- OnApplicationQuit may not fire when the OS kills the app. In OnApplicationQuit, use synchronous I/O (async may not complete before the process exits). Never save in Update synchronously -- use BackgroundThreadAsync or a timer. Cross-ref: unity-lifecycle covers the quit sequence.
Anti-Patterns
| Anti-Pattern | Problem | Fix |
BinaryFormatter | Security vulnerability, deprecated | Use JsonUtility or Newtonsoft |
| Game state in PlayerPrefs | No structure, no versioning, key collision | Use JSON files + persistentDataPath |
| Saving MonoBehaviour directly | Not serializable, couples save to scene | Use plain C# DTOs with [Serializable] |
| No version field in save data | Cannot migrate when format changes | Always include int version |
| Synchronous save in Update | Frame hitches, especially on mobile | Use BackgroundThreadAsync or timer |
| No backup/atomic write | Crash during write corrupts save | Write to .tmp, rename atomically |
Related Skills
- unity-lifecycle -- OnApplicationPause, OnApplicationQuit, quit sequence timing
- unity-packages-services -- Unity Cloud Save API (do not duplicate)
- unity-async-patterns -- BackgroundThreadAsync for async I/O
- unity-data-driven -- JsonUtility vs Newtonsoft, versioning patterns for config data
Additional Resources
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

