The safest simple way to save player progress in Unity is to copy the required gameplay state into a small, versioned plain C# data object, serialize it to JSON, and write it inside Application.persistentDataPath. Keep the previous file as a backup, validate data when loading, and save at meaningful checkpoints plus mobile pause events. Do not serialize an entire scene, MonoBehaviour, or ScriptableObject graph and hope it survives future builds.
A save system is part of game design as much as engineering. Losing progress damages trust; saving the wrong moment can trap a player in a broken state; changing the data format carelessly can invalidate every existing installation. A modest system with clear boundaries is usually more dependable than a clever one.
Choose the right storage for each kind of data
| Data | Recommended home | Why |
|---|---|---|
| Audio volume, language, control preference | PlayerPrefs or the main save file | Small settings fit key-value storage |
| Unlocked levels, scores, inventory, tutorial state | Versioned save file | Related values can be validated and migrated together |
| Level definitions and balance values | ScriptableObject assets | These are developer-authored project data, not player progress |
| Passwords, tokens, valuable server authority | Secure platform or server-side system | Local files and PlayerPrefs are not trustworthy secrets |
Unity’s PlayerPrefs documentation says it stores strings, floats, and integers locally without encryption. It is convenient for preferences, but it is not a security layer and becomes awkward when many related progress values must change together.
For a deeper distinction between authored configuration and runtime state, see Blekol’s guide to ScriptableObject, MonoBehaviour, and plain C# data.
Save a data snapshot, not live Unity objects
Create a dedicated data-transfer class containing only the values needed to reconstruct progress. Use stable identifiers such as a level ID or item ID instead of scene object references. Unity’s JsonUtility documentation explains that it serializes supported fields through Unity’s serializer; references to Unity objects are recorded as in-memory instance IDs and are not suitable for a save that must load in another session.
Keep the object small and explicit. A puzzle game might save an unlocked level, a best result, and settings—not every cube, animation, and UI component currently in memory.
A practical JSON save system
using System;
using System.IO;
using UnityEngine;
[Serializable]
public sealed class SaveData
{
public int version = 1;
public int unlockedLevel = 1;
public int bestMoves;
public float musicVolume = 0.8f;
}
public static class SaveSystem
{
private const string FileName = "save.json";
private const string BackupName = "save.backup.json";
private static string SavePath =>
Path.Combine(Application.persistentDataPath, FileName);
private static string BackupPath =>
Path.Combine(Application.persistentDataPath, BackupName);
public static void Save(SaveData data)
{
string json = JsonUtility.ToJson(data, true);
string tempPath = SavePath + ".tmp";
File.WriteAllText(tempPath, json);
if (File.Exists(SavePath))
File.Copy(SavePath, BackupPath, true);
File.Copy(tempPath, SavePath, true);
File.Delete(tempPath);
}
public static SaveData Load()
{
return TryLoad(SavePath)
?? TryLoad(BackupPath)
?? new SaveData();
}
private static SaveData TryLoad(string path)
{
if (!File.Exists(path))
return null;
try
{
string json = File.ReadAllText(path);
SaveData data = JsonUtility.FromJson<SaveData>(json);
return data != null && data.version > 0
? data
: null;
}
catch (Exception exception)
{
Debug.LogWarning($"Could not load {path}: {exception.Message}");
return null;
}
}
}
This example writes JSON to a temporary file first, copies the last working save to a backup, and falls back to that backup if the primary file cannot be parsed. It is a useful baseline, not a complete anti-cheat or cloud-sync solution. If progress has financial value or affects multiplayer authority, validate it on a trusted server.
Why persistentDataPath matters
Application.persistentDataPath provides the platform-specific directory intended for data retained between runs. Unity documents different locations for Android, iOS, Windows, macOS, Linux, and Web builds, so avoid hard-coded paths and combine the directory with a filename using Path.Combine.
Unity also notes that mobile app updates continue to use the same persistent location when the Bundle Identifier remains unchanged. Treat that identifier as part of the save contract: changing it can make an updated build appear to have no previous data.
Version the format before you need migration
Add a version field on day one. When a later build adds currencies, renames a field, or replaces a level structure, load the old data and migrate it deliberately:
- Deserialize the known old format.
- Fill new fields with safe defaults.
- Translate retired identifiers to current ones.
- Validate ranges and required values.
- Save the upgraded format only after migration succeeds.
Never assume a missing field means corruption. With Unity serialization, a newly added field can receive its default value when reading older JSON. The version tells your code whether that default is expected and what conversion is needed.
Save at reliable moments
Do not write every frame. Save after meaningful, completed transactions: finishing a level, claiming a reward, changing an important setting, or returning to a menu. Also save when a mobile or standalone application pauses. Unity’s OnApplicationPause reference documents the notification sent when the application pauses or resumes after losing or regaining focus.
A practical MonoBehaviour can call SaveSystem.Save(currentData) when pauseStatus is true. Keep explicit gameplay checkpoints as the primary strategy; a quit callback should be a final opportunity, not the only opportunity.
Validate before applying loaded progress
Parsing valid JSON does not prove the values make sense. Before copying data into the game, check that:
- the version is recognized;
- level and item IDs still exist;
- counts and scores are within acceptable ranges;
- required collections are not null;
- the current scene can safely restore the saved state;
- a corrupted primary file falls back to the backup;
- a missing file creates clean first-run defaults.
Test the failures players will eventually encounter
Test a fresh install, a normal update, an old-format save, an empty file, malformed JSON, a missing backup, and repeated pause/resume cycles on real devices. Also verify that changing development bundle identifiers does not mislead the test. A save system is finished only when its recovery path has been exercised.
How this supports a mobile and VR game studio
In a mobile puzzle game such as Cubus for Android and iOS, a clean save snapshot can preserve level access, best results, and player preferences without coupling those values to scene objects. The same architecture can support VR settings and progression while each platform chooses its own lifecycle triggers.
Players rarely notice a save system when it works—and that is the point. Clear data ownership, backups, migration, and device testing quietly protect every session. Explore more practical production notes and Blekol’s mobile and VR games at Blekol Games.
Featured photo by Kasra Askari on Unsplash.


Leave a Reply
You must be logged in to post a comment.