Featured Articles:

Thursday, November 12, 2026

Saving and Loading In Unity (PlayerPrefs vs JSON vs Binary vs The ECS System, And Which One I Use)

I thought it was a good idea to write something about saving and loading in Unity. I get a lot of interest on the socials about what the best way is to save and load in Unity and how I implement it in my own games. There are actually several ways to do this, and it may sound scary at first, but it's actually really easy if you know what you're doing. Let's start with the basics and then gradually move on to a better way to do it. I'll also show you which method I use to save and load data in my projects. It really depends on what you want to save and what kind of game you're building. You can imagine that a simple game that only needs to store data like volume or resolution settings implements a different structure than a supermarket simulator that needs to store thousands of game objects and their locations.


Let's dive in!


What Exactly Is Saving And Loading In Unity And How Does It Work?

Everything in your game lives in memory while it runs, and memory is wiped the moment the game closes. Saving means writing the data you care about to disk. Loading means reading it back and putting it where it belongs.

To write data to disk, you first have to turn it into something storable, like text or bytes. That conversion is called serialization, the same word Unity uses when it saves your Inspector values. Reading it back is deserialization.

game data in memory  →  serialize  →  write to disk  →  read from disk  →  deserialize  →  game data again

Unity gives you three popular ways to do this, and they're built for very different jobs.


PlayerPrefs: The Quick And Tiny Option

