Saving progress: PlayerPrefs, JSON, and when each is right
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
PlayerPrefs.SetFloat("music", 0.7f);
PlayerPrefs.SetInt("language", 1);
PlayerPrefs.Save();
float music = PlayerPrefs.GetFloat("music", 1f); // 1f if never setIt 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
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
// 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.