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.

Persistent Object your data Serializer data to text Persistence Manager where & how Storage disk / cloud save ▶ ◀ load clear ✕
The object, the serializer and the manager, connected by operations
With the defaults, all of this runs on its own: you read and write your fields, and the loads and saves happen for you. Manual control is there when your game calls for it.

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.

A persistent object follows Unity's normal ScriptableObject lifetime: Unity loads and unloads it based on what references it, just like any other asset. Persistent Asset does not keep it alive or change that flow; it simply hooks into those load and unload moments to restore and save the data.

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.

Persistence Manager dropdown
One object, one manager, swappable without touching code

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.

Manager logs in the inspector
Each manager logs its own activity during play

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.

A manager will not save until it has first loaded, or confirmed there is nothing to load. This is what stops a freshly launched game from overwriting an existing save with default values before it has read that save. Until the first load settles, both automatic and manual saves are held; if a load fails (the storage was temporarily unavailable), saving stays held so a retry can still recover the real data. Whether saving is currently held or allowed is reflected by 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 an async method.
  • Coroutine: yield return SaveRoutine(). Yield it in a coroutine, then read its Result.
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. Message says 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 special IsIgnored case, 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;
ready (true) not ready (false) true false scope in first load lands slot change load lands
When IsReady is true and false across a session

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.

So in the editor: press Play, change values, stop, and the asset shows your authored defaults again, while the save persists. Press Play again and the save loads back. That is the system working as intended, not a lost save.

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>().