U
21 / 22 · 9 min
FAQ

Saving progress: PlayerPrefs, JSON, and when each is right

the short answer

How do I save the player's progress?

Use PlayerPrefs for a handful of settings, and a JSON file under Application.persistentDataPath for anything that is really game state. Write the file in OnApplicationPause as well as OnApplicationQuit — on a phone, Quit often never runs.

Unity gives you two ways to save and they are not competitors: PlayerPrefs is for a handful of settings, a JSON file is for game state. Using PlayerPrefs for game state is the mistake that gets a project halfway to release before it becomes a problem.

PlayerPrefs — settings, and nothing else

What PlayerPrefs is genuinely good at
PlayerPrefs.SetFloat("music", 0.7f);
PlayerPrefs.SetInt("language", 1);
PlayerPrefs.Save();

float music = PlayerPrefs.GetFloat("music", 1f);   // 1f if never set
  • It stores three types only: int, float and string. Anything else has to be flattened into one of them by hand.

  • It is plain text on disk — the registry on Windows, a .plist on macOS, an XML file on Android. A player who wants to edit their score can, in a text editor.

  • There is no way to list what is in it and no way to version it. A save format that grows will outgrow PlayerPrefs, and there is no migration path.

JSON — actual game state

SaveSystem.cs — one file, two methods
using System.IO;
using UnityEngine;

[System.Serializable]
public class SaveData
{
    public int version = 1;          // pays for itself the first time you change the format
    public int level;
    public float health;
    public string[] unlocked;
}

public static class SaveSystem
{
    // persistentDataPath is the only folder that survives an app update on a
    // phone, and the only one you are allowed to write to on iOS.
    static string Path => System.IO.Path.Combine(Application.persistentDataPath, "save.json");

    public static void Save(SaveData data)
    {
        // Write to a temporary file, then swap. A crash mid-write then loses
        // the new save instead of destroying the old one.
        string tmp = Path + ".tmp";
        File.WriteAllText(tmp, JsonUtility.ToJson(data, true));
        File.Copy(tmp, Path, true);
        File.Delete(tmp);
    }

    public static SaveData Load()
    {
        if (!File.Exists(Path)) return new SaveData();
        return JsonUtility.FromJson<SaveData>(File.ReadAllText(Path));
    }
}

What JsonUtility will and will not serialise

  • It saves public fields, and private fields marked [SerializeField]. It ignores properties with { get; set; } entirely — a very common surprise.

  • It cannot serialise a Dictionary. Save two parallel arrays, or a list of small [System.Serializable] classes.

  • The class itself needs [System.Serializable] and must not inherit MonoBehaviour. Save data is data, not a component.

Saving at the right moment

On a phone this is the difference between saving and not saving
// Android and iOS kill a backgrounded app without ever calling
// OnApplicationQuit. Pause is the last callback you are guaranteed.
void OnApplicationPause(bool paused)
{
    if (paused) SaveSystem.Save(current);
}

void OnApplicationQuit()
{
    SaveSystem.Save(current);
}

the lesson that builds this

Saving progress

This post is a standalone recipe. In the course, the same thing is built as part of the one project that runs through all five tabs — Export to Mobile, lesson 04.