PlayerPrefs is Unity's built-in key-value store. Probably the one you've used already in your (Udemy) tutorial games. (Yes, I'm looking at you, James Doyle!) You save a number or a string under a name, and you get it back later using that same name. It takes about three seconds to learn:


// Save
PlayerPrefs.SetFloat("musicVolume", 0.8f);
PlayerPrefs.SetInt("highScore", 1200);
PlayerPrefs.SetString("playerName", "Hero");
PlayerPrefs.Save();

// Load, with a default value if nothing was saved yet
float volume = PlayerPrefs.GetFloat("musicVolume", 1f);
int highScore = PlayerPrefs.GetInt("highScore", 0);
string playerName = PlayerPrefs.GetString("playerName", "Player");
  

Unity then writes PlayerPrefs to disk when the game quits, but calling PlayerPrefs.Save() after important changes protects you if the game crashes first. It's perfect for settings like volume, graphics quality, or the last level you played.

It's also limited. It only stores integers, floats, and strings, so there's no bool (use 0 and 1) and certainly no inventory. The data lives in a platform-specific spot, which on Windows is the registry, an odd home for save files. And anyone can edit it. Treat it as a notepad for settings, not a place for game progress.


PlayerPrefs Vs JSON: When Your Data Outgrows PlayerPrefs

The moment your save data has structure, like a position, a level, and a list of items, PlayerPrefs vs JSON stops being a close call. JSON wins hands down. JSON is just structured text, and Unity can turn almost any simple C# class into it with one call.

First, you describe what you want to save. The [System.Serializable] attribute is what lets Unity convert it:


[System.Serializable]
public class SaveData
{
    public string playerName;
    public int level;
    public float health;
    public Vector3 position;
    public int[] inventoryItemIds;
}
  

Then a small helper does the saving and loading. Files go in Application.persistentDataPath, a folder Unity gives you that's safe to write to on every platform:


using System.IO;
using UnityEngine;

public static class SaveSystem
{
    private static string SavePath => Path.Combine(Application.persistentDataPath, "save.json");

    public static void Save(SaveData data)
    {
        string json = JsonUtility.ToJson(data, true);
        File.WriteAllText(SavePath, json);
    }

    public static SaveData Load()
    {
        if (!File.Exists(SavePath))
        {
            return new SaveData();   // nothing saved yet, start fresh
        }

        string json = File.ReadAllText(SavePath);
        return JsonUtility.FromJson<SaveData>(json);
    }
}
  

Using it from anywhere in your game takes a few lines:


// Save
SaveData data = new SaveData();
data.playerName = "Hero";
data.level = 5;
data.health = 80f;
data.position = transform.position;
SaveSystem.Save(data);

// Load
SaveData loaded = SaveSystem.Load();
transform.position = loaded.position;
  

And the file it creates is plain, readable text. That's a gift when you're debugging, since you can open it and see exactly what got saved:

{
    "playerName": "Hero",
    "level": 5,
    "health": 80.0,
    "position": { "x": 1.0, "y": 0.0, "z": 3.0 },
    "inventoryItemIds": [1, 4, 7]
}


JsonUtility In Unity: What It Can And Can't Do

JsonUtility in Unity is fast and built-in, but it's deliberately simple. Know its limits before you design your save data:

  • Your class needs [System.Serializable], or nothing gets saved
  • It saves fields, not properties
  • It can't handle Dictionary
  • It can't save a bare array or list at the top level, so wrap it in a class (like SaveData above)
  • It doesn't remember derived types, so a list of base-class objects loses its subclasses

If you outgrow it, Unity offers the Newtonsoft JSON package through the Package Manager, which handles dictionaries and more complex data. For most solo projects, though, JsonUtility is plenty.


Binary: Smaller Files, Harder To Read

Binary saving writes raw bytes instead of text. Files come out smaller and slightly faster to read, but you can't open them and see what's inside. Here's the safe way to do it, using BinaryWriter and BinaryReader:


using System.IO;
using UnityEngine;

public static class BinarySaveSystem
{
    private static string SavePath => Path.Combine(Application.persistentDataPath, "save.dat");

    public static void Save(SaveData data)
    {
        using (BinaryWriter writer = new BinaryWriter(File.Open(SavePath, FileMode.Create)))
        {
            writer.Write(data.level);    // int
            writer.Write(data.health);   // float
        }
    }

    public static SaveData Load()
    {
        SaveData data = new SaveData();

        using (BinaryReader reader = new BinaryReader(File.Open(SavePath, FileMode.Open)))
        {
            data.level = reader.ReadInt32();
            data.health = reader.ReadSingle();
        }

        return data;
    }
}
  

The catch is that you must read values in exactly the order you wrote them. Add a field later, and old save files break. And a word of warning: older tutorials still teach BinaryFormatter, but Microsoft considers it insecure and has been phasing it out. Skip it.

Binary also isn't encryption. A determined player can still read it with a hex editor. For a single-player game, that's usually fine, since people editing their own save file hurt nobody but themselves.


How BIG Games Save Their Data

You might be wondering how bigger games handle saving and loading. Take a game like Cities: Skylines 2, for example. It has thousands of buildings, roads, vehicles, and potentially hundreds of thousands of citizens. Surely the developers aren't attaching a separate save system to every single object?

Nope! The trick is to save the game state, not the actual GameObjects.

Instead of saving every object individually, the game stores the data needed to recreate the world. Think about a building: you don't need to save the entire GameObject. You only need information such as its building type, position, rotation, and any other relevant data. When the game loads, it uses that information to recreate the building from its prefab.

Large games often take this concept even further by using architectures like Entity Component System (ECS). Rather than relying on traditional GameObjects and MonoBehaviours, ECS represents objects as entities with separate data components. This makes it possible to efficiently manage and process enormous amounts of game data.

You don't need ECS for your own projects, though. The underlying principle is exactly the same whether you're making a small indie game or a massive city simulator: separate your game data from the objects used to display it.


Let's See How This Works In Practice

Imagine you're making a supermarket simulator. Your store has 100 shelves, each containing different products. Instead of saving every GameObject, you create a data structure that describes the store.

First, we create two simple classes to hold our data:


[System.Serializable]
public class StoreSaveData
{
    public List<ShelfSaveData> shelves = new List<ShelfSaveData>();
    public int money;
}

[System.Serializable]
public class ShelfSaveData
{
    public int shelfID;
    public Vector3 position;
    public Quaternion rotation;
    public int productID;
    public int quantity;
}
  

Notice that we're not storing any GameObjects here. We're only storing information about them.

Next, we create a centralized SaveManager that collects this information and saves it:


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

public class SaveManager : MonoBehaviour
{
    private string SavePath =>
        Path.Combine(Application.persistentDataPath, "store.json");

    public void SaveGame()
    {
        StoreSaveData saveData = new StoreSaveData();

        // Find all shelves in the store
        Shelf[] shelves = FindObjectsByType<Shelf>(
            FindObjectsSortMode.None);

        foreach (Shelf shelf in shelves)
        {
            ShelfSaveData data = new ShelfSaveData();

            data.shelfID = shelf.ShelfID;
            data.position = shelf.transform.position;
            data.rotation = shelf.transform.rotation;
            data.productID = shelf.ProductID;
            data.quantity = shelf.Quantity;

            saveData.shelves.Add(data);
        }

        // Save other game information
        saveData.money = GameManager.Instance.Money;

        // Convert data to JSON and write it to disk
        string json = JsonUtility.ToJson(saveData, true);
        File.WriteAllText(SavePath, json);

        Debug.Log("Game saved!");
    }
}
  

Now comes the interesting part: loading the game. Instead of trying to restore the original GameObjects, we simply recreate them using their prefabs and the information stored in our save file.


public void LoadGame()
{
    if (!File.Exists(SavePath))
        return;

    // Read saved data
    string json = File.ReadAllText(SavePath);
    StoreSaveData saveData =
        JsonUtility.FromJson<StoreSaveData>(json);

    // Restore money
    GameManager.Instance.Money = saveData.money;

    // Recreate shelves
    foreach (ShelfSaveData data in saveData.shelves)
    {
        GameObject shelfObject = Instantiate(
            ShelfDatabase.Instance.GetShelfPrefab(data.shelfID),
            data.position,
            data.rotation
        );

        // Restore product information
        Shelf shelf = shelfObject.GetComponent<Shelf>();

        shelf.ProductID = data.productID;
        shelf.Quantity = data.quantity;
    }

    Debug.Log("Game loaded!");
}
  

Note: This is a simplified example. A production save system would also need to handle duplicate loading, missing prefabs, object relationships, save versions, and potentially much more.


So What's Actually Happening?

The important thing to understand is that the SaveManager doesn't need to know how every individual object works internally. It simply collects the information required to recreate the store.

You could extend this system to include employees, vehicles, furniture, customer information, and practically anything else in your game.

And that's the beauty of this approach: whether you have 10 objects or 10,000, the underlying principle remains the same. You're saving the information needed to rebuild your game world, not the GameObjects themselves.


Save File Gotchas

A few habits will save you real pain:

  • Add a version number. A public int version; field in your save data lets future updates recognize and upgrade old saves.
  • Handle a missing file. The first launch has no save, so check File.Exists like we did above.
  • Expect corrupt files. Wrap your load in a try/catch so a bad file starts a fresh game instead of crashing it.
  • Save ids, not scene references. Store "item 7" or a position, never a reference to a GameObject.
  • Save at safe moments. Checkpoints and menus, not every frame.


What Do I Use To Save Data In My Own Games?


If you're wondering what I use in my own games, I rely on Easy Save (ES3). It's a paid asset you can download from the Unity Asset Store that takes a lot of the manual work out of saving and loading (no, I'm not sponsored! I just really like this asset 😊). Instead of building and maintaining your own serialization and file-handling code, you can save and load many common C# data types, collections, and Unity objects with just a few calls. It also provides useful features such as encryption and compression, so you have more options as your save system grows. I find it especially handy when a project gets more complex, and I want to focus on making the game rather than maintaining save code. You can usually catch a sale on it.


So Which Approach Should You Actually Use?

It really depends, mainly on the size of your project and what you want to save:

  • Settings and tiny values like volume or quality → PlayerPrefs
  • Real game progress → JSON through JsonUtility in Unity, saved to Application.persistentDataPath
  • Large or performance-sensitive data → binary with BinaryWriter
  • Dictionaries or complex data → the Newtonsoft JSON package
  • Anything an old tutorial tells you to do with BinaryFormatter → don't
  • Not sure how to save and load in Unity for your game → ask "is this a setting or progress?"

Alright, that last question should settle most of the PlayerPrefs vs JSON debate. Settings go in PlayerPrefs, progress goes in a JSON file, and you'll rarely need anything fancier.


Wrapping Up

Saving data is mostly three steps: put your data in a simple class, turn it into text or bytes, and write it to Application.persistentDataPath. Learn to save and load in Unity with PlayerPrefs for settings and JSON for progress, add a version number from day one, and your players will never have to relive that tutorial level. 


 ~happy Coding!

No comments:

Post a Comment