Unity Multiple Save Slots: Save Files, Metadata and a Load Menu

Illustration: three glowing save slot cards with a disk icon

A single save file is fine until players want to start a second run, share the console with a sibling, or keep a save before a big choice. Unity save slots with multiple saves need a little more structure than one JSON file: a file per slot, small metadata for the slot menu, safe writes that can’t corrupt a save, and delete and overwrite flows. This guide builds a slot-based save manager and a load menu on top of plain JSON.

How Slot Saves Are Organised

Each slot gets two files in Application.persistentDataPath:

  • slot0.json: the full game state.
  • slot0.meta.json: a tiny summary for the menu (player name, level, play time, date, scene).

The slot menu reads only the small meta files, so it opens instantly even if full saves are large. Loading reads the full file only for the chosen slot. If you haven’t built a basic JSON save yet, start with the save and load system guide; this builds on it.

Save Data and Slot Metadata

using System;
using System.Collections.Generic;

[Serializable]
public class SaveData
{
    public int version = 1;
    public string scene;
    public float[] playerPosition = new float[3];
    public int health;
    public List<string> inventory = new List<string>();
    public List<string> completedQuests = new List<string>();
}

[Serializable]
public class SlotMeta
{
    public int slot;
    public string label;      // e.g. "Chapter 2 - Forest"
    public int playerLevel;
    public float playTimeSeconds;
    public string savedAtUtc; // ISO 8601 string, JsonUtility can't serialize DateTime
}

JsonUtility handles public fields and [Serializable] classes, but not dictionaries, DateTime or properties. Store dates as strings and positions as float arrays (or a serializable struct).

The SaveSlots Manager

using System;
using System.IO;
using UnityEngine;

public static class SaveSlots
{
    public const int SlotCount = 3;

    static string DataPath(int slot) => Path.Combine(Application.persistentDataPath, $"slot{slot}.json");
    static string MetaPath(int slot) => Path.Combine(Application.persistentDataPath, $"slot{slot}.meta.json");

    public static bool Exists(int slot) => File.Exists(DataPath(slot));

    public static void Save(int slot, SaveData data, SlotMeta meta)
    {
        meta.slot = slot;
        meta.savedAtUtc = DateTime.UtcNow.ToString("o");
        WriteSafely(DataPath(slot), JsonUtility.ToJson(data));
        WriteSafely(MetaPath(slot), JsonUtility.ToJson(meta));
    }

    public static SaveData Load(int slot)
    {
        if (!Exists(slot)) return null;
        try
        {
            return JsonUtility.FromJson<SaveData>(File.ReadAllText(DataPath(slot)));
        }
        catch (Exception e)
        {
            Debug.LogError($"Save slot {slot} is unreadable: {e.Message}");
            return null;
        }
    }

    public static SlotMeta LoadMeta(int slot)
    {
        string path = MetaPath(slot);
        if (!File.Exists(path)) return null;
        try { return JsonUtility.FromJson<SlotMeta>(File.ReadAllText(path)); }
        catch { return null; }
    }

    public static void Delete(int slot)
    {
        File.Delete(DataPath(slot));   // File.Delete doesn't throw if the file is missing
        File.Delete(MetaPath(slot));
    }

    static void WriteSafely(string path, string contents)
    {
        string temp = path + ".tmp";
        File.WriteAllText(temp, contents);
        if (File.Exists(path))
            File.Replace(temp, path, path + ".bak");
        else
            File.Move(temp, path);
    }
}

A static class is enough because it holds no state. Gather the game state into a SaveData in your game manager, then call SaveSlots.Save(currentSlot, data, meta). Remember which slot the current game came from, so “Save” writes back to the same one.

Writing Saves Safely

If the game crashes or the device loses power halfway through File.WriteAllText, the save is truncated and the player loses everything. WriteSafely avoids that: it writes the new content to a temporary file first, then swaps it in with File.Replace, which also keeps the previous version as .bak. If the main file is ever unreadable, try loading the .bak before giving up.

On WebGL, persistentDataPath is backed by IndexedDB and writes may need flushing; on consoles, use the platform’s save APIs. On desktop and mobile, this approach works as is. Note that File.Replace may not be supported on every platform; if it throws, fall back to delete-then-move.

using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class SlotButton : MonoBehaviour
{
    [SerializeField] int slot;
    [SerializeField] TMP_Text title;
    [SerializeField] TMP_Text details;
    [SerializeField] Button deleteButton;

    void OnEnable() => Refresh();

    public void Refresh()
    {
        var meta = SaveSlots.LoadMeta(slot);
        if (meta == null)
        {
            title.text = $"Slot {slot + 1} - Empty";
            details.text = "Start a new game";
            deleteButton.gameObject.SetActive(false);
            return;
        }

        var t = System.TimeSpan.FromSeconds(meta.playTimeSeconds);
        var saved = System.DateTime.Parse(meta.savedAtUtc, null, System.Globalization.DateTimeStyles.RoundtripKind).ToLocalTime();
        title.text = $"Slot {slot + 1} - {meta.label}";
        details.text = $"Level {meta.playerLevel}   {(int)t.TotalHours}h {t.Minutes:00}m   {saved:g}";
        deleteButton.gameObject.SetActive(true);
    }

    public void OnClick()
    {
        if (SaveSlots.Exists(slot)) GameSession.LoadFromSlot(slot);
        else GameSession.NewGameInSlot(slot);
    }

    public void OnDelete()
    {
        // show a confirm dialog first in a real game
        SaveSlots.Delete(slot);
        Refresh();
    }
}

GameSession is your own class that remembers the active slot, loads the saved scene and applies the data. Always confirm before deleting or overwriting a slot. Players blame the game, not themselves, when a misclick wipes 20 hours. For scene loading with a loading screen, see the async scene loading guide.

Autosave and Quicksave Slots

Treat autosave as its own slot index (for example, 99) that isn’t shown among the manual slots, or show it at the top as “Autosave”. Rules that keep players happy:

  • Autosave at checkpoints, level transitions and after major events, never mid-combat.
  • Never let an autosave overwrite a manual slot.
  • Keep two rotating autosaves, so one bad autosave (saved just before death) isn’t the only option.

The checkpoint system guide pairs well with autosaving.

Versioning Save Files

Your SaveData will change as you develop. Keep the version field and upgrade old saves when loading:

static SaveData Migrate(SaveData d)
{
    if (d.version < 2)
    {
        // v2 added completedQuests; old saves have null
        d.completedQuests ??= new System.Collections.Generic.List<string>();
        d.version = 2;
    }
    return d;
}

JsonUtility leaves missing fields at their default values, so new fields are usually safe; renamed or removed fields are what need migration code.

🔧 Save system assets

If you need encryption, cloud sync or complex object graphs, a dedicated save asset can save weeks. See our recommended Asset Store tools.

Troubleshooting

Slot menu shows “Empty” for a slot that has a save

The meta file is missing or unreadable. Rebuild meta from the full save, or save again.

Save file is empty or truncated

The game was closed mid-write. Use the temp-file and replace approach, and load the .bak as a fallback.

Dates show in the wrong time zone

Store UTC with the “o” format and convert with ToLocalTime when displaying.

Lists load as empty

The list’s element type isn’t serializable, or the list is a property. Use public fields of serializable types.

Where are the files?

Log Application.persistentDataPath. On Windows it’s under AppData\LocalLow\CompanyName\ProductName.

FAQ

Can I use PlayerPrefs for save slots?

For tiny data only. PlayerPrefs isn’t meant for game state and is awkward to manage per slot.

Should I encrypt saves?

For single-player games, usually not. Encryption mostly annoys modders. Use a checksum if you want to detect tampering.

How many slots should I offer?

Three is a classic default. Unlimited saves with a scrolling list suit long RPGs.

Can I add a screenshot to each slot?

Yes. Capture a small texture, EncodeToPNG, and save it next to the meta file.

Should saving run on a background thread?

Serialization must happen on the main thread with JsonUtility, but writing the string to disk can be done with async file APIs.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *