Saving & Loading Games
Save the player's progress to a file and load it back, using a plain C# class and JSON.
Saving is like packing a suitcase. You don't pack the whole house; you choose the things you need (health, coins, level), fold them neatly into the case (JSON text), and put it in the cupboard (a file). Loading is unpacking the suitcase and putting everything back where it belongs.
PlayerPrefs or a save file?
| PlayerPrefs | Save file (this page) |
|---|---|
| A few settings: volume, graphics quality | Real progress: inventory, level, position, quests |
| One value at a time | Everything together in one file |
| Hard to have several save slots | Easy: one file per slot |
Step 1: describe what to save
Make a plain class that holds only the data. [System.Serializable] means "this can be turned into text".
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class SaveData
{
public int coins;
public int health = 100;
public string levelName = "Level1";
public Vector3 playerPosition;
public List<string> inventory = new List<string>();
}
JsonUtility saves public fields (or [SerializeField] private ones). It can't save properties, Dictionary, or references to GameObjects. Save an ID or name instead, like "sword_01".
Step 2: the save system
using System.IO;
using UnityEngine;
public static class SaveSystem
{
private static string PathFor(int slot) =>
Path.Combine(Application.persistentDataPath, $"save{slot}.json");
public static void Save(SaveData data, int slot = 0)
{
string json = JsonUtility.ToJson(data, prettyPrint: true);
File.WriteAllText(PathFor(slot), json);
}
public static SaveData Load(int slot = 0)
{
string path = PathFor(slot);
if (!File.Exists(path)) return new SaveData(); // no save yet: fresh game
try
{
return JsonUtility.FromJson<SaveData>(File.ReadAllText(path));
}
catch (System.Exception e)
{
Debug.LogWarning($"Save file was broken, starting fresh: {e.Message}");
return new SaveData();
}
}
public static bool HasSave(int slot = 0) => File.Exists(PathFor(slot));
public static void Delete(int slot = 0) => File.Delete(PathFor(slot));
}
Step 3: use it
using UnityEngine;
public class GameSaver : MonoBehaviour
{
[SerializeField] private Transform player;
[SerializeField] private Wallet wallet; // your own coin script
public void SaveGame()
{
var data = new SaveData
{
coins = wallet.Coins,
playerPosition = player.position,
};
SaveSystem.Save(data);
}
public void LoadGame()
{
SaveData data = SaveSystem.Load();
wallet.SetCoins(data.coins);
player.position = data.playerPosition;
}
}
Hook SaveGame and LoadGame up to buttons with a UnityEvent, or call SaveGame at checkpoints.
What the file looks like
{
"coins": 42,
"health": 100,
"levelName": "Level1",
"playerPosition": { "x": 3.5, "y": 1.0, "z": -2.0 },
"inventory": [ "sword_01", "potion_small" ]
}
To find it, log Application.persistentDataPath. On Windows it's inside AppData\LocalLow\<company>\<product>. It's plain text, so you can read it (and so can players).
Good habits
- Add a version number (
public int version = 1;). When you changeSaveDatalater, you can tell old saves apart. - New fields are safe: loading an old file leaves missing fields at their default values.
- Save IDs, not objects: store
"potion_small"and look it up in a list of ScriptableObject items when loading. - Need dictionaries? Use the Newtonsoft JSON package (
com.unity.nuget.newtonsoft-json); see Free Libraries. - WebGL: file saving works differently in the browser. Test there early if you're publishing to the web.
Common Mistakes
- Saving to
Application.dataPath(inside the game folder). In a finished game it often can't be written to, and on some platforms it's read-only. UsepersistentDataPath. - Forgetting
[System.Serializable], or using properties, so the file comes out as{}. - Trying to save a
GameObjectorTransformdirectly. - Not handling "no save file yet" on the very first launch.
See also
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official documentation.