Core Concepts Must read
The parts
Three pieces do the work:
- The persistent object: your data, a
PersistentScriptableObject. - The persistence manager: where and how that data is stored (the inspector dropdown).
- The serializer: how the data is turned into text to store, and back.
Around those, three operations (load, save and clear) move data between the object and its storage.
The persistent object
Your data is a class inheriting PersistentScriptableObject, and its serialized
fields are your data. You read and write them through the object at runtime, exactly like a
normal ScriptableObject; those same fields are what gets saved and loaded.
[CreateAssetMenu(menuName = "Player Data")]
public class PlayerData : PersistentScriptableObject
{
public int gold;
public int level = 1;
}
Whatever the fields hold outside play mode is the object's default values: the state a brand-new player starts with, and what the data falls back to when there is no save.
A persistent object is a single shared asset, so everything that references it reads and writes the same live data. When you want more (multiple save slots, save versioning, reacting to load and save) you opt in through one of the small optional interfaces.
The persistence manager
At the top of every persistent object's inspector is a Persistence Manager dropdown. The manager decides where and how the data is stored: a local file, Player Prefs, memory, a server. Pick one, or pick None to opt the object out of persistence entirely. The manager you pick is created automatically as a sub-asset of the persistent object, you never make or wire it yourself.
Because the manager is data on the asset and not in your scripts, you can switch managers at any time without changing a line of code. Its inspector has three sections:
- Parameters: the manager's settings, the serializer, whether it loads and saves automatically, how often, timeouts, and manager-specific options.
- Actions: buttons to Load, Save or Clear by hand while you play.
- Status: the object's live state (ready or not, the last operation's result), and a running log of every load, save and clear as it happens.
See Local Saving for the managers you can choose.
The serializer
Storage works in text, so the manager runs the object through a serializer to turn it into a string on save, and back into the object on load. The serializer is a field on the manager, so you choose it in the same inspector.
By default that is Unity JSON, which serializes your fields exactly the way Unity already does (the same rules as the inspector and prefabs), so it just works for typical data with no setup. The save it produces is plain, readable JSON when no compression or encryption is applied.
You rarely need to think about it, but it is a real extension point: swap in your own format
by writing a Serializer (see Extending).
Operations: load, save, clear
Three operations move data between the object and its storage. Most of the time you call none of them, because the manager runs them for you. A manager is in scope while its object is loaded and in use by the running game, and out of scope while nothing uses the object. By default:
- It loads when it enters scope, at launch or whenever Unity loads the object, and retries automatically if that load fails.
- It saves at safe points: when the object is unloaded, when the application loses focus or is paused, and on a regular timer.
That is why the basic setup needs no save code. Every part of this can be tuned or turned
off per manager, in its inspector (AutoLoad, AutoSave and their
delay settings).
Load
Reads the stored data back into the object. It happens automatically when the object enters scope; call it yourself for a "load game" or "continue" flow.
Save
Writes the object's current data to storage. It happens automatically at the safe points above; call it yourself at your own checkpoints.
IsReady (see when the data is ready).
Clear
The destructive one: it deletes the saved data, resets the object to its defaults, and pauses
saving until the next load, so the just-cleared state is never written back. To reset the
object in memory without deleting the save, use ResetToDefaults() instead
(see Resetting & Restoring).
Driving it yourself
When you call an operation yourself, each of Load, Save and Clear comes in five shapes so it fits any calling style (shown here with Save):
- Fire and forget:
Save(). The default "don't think about it" call, kick it off and move on. It also drops straight onto a UI event, with no wrapper script. - Callback:
Save(result => ...). Runs your callback once the operation finishes, handing it the result. - Synchronous:
SaveSync(). Forces the operation to run inline and returns its final result. Not every manager can work inline (a server cannot); one that cannot reports it in the result rather than blocking the game. - Async:
await SaveAsync(). Await the result inside anasyncmethod. - Coroutine:
yield return SaveRoutine(). Yield it in a coroutine, then read itsResult.
PersistenceManager pm = playerData.PersistenceManager;
pm.Save(); // fire and forget
pm.Save(result => ShowSavedTick()); // callback when done
SaveResult syncResult = pm.SaveSync(); // forced synchronous, result inline
SaveResult asyncResult = await pm.SaveAsync(); // async
PersistenceOperation<SaveResult> op = pm.SaveRoutine(); // coroutine: create the handle,
yield return op; // yield return it,
SaveResult routineResult = op.Result; // then read its Result
Results
Every operation reports back a Result (LoadResult,
SaveResult or ClearResult). The simplest way to read it is its
flags:
IsSuccess: it worked.IsFailure: something genuinely went wrong (the storage was temporarily unavailable, or code threw). A retry might help.IsInvalid(load only): there was no usable save to read, missing or corrupt. This is the normal first run, not an error: the object keeps its current values and saving is allowed.IsIgnored: the operation decided not to run and changed nothing.Messagesays why (nothing loaded yet, no slot selected, and so on).IsCancelled: it started but was abandoned (timed out, superseded, or the manager left scope).IsBusy: a specialIsIgnoredcase, where a synchronous call was declined because asynchronous work was still running. Retry once it is done, or use the async path.
A result tells you how that one operation ended. To know whether the object is
currently holding its real data, read IsReady, covered next.
When the data is ready
Once a manager has loaded, its object holds real data and is safe to read and modify. That
moment is exposed as IsReady, but how much you need to care depends on your
persistence manager:
- Never think about it: Player Prefs and Session (Memory) load instantly and cannot fail, so the data is ready before your game code ever sees the object (for Player Prefs, as long as its Auto Load stays on, the default). Skip the checks entirely.
- Ready in practice: Prototype and Local File also load synchronously as the object comes into scope, so the data is normally just as ready, only without the hard guarantee: a file read can fail in rare cases (a locked file, an unset runtime secret; see Local File).
- Always gate: a remote manager (a server, Cloud Save) loads asynchronously and can wait on authentication or connectivity, so readiness is a real state your game must handle. See Cloud & Remote.
Touching the object before it is ready is never dangerous: it simply still holds its default values, and saving stays blocked until the real data lands, so an early read can never corrupt or overwrite a save. The only stake is your game acting on defaults as if they were the player's data.
Gate in one place, at the front door, not with checks sprinkled through the code: in your
boot flow or loading screen, await WhenReady (with await or
yield return), then proceed only if IsReady is true.
The rest of the game needs no checks. Changing the active
slot starts a fresh load, so gate a slot change the same
way.
await playerData.PersistenceManager.WhenReady;
if (playerData.PersistenceManager.IsReady)
StartGame(playerData);
IsReady itself is the underlying state, to check at any moment, and it doubles
as the save permission: saves are refused while it is false. It is
true once the object holds its data, and false before the first
load completes, while a
slot change reloads, after a clear until the next load, and
after a load that failed, so a save never overwrites data a retry might still
recover.
if (playerData.PersistenceManager.IsReady)
playerData.gold += 100;
Editor vs play mode
In the editor, when you stop playing, your persistent objects are restored to the default values you authored on the asset, not the runtime values from the play session.
This is deliberate. A ScriptableObject normally keeps runtime edits written into the asset in the editor (a well-known Unity quirk that does not happen in a build). Persistent Asset undoes that for you, so the defaults you set on the asset stay exactly as authored and version control does not fill with play-session noise. The save itself still exists on disk and still loads on the next launch, in the editor and in a build alike.
Acting on everything at once
Most code talks to one object's manager through
yourObject.PersistenceManager. When you want to act on every active manager
together (a global save on quit, a "new game" wipe), the static Persistence class
mirrors the same operations across all of them:
// Save every active manager at once
Persistence.SaveAll();
// Wait for every manager's load to settle
await Persistence.WhenAllReady;
Persistence also exposes global events (such as OnAfterAnySave and
ManagerAdded) for project-wide reactions like a "saving..." indicator.
And for the objects you marked always-global (a settings
singleton, a slot registry), the static GlobalPersistedData class reaches them
from anywhere by type, with no serialized reference, through
GlobalPersistedData.Find<GameSettings>().