Saving & Loading
Getting state onto disk without losing it
Saving is easy to get working and easy to get wrong in ways that only show up after release — when players already have saves you cannot afford to break.
Pick the right storage
| Method | Good for | Avoid when |
|---|---|---|
PlayerPrefs | Settings, volume, best score | Anything structured, or anything cheatable. |
JSON file | Full game state | You need to hide the contents. |
Binary file | Large or private data | You want to debug it by eye. |
Cloud save | Cross-device progress | You have no account system. |
PlayerPrefs is a registry write on Windows and a plist on macOS. It is fine for a volume slider and wrong for an inventory. It also has no atomicity — a crash mid-write can leave half your keys updated.
Where the file goes
Always Application.persistentDataPath. It is the one location that is writable on every platform and survives an app update.
C#
// Correct: writable everywhere, survives updates
string path = Path.Combine(Application.persistentDataPath, "save_01.json");
// Wrong: read-only in a build, and inside the app bundle on mobile
string bad = Path.Combine(Application.dataPath, "save_01.json");A JSON save that works
C#
using System.IO;
using UnityEngine;
[System.Serializable]
public class SaveData
{
public int version = 2; // bump when the shape changes
public string sceneName;
public float[] playerPosition;
public int health;
public SavedSlot[] inventory;
}
public static class SaveSystem
{
private static string PathFor(int slot) =>
Path.Combine(Application.persistentDataPath, $"save_{slot:00}.json");
public static void Save(SaveData data, int slot)
{
string json = JsonUtility.ToJson(data, prettyPrint: true);
string target = PathFor(slot);
string temp = target + ".tmp";
// Write to a temp file first, then swap. A crash mid-write
// then costs you the new save, not the previous one too.
File.WriteAllText(temp, json);
if (File.Exists(target)) File.Delete(target);
File.Move(temp, target);
}
public static SaveData Load(int slot)
{
string path = PathFor(slot);
if (!File.Exists(path)) return null;
try
{
return JsonUtility.FromJson<SaveData>(File.ReadAllText(path));
}
catch (System.Exception e)
{
Debug.LogError($"Save {slot} is corrupt: {e.Message}");
return null; // let the caller offer a new game
}
}
}The temp-file-then-move pattern is the whole reason players do not lose a save when the game crashes during autosave. It costs four lines.
What JsonUtility will not do
Unity's built-in JsonUtility is fast but limited. Knowing the limits up front saves a confusing afternoon.
- It cannot serialise
Dictionary. Use parallel arrays or a list of key/value structs. - It cannot serialise a top-level array. Wrap it in a class.
- It ignores properties — only public fields, or private fields marked
[SerializeField]. - It cannot serialise
nullfor a nested class; you get a default instance back. - It cannot handle polymorphism. A
List<Item>holding aWeaponloads back as anItem.
When those limits bite, Newtonsoft Json.NET is available as a Unity package (
com.unity.nuget.newtonsoft-json) and handles all of the above.Versioning
The moment you ship, players have saves in the old shape. A version number plus a migration step keeps them working.
C#
public static SaveData Migrate(SaveData data)
{
if (data.version < 2)
{
// v1 stored a single item id; v2 stores an inventory array
data.inventory = string.IsNullOrEmpty(data.legacyItemId)
? new SavedSlot[0]
: new[] { new SavedSlot { itemId = data.legacyItemId, quantity = 1 } };
data.version = 2;
}
return data;
}Deleting a field is not free. Old saves still contain it, and if your migration reads it, keep the field around marked obsolete rather than removing it outright.
What not to save
- GameObject references. They do not survive a scene reload. Save an id and find the object on load.
- Vector3 directly is fine with JsonUtility, but a float array is more portable across tools.
- Anything derivable. If max health comes from level, save the level.
- Absolute paths. They differ per machine and per platform.
What to take away
- PlayerPrefs for settings; a JSON file for game state.
- Always write to
Application.persistentDataPath. - Write to a temp file and move it, so a crash cannot destroy the old save.
- Wrap loading in try/catch and handle corrupt files as a normal case.
- Put a version number in the save from day one.
- Save ids and values, never object references.