Quick answer: Use Application.persistentDataPath for all save files, serialize data with JsonUtility or Newtonsoft JSON, write to a temp file then rename for atomicity, and include a version number in every save so you can migrate old formats without breaking them.

A save system is critical infrastructure that touches every part of your game. Getting it wrong means corrupted saves, lost progress, and the kind of Steam reviews that start with “DO NOT BUY.” This guide covers everything you need to build a reliable save system in Unity that works across every platform.

Stop Using PlayerPrefs for Game Saves

PlayerPrefs is for small settings: audio volume, graphics quality, control preferences. It is not designed for structured game data. On Windows, PlayerPrefs writes to the registry, which has size limits and is not meant for large data. On WebGL, it uses the browser’s IndexedDB with strict quotas.

Use PlayerPrefs only for user preferences. Use file-based saves with JSON for everything else.

Application.persistentDataPath Is Your Save Directory

Application.persistentDataPath returns a writable directory that persists between sessions and survives app updates. It is the correct location for all save files on every platform:

Windows: C:/Users/<user>/AppData/LocalLow/<company>/<product>/

macOS: ~/Library/Application Support/<company>/<product>/

Linux: ~/.config/unity3d/<company>/<product>/

Android: /data/data/<package>/files/

iOS: /var/mobile/Containers/Data/Application/<guid>/Documents/

Never use Application.dataPath or Application.streamingAssetsPath for saves. These are read-only in builds and will fail silently or throw exceptions on most platforms.

Structuring Save Data with Serializable Classes

Define your save data as plain C# classes marked with [System.Serializable]. This separates your save format from your runtime game objects and makes serialization predictable.

using System.Collections.Generic;

[System.Serializable]
public class SaveData
{
    public int version = 1;
    public string timestamp;
    public PlayerSaveData player;
    public List<InventoryItem> inventory = new();
    public List<string> completedQuests = new();
}

[System.Serializable]
public class PlayerSaveData
{
    public float posX, posY, posZ;
    public int health = 100;
    public int gold = 0;
    public string currentScene;
}

[System.Serializable]
public class InventoryItem
{
    public string itemId;
    public int quantity;
}

Unity’s JsonUtility handles these classes directly but has limitations: no dictionaries, no polymorphism, no null support for value types. If you need those features, use Newtonsoft JSON (included in Unity via the com.unity.nuget.newtonsoft-json package).

The Save Manager

Centralize all file operations in a single class. This makes it easy to add features like encryption, backups, and versioning without touching game logic.

using UnityEngine;
using System.IO;

public static class SaveManager
{
    private static string SaveDir =>
        Path.Combine(Application.persistentDataPath, "saves");

    public static bool Save(int slot, SaveData data)
    {
        Directory.CreateDirectory(SaveDir);
        data.timestamp = System.DateTime.UtcNow.ToString("o");

        string json = JsonUtility.ToJson(data, true);
        string path = SlotPath(slot);
        string tmpPath = path + ".tmp";
        string bakPath = path + ".bak";

        try
        {
            File.WriteAllText(tmpPath, json);

            // Atomic swap with backup
            if (File.Exists(path))
                File.Copy(path, bakPath, true);
            File.Move(tmpPath, path, true);
            return true;
        }
        catch (IOException e)
        {
            Debug.LogError($"Save failed: {e.Message}");
            return false;
        }
    }

    public static SaveData Load(int slot)
    {
        string path = SlotPath(slot);
        if (!File.Exists(path))
        {
            // Try backup
            string bakPath = path + ".bak";
            if (File.Exists(bakPath))
                path = bakPath;
            else
                return null;
        }

        try
        {
            string json = File.ReadAllText(path);
            SaveData data = JsonUtility.FromJson<SaveData>(json);
            return MigrateSave(data);
        }
        catch (System.Exception e)
        {
            Debug.LogError($"Load failed: {e.Message}");
            return null;
        }
    }

    private static string SlotPath(int slot) =>
        Path.Combine(SaveDir, $"slot_{slot}.json");
}

Save File Versioning

Always include a version field in your save data. When your save format changes between updates, use the version number to migrate old saves forward instead of breaking them.

private static SaveData MigrateSave(SaveData data)
{
    if (data.version < 1)
    {
        // v0 -> v1: inventory was added
        if (data.inventory == null)
            data.inventory = new();
    }

    if (data.version < 2)
    {
        // v1 -> v2: split position into components
        // (handle any data transformation needed)
    }

    data.version = 2; // Current version
    return data;
}

