What a Simple Save System Actually Needs to Do
Before writing any code, clarify what your save system should handle.
- Scope β What data should you remember? Levels, health, inventory, settings?
- Frequency β When do you save? On checkpoints, on quit, after every level?
- Format β How do you store the data? JSON, binary, PlayerPrefs, or custom?
- Safety β What happens if saving fails or the data is corrupted?
For a beginner-friendly save system, aim for:
- A single SaveData model that describes all the data you care about
- A SaveManager that only knows how to read and write that model
- Game systems that push and pull values from the SaveManager, not from disk directly
You can always refactor the storage layer later (for example, to cloud saves) as long as the interface to the rest of your game is clean.
Choosing a Storage Method in Unity
Unity gives you several options for persisting data:
PlayerPrefsβ Simple keyβvalue store (fine for a few settings, not ideal for complex state)- JSON to file β Human-readable, easy to debug, good for most indie games
- Binary formats β Smaller and harder to tamper with, but harder to debug
For most 2D or 3D indie projects, JSON files stored in Application.persistentDataPath are a sweet spot:
- Cross-platform
- Easy to inspect during development
- Flexible enough to store complex nested objects
The rest of this tutorial assumes you are using JSON + files.
Step 1 β Define Your Save Data Model
Create a SaveData class that captures exactly what you want to save. Keep it focused; you can add fields later as needed.
Examples of data to include:
- Current level or scene
- Player position and health
- Unlocked abilities or upgrades
- Simple inventory (counts, flags)
In C#:
[System.Serializable]
public class SaveData
{
public string currentScene;
public float playerPositionX;
public float playerPositionY;
public float playerPositionZ;
public int playerHealth;
public int coins;
public bool[] unlockedAbilities;
}
The [System.Serializable] attribute allows Unityβs JsonUtility to serialize the class.
Step 2 β Create a Save Manager
Next, create a central SaveManager responsible for:
- Knowing where the save file lives
- Converting
SaveDatato and from JSON - Providing simple
SaveandLoadmethods for the rest of your game
You can implement it as a singleton-style component or as a static class. For many small games, a DontDestroyOnLoad singleton is straightforward.
using System.IO;
using UnityEngine;
public class SaveManager : MonoBehaviour
{
public static SaveManager Instance { get; private set; }
private string saveFilePath;
public SaveData CurrentSave { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
saveFilePath = Path.Combine(Application.persistentDataPath, "save.json");
LoadOnStartup();
}
private void LoadOnStartup()
{
if (File.Exists(saveFilePath))
{
var json = File.ReadAllText(saveFilePath);
CurrentSave = JsonUtility.FromJson<SaveData>(json);
}
else
{
CurrentSave = new SaveData();
}
}
public void SaveGame()
{
var json = JsonUtility.ToJson(CurrentSave, prettyPrint: true);
File.WriteAllText(saveFilePath, json);
}
}
This gives you:
- A single place to read and write your save file
- A
CurrentSaveobject you can update from other systems
Step 3 β Hook Player and Game Systems into the Save Data
Now you need to connect your actual game data to the SaveData model.
Common pattern:
- On save β Read values from your player, inventory, and world, then copy them into
CurrentSavebefore writing to disk. - On load β Read values from
CurrentSaveand apply them back to your player, inventory, and world.
Example: Saving and loading player state
using UnityEngine;
using UnityEngine.SceneManagement;
public class PlayerSaveHandler : MonoBehaviour
{
private void Start()
{
LoadPlayerState();
}
public void SavePlayerState()
{
var save = SaveManager.Instance.CurrentSave;
save.currentScene = SceneManager.GetActiveScene().name;
var position = transform.position;
save.playerPositionX = position.x;
save.playerPositionY = position.y;
save.playerPositionZ = position.z;
// Example health and coins
var health = GetComponent<PlayerHealth>();
var wallet = GetComponent<PlayerWallet>();
save.playerHealth = health.CurrentHealth;
save.coins = wallet.Coins;
SaveManager.Instance.SaveGame();
}
private void LoadPlayerState()
{
var save = SaveManager.Instance.CurrentSave;
if (!string.IsNullOrEmpty(save.currentScene) &&
save.currentScene == SceneManager.GetActiveScene().name)
{
transform.position = new Vector3(
save.playerPositionX,
save.playerPositionY,
save.playerPositionZ
);
var health = GetComponent<PlayerHealth>();
var wallet = GetComponent<PlayerWallet>();
if (save.playerHealth > 0)
{
health.SetCurrentHealth(save.playerHealth);
}
wallet.SetCoins(save.coins);
}
}
}
From UI buttons or triggers, you can now call SavePlayerState() to persist progress.
Step 4 β Decide When to Save
Saving too often can hurt performance or create weird edge cases; saving too rarely risks player frustration.
Common strategies:
- Manual save button β Player presses a button in the pause menu
- Checkpoint triggers β Save when the player reaches a safe area
- On scene change β Save when loading a new level
- On quit β Save when the player exits to main menu or closes the game
For a simple beginner project, combine:
- A Save button in the pause menu
- An auto-save on scene change
Example: Auto-save on scene change:
public class AutoSaveOnSceneChange : MonoBehaviour
{
private void OnEnable()
{
UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged;
}
private void OnDisable()
{
UnityEngine.SceneManagement.SceneManager.activeSceneChanged -= OnSceneChanged;
}
private void OnSceneChanged(Scene oldScene, Scene newScene)
{
if (SaveManager.Instance != null)
{
// You might call into systems that know how to populate CurrentSave
FindObjectOfType<PlayerSaveHandler>()?.SavePlayerState();
}
}
}
Step 5 β Handle Corrupted or Missing Save Files
Even simple games should fail gracefully if something goes wrong with the save file.
Add basic checks:
- If the save file is missing, create a fresh SaveData.
- If deserialization fails, catch the exception and reset to a safe default instead of crashing.
Extend your LoadOnStartup method:
private void LoadOnStartup()
{
if (!File.Exists(saveFilePath))
{
CurrentSave = new SaveData();
return;
}
try
{
var json = File.ReadAllText(saveFilePath);
CurrentSave = JsonUtility.FromJson<SaveData>(json);
if (CurrentSave == null)
{
throw new System.Exception("Deserialized SaveData is null");
}
}
catch (System.Exception e)
{
Debug.LogWarning($"Failed to load save file, starting fresh. Reason: {e.Message}");
CurrentSave = new SaveData();
}
}
This way, a broken file turns into a new game rather than a crash or a confusing error.
Step 6 β Evolving Your Save System Safely
As your game grows, you will add new fields to SaveData. A few habits keep this safe:
- Provide defaults β Assume older saves may not have new fields set
- Version your data β Add a simple
int versionfield toSaveData - Write migration code β When
versionis older, fill in new values with sensible defaults
Example:
[System.Serializable]
public class SaveData
{
public int version = 1;
// existing fields...
}
When you change the structure, bump the version and handle older versions in a small migration method that runs after load.
Common Mistakes to Avoid
- Saving every frame β This tanks performance and bloats disks; save on meaningful events instead.
- Letting every script write to disk β Keep disk I/O inside a dedicated
SaveManager. - Using PlayerPrefs for everything β Fine for a few settings, fragile for full game state.
- Not testing load-only flows β Always test starting the game from a saved file without going through a βnew gameβ path first.
FAQ
Where should I store save files in Unity?
Use Application.persistentDataPath. Unity maps this to the right location on each platform.
Can I use JSON for large or complex games?
Yes, many commercial games do. For very large data you may eventually move to binary formats or chunked saves, but JSON is fine for most indie scopes.
How do I let players have multiple save slots?
Use one file per slot, for example save_slot_1.json, save_slot_2.json, and track the active slot in a small settings file or in memory.
How do I handle secure or tamper-proof saves?
For single-player offline games, basic integrity checks or light obfuscation are usually enough. For competitive or online games, enforce truth on the server side instead of trusting local saves.
Wrap-Up
A simple save and load system in Unity is just a clear data model, a small manager that reads and writes JSON, and a few well-chosen moments to trigger saves. Start small with a single SaveData class and SaveManager, wire your player and core systems into it, and then evolve the model as your game grows. If you keep your interface clean and your responsibilities separated, you will be able to add features like multiple save slots or cloud sync later without rewriting everything from scratch.