Best Unity Save System for Beginners (PlayerPrefs vs JSON)

Compare PlayerPrefs and JSON for saving game data in Unity. Learn when to use each approach with code examples, pros and cons, and a complete save system architecture.

"How do I save my game?" is one of the most common questions beginner Unity developers ask — and one of the most confusing to answer because Unity offers multiple approaches with different trade-offs. The two most popular options are PlayerPrefs and JSON file serialization, and choosing the wrong one for your use case leads to pain down the road.

TL;DR: Use PlayerPrefs for small, simple data (settings, high scores, unlocks). Use JSON serialization for structured game state (inventory contents, quest progress, world state). This guide covers both approaches with complete code, a comparison table, and links to our ready-made PlayerPrefs Save Helper, JSON Save Utility, and Save System bundle.

By the end of this post, you'll know exactly which approach fits your project and how to implement it properly. Let's start with why saving matters more than most beginners realize.

Why Saving Matters

Players expect their progress to persist. Close the game, reopen it, and everything should be where they left it. This is so fundamental that players don't think of it as a "feature" — they think of its absence as a bug.

Beyond player expectations, your save system architecture affects your entire codebase. If you design it late, you'll be retrofitting serialization into classes that weren't built for it. If you design it early with the right approach, every new system you add can plug into saving naturally.

The key question isn't "how do I save" — it's "what data structure does my game's save file need?" A casual puzzle game that only needs to track level progress and settings has very different requirements from an RPG that needs to save inventory, quest states, NPC relationships, and world changes.

PlayerPrefs: Quick & Simple

PlayerPrefs is Unity's built-in key-value storage system. It stores data in the system registry (Windows), plist files (macOS), or shared preferences (Android). It supports three data types: int, float, and string.

C#
using UnityEngine;

public class SettingsManager : MonoBehaviour
{
    // Save settings
    public void SaveSettings(float musicVolume, float sfxVolume, int qualityLevel, bool fullscreen)
    {
        PlayerPrefs.SetFloat("MusicVolume", musicVolume);
        PlayerPrefs.SetFloat("SFXVolume", sfxVolume);
        PlayerPrefs.SetInt("QualityLevel", qualityLevel);
        PlayerPrefs.SetInt("Fullscreen", fullscreen ? 1 : 0); // no bool type, use int
        PlayerPrefs.SetString("LastSaveDate", System.DateTime.Now.ToString());
        PlayerPrefs.Save(); // flush to disk
    }

    // Load settings with defaults
    public void LoadSettings()
    {
        float musicVol = PlayerPrefs.GetFloat("MusicVolume", 0.8f);
        float sfxVol = PlayerPrefs.GetFloat("SFXVolume", 1.0f);
        int quality = PlayerPrefs.GetInt("QualityLevel", 2);
        bool fullscreen = PlayerPrefs.GetInt("Fullscreen", 1) == 1;

        // Apply the loaded values...
        AudioListener.volume = musicVol;
        QualitySettings.SetQualityLevel(quality);
        Screen.fullScreen = fullscreen;
    }

    // Save high score
    public void SaveHighScore(int score)
    {
        int currentBest = PlayerPrefs.GetInt("HighScore", 0);
        if (score > currentBest)
        {
            PlayerPrefs.SetInt("HighScore", score);
            PlayerPrefs.Save();
        }
    }
}

When to use PlayerPrefs:

  • Audio/video settings (volume, quality, resolution)
  • High scores and personal bests
  • Simple unlock flags ("has the player beaten level 5?")
  • Tutorial completion flags
  • Last selected character or game mode

Limitations of PlayerPrefs:

  • No structured data — You can't store an inventory, quest log, or any nested data without manually serializing it to a string.
  • No encryption — Data is stored in plain text. Players can edit it trivially on desktop platforms.
  • Platform inconsistency — Stored in the Windows registry on PC, which is a shared, polluted namespace. On WebGL, it uses IndexedDB with size limits.
  • Key collision risk — All PlayerPrefs share a flat namespace. Two different systems using the key "Score" will overwrite each other.
  • No versioning — If you rename a key, old save data is silently lost.

Our PlayerPrefs Save Helper script wraps PlayerPrefs with type-safe methods, automatic key prefixing (to prevent collisions), default value management, and a one-call reset function.

JSON: Flexible & Scalable

JSON serialization writes your game data as a structured text file to Application.persistentDataPath. You define a C# class that represents your entire save state, serialize it to JSON, and write it to disk. On load, you read the file, deserialize it back to the class, and apply the values.

C#
using System.IO;
using UnityEngine;
using System.Collections.Generic;

// Define your save data structure
[System.Serializable]
public class GameSaveData
{
    public int saveVersion = 1;
    public string playerName;
    public int currentLevel;
    public float playTime;
    public Vector3Serializable playerPosition;
    public List<string> unlockedAbilities = new List<string>();
    public List<InventoryEntry> inventory = new List<InventoryEntry>();
    public Dictionary<string, bool> questFlags; // Note: needs Newtonsoft for Dictionary
}

[System.Serializable]
public class Vector3Serializable
{
    public float x, y, z;
    public Vector3Serializable(Vector3 v) { x = v.x; y = v.y; z = v.z; }
    public Vector3 ToVector3() => new Vector3(x, y, z);
}

[System.Serializable]
public class InventoryEntry
{
    public string itemID;
    public int quantity;
    public int slotIndex;
}

// The save/load manager
public class JsonSaveManager : MonoBehaviour
{
    private string SavePath => Path.Combine(Application.persistentDataPath, "savegame.json");

    public void Save(GameSaveData data)
    {
        string json = JsonUtility.ToJson(data, true);
        File.WriteAllText(SavePath, json);
        Debug.Log($"Game saved to {SavePath}");
    }

    public GameSaveData Load()
    {
        if (!File.Exists(SavePath))
        {
            Debug.Log("No save file found, returning defaults.");
            return new GameSaveData();
        }

        string json = File.ReadAllText(SavePath);
        GameSaveData data = JsonUtility.FromJson<GameSaveData>(json);
        Debug.Log("Game loaded successfully.");
        return data;
    }

    public void DeleteSave()
    {
        if (File.Exists(SavePath))
        {
            File.Delete(SavePath);
            Debug.Log("Save file deleted.");
        }
    }

    public bool SaveExists() => File.Exists(SavePath);
}

The save file looks like this on disk, which is human-readable and debuggable:

json
{
    "saveVersion": 1,
    "playerName": "Hero",
    "currentLevel": 3,
    "playTime": 1847.5,
    "playerPosition": { "x": 12.5, "y": 0.0, "z": -3.2 },
    "unlockedAbilities": ["DoubleJump", "WallSlide", "Dash"],
    "inventory": [
        { "itemID": "health_potion", "quantity": 5, "slotIndex": 0 },
        { "itemID": "iron_sword", "quantity": 1, "slotIndex": 1 }
    ]
}

When to use JSON:

  • Any game with structured state (inventories, quest logs, skill trees)
  • Multiple save slots (each slot is a separate file)
  • Games where players need to back up or transfer saves
  • Any data that has nested objects or lists
  • When you need save versioning and migration

When to Use Which

Here's the decision framework:

  • Use PlayerPrefs if your save data is fewer than 10 key-value pairs, all values are simple types (int, float, string, bool), and you don't need multiple save slots. Typical: settings screens, casual puzzle games, hyper-casual high scores.
  • Use JSON if your save data has any nested structures (lists, objects within objects), you need multiple save slots, you want human-readable save files for debugging, or the data might grow as you add features. Typical: RPGs, survival games, adventure games, anything with an inventory.
  • Use both for the best of both worlds: PlayerPrefs for settings (they should persist even if the player deletes their save file) and JSON for game state. This is the approach most shipped games use.

Important: Unity's built-in JsonUtility does not support Dictionary types or polymorphic lists. If you need those, use Newtonsoft JSON (Json.NET), which Unity officially supports via the com.unity.nuget.newtonsoft-json package.

Building a Complete Save System

A production save system needs more than just serialization. It needs save versioning (so you can migrate old saves when you update the game), auto-save triggers, save slot management, and corruption protection.

C#
using System.IO;
using UnityEngine;

public class SaveSystem : MonoBehaviour
{
    public static SaveSystem Instance { get; private set; }

    [SerializeField] private float autoSaveInterval = 300f; // 5 minutes
    private float autoSaveTimer;
    private const int CURRENT_SAVE_VERSION = 2;

    void Awake()
    {
        if (Instance != null) { Destroy(gameObject); return; }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    void Update()
    {
        autoSaveTimer += Time.deltaTime;
        if (autoSaveTimer >= autoSaveInterval)
        {
            autoSaveTimer = 0f;
            SaveToSlot(0); // auto-save to slot 0
        }
    }

    public void SaveToSlot(int slot)
    {
        GameSaveData data = GatherSaveData();
        data.saveVersion = CURRENT_SAVE_VERSION;

        string json = JsonUtility.ToJson(data, true);
        string path = GetSlotPath(slot);

        // Write to temp file first, then rename (corruption protection)
        string tempPath = path + ".tmp";
        File.WriteAllText(tempPath, json);
        if (File.Exists(path)) File.Delete(path);
        File.Move(tempPath, path);
    }

    public GameSaveData LoadFromSlot(int slot)
    {
        string path = GetSlotPath(slot);
        if (!File.Exists(path)) return null;

        string json = File.ReadAllText(path);
        GameSaveData data = JsonUtility.FromJson<GameSaveData>(json);

        // Migrate old save versions
        if (data.saveVersion < CURRENT_SAVE_VERSION)
            data = MigrateSave(data);

        return data;
    }

    private GameSaveData MigrateSave(GameSaveData data)
    {
        // Version 1 -> 2: added unlockedAbilities field
        if (data.saveVersion < 2)
        {
            if (data.unlockedAbilities == null)
                data.unlockedAbilities = new System.Collections.Generic.List<string>();
            data.saveVersion = 2;
        }
        return data;
    }

    private GameSaveData GatherSaveData()
    {
        // Collect state from all game systems
        return new GameSaveData();
    }

    private string GetSlotPath(int slot)
        => Path.Combine(Application.persistentDataPath, $"save_slot_{slot}.json");
}

Key features in this implementation: temp file writes prevent corruption if the game crashes mid-save, save versioning lets you add new fields without breaking old saves, and auto-save runs on a configurable interval. Our JSON Save Utility script provides this foundation in a drop-in package.

Download Ready-Made Solutions

If you want to skip the boilerplate and get straight to building your game, we offer three levels of save system support:

  • PlayerPrefs Save Helper — Type-safe wrapper with key prefixing, defaults, and bulk reset. Perfect for settings and simple data.
  • JSON Save Utility — Complete JSON save/load with multiple slots, auto-save, versioning, and corruption protection.
  • Save System bundle — Both scripts plus a SaveableEntity component that lets any MonoBehaviour opt into saving with a simple interface. Includes a save slot UI selector, cloud save hooks, and migration utilities.

All scripts are MIT-licensed, zero-dependency, and work with Unity 2022.3 LTS through Unity 6. Drop them in, configure via the Inspector, and focus on building your game instead of your save infrastructure.

Pair the save system with an Inventory System for persistent item tracking, or use our Health System to save and restore player health across sessions.