Each migration step handles one version bump. A save from version 0 runs through every step to reach the current version. New fields should have sensible defaults in their class declarations so old saves that lack them still load correctly.

Atomic Writes and Backup Saves

If your game crashes while File.WriteAllText is running, you can end up with a partially written save file. The save manager above already handles this with the write-to-temp-then-rename pattern. Here is why each step matters:

1. Write to .tmp file: If the write fails or is interrupted, only the temp file is damaged. The real save is untouched.

2. Copy current save to .bak: Even after a successful write, keep the previous save as a backup. If the new save turns out to be corrupt (bad serialization, missing data), you can recover from the backup.

3. Move .tmp to final path: File.Move with overwrite is atomic on most filesystems. The save file is either the old version or the new version, never a partial write.

Encrypting Save Data

For games with competitive elements, leaderboards, or in-app purchases, you may want to encrypt save files to discourage tampering:

using System.Security.Cryptography;
using System.Text;

public static class SaveEncryption
{
    private static readonly byte[] Key = Encoding.UTF8.GetBytes("your-32-byte-encryption-key-here!");
    private static readonly byte[] IV = Encoding.UTF8.GetBytes("16-byte-iv-here!");

    public static string Encrypt(string plainText)
    {
        using var aes = Aes.Create();
        aes.Key = Key;
        aes.IV = IV;
        using var encryptor = aes.CreateEncryptor();
        byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
        byte[] encrypted = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
        return System.Convert.ToBase64String(encrypted);
    }

    public static string Decrypt(string cipherText)
    {
        using var aes = Aes.Create();
        aes.Key = Key;
        aes.IV = IV;
        using var decryptor = aes.CreateDecryptor();
        byte[] cipherBytes = System.Convert.FromBase64String(cipherText);
        byte[] decrypted = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
        return Encoding.UTF8.GetString(decrypted);
    }
}

Keep in mind that client-side encryption is always breakable with enough effort. It raises the barrier for casual cheating but will not stop determined players. For single-player games, plain JSON is usually fine and much easier to debug.

Autosave

Implement autosave with a coroutine that runs on a fixed interval. Also trigger saves on key game events like scene transitions, quest completions, and pause menu opens.

using System.Collections;
using UnityEngine;

public class AutosaveController : MonoBehaviour
{
    [SerializeField] private float autosaveInterval = 300f; // 5 minutes
    private const int AutosaveSlot = 0;

    private void Start()
    {
        StartCoroutine(AutosaveLoop());
    }

    private IEnumerator AutosaveLoop()
    {
        while (true)
        {
            yield return new WaitForSecondsRealtime(autosaveInterval);
            SaveManager.Save(AutosaveSlot, GatherSaveData());
            Debug.Log("Autosave complete");
        }
    }

    private void OnApplicationPause(bool paused)
    {
        if (paused)
            SaveManager.Save(AutosaveSlot, GatherSaveData());
    }

    private void OnApplicationQuit()
    {
        SaveManager.Save(AutosaveSlot, GatherSaveData());
    }

    private SaveData GatherSaveData()
    {
        // Collect current game state into SaveData
        return new SaveData();
    }
}

On mobile, OnApplicationPause is essential because the OS can kill your app at any time after it moves to the background. On consoles, follow the platform’s save notification requirements—most require showing a “saving” indicator and preventing the player from quitting during a write.

Platform Considerations

WebGL: File I/O is not available. Use PlayerPrefs (backed by IndexedDB) or implement cloud saves via UnityWebRequest. Storage is limited and can be cleared by the browser.

Consoles (PlayStation, Xbox, Switch): Each platform has its own save data API that you must use instead of raw file I/O. These APIs handle storage management, user profiles, and platform-mandated save indicators. Check the platform SDK documentation for specifics.

Mobile: Storage can be limited. On Android, Application.persistentDataPath can be cleared by the user via app settings. On iOS, files in the Documents directory are backed up to iCloud by default. For critical progress, implement cloud saves as a backup.

Related Issues

If serialized fields are disappearing after renaming variables, see Fix: Unity Serialization Field Lost After Rename. For tracking save-related bugs reported by players, read Bug Reporting Tools for Unity Developers.

Never ship without testing your save system on every target platform in a real build. The editor hides path issues, permission problems, and serialization edge cases that will cost your players their progress.