Session lifecycle

Each hook runs at a fixed point in the asset's session, from the moment Unity loads the asset until it unloads it. Use this order to decide where your setup and cleanup code goes.

package step your code runs done or ready stopped or not ready note Scope in Unity loads the asset: a scene uses it, or it is Always Global Your authored values are kept for ResetToDefaults() 1 Slot starts on Slots.GlobalSlot (slotted assets) 2 The asset is told it is entering scope OnBeforeScopeIn(slot, true) 3 The automatic load starts (AutoLoad on) OnBeforeLoad 4 A custom manager is told OnEnteringScope() 5 The load finishes: IsReady true OnReadinessChanged, OnAfterLoad 6 In scope play Automatic saves: RegularSaveDelay timer, focus loss, pause OnBeforeSave, OnAfterSave 7 Your own Load, Save and Clear calls 8 A failed automatic load retries after AutoLoadRetryDelay 9 The slot changes refer to Slot change 10 Scope out Unity unloads the asset during play A custom manager is told OnLeavingScope() 11 The final automatic save (AutoSave on) OnBeforeSave, OnAfterSave 12 The asset is told it left scope OnAfterScopeOut(slot, true) 13 IsReady false OnReadinessChanged 14 In the Editor: the asset shows its authored values again 15 Quit the player quits, or Play mode ends Saves still running finish before the exit refer to Quitting mid-save 16

Read or change the data only once IsReady is true, at step 6. OnEnteringScope() at step 5 does not wait for the load, so with an asynchronous manager the data is usually not there yet.

For what brings an asset into scope, refer to Scope and global assets. For the hooks, refer to Optional hooks.

When a call is skipped

A load, save or clear that returns Ignored did nothing on purpose. Find the reason here before looking for a bug.

Any operation During quit, anything but an automatic save Ignored A slotted asset with no slot set Ignored Started from an OnBefore handler, or the OnAfter handler of a synchronous call Ignored, IsBusy A SaveSync, LoadSync or ClearSync while an asynchronous operation runs Ignored, IsBusy A load is ignored when AutoLoad is off (automatic load) It already loaded or saved (automatic) BlockLoad returns true A save is ignored when It is not ready, and no load is running Downgrade returned false An Upgrade threw It is not dirty (IDirtyTracked) BlockSave returns true A clear is ignored when BlockClear returns true

In the Editor, the manager's log in the Inspector names the reason for every ignored call. To get past an IsBusy result:

  • From a synchronous call, call Save, Load or Clear instead: they queue behind the running operation.
  • From an event handler, or from OnBeforeScopeIn or OnReadinessChanged during a slot change, start the call after the handler returns. The asynchronous forms are refused there too.
  • A LoadSync that has to read an import copy only readable asynchronously also answers IsBusy, and the load goes on in the background: await WhenReady.

Overlapping operations

You can call Load, Save and Clear at any time, even while another operation on the same asset is still running. Nothing overlaps, and your calls take effect in the order you make them: each one is answered as this table shows.

you ask for already running Load Save Clear Load Shares it same slot: you get the same result Waits saves once the load has finished Clear wins the load is asked to stop, anything waiting is dropped, then the slot is cleared Save Waits loads what was just saved Merged one extra save covers every request Clear wins the save is asked to stop, anything waiting is dropped, then the slot is cleared Clear Waits loads the emptied slot Dropped saving pauses until the next load Replaced the running clear is asked to stop, anything waiting is dropped, then this one runs your request is served it waits its turn it is dropped

For the ordering guarantee and its limits, refer to Concurrency and threading.

What a load leaves you with

Every load ends in one of three results, and the result tells you what the asset now holds.

package step check your code runs done or ready stopped or not ready The load starts OnBeforeLoad(source) Read the save Slot change, clear or unload meanwhile? yes The result is dropped: Cancelled no a save was found no save the read failed or timed out Read it into the asset Readable? yes no Try a backup copy, if any Backup readable? yes no: counts as no save Import copies on this manager? yes no Try them, newest first One holds a save? no yes The asset keeps its values, then DynamicInitialize runs The asset is not touched a copy can't be read Success: IsReady true data from the save or a copy Invalid: IsReady true values as they were Failure: IsReady unchanged not ready stays not ready The load finishes OnAfterLoad(source, result)

An unreadable save is treated as no save because it never becomes readable again, so blocking saves would strand the player for good. A failed read may succeed on a retry, so the asset is not made ready over a save it could not reach. For what to show the player in each case, refer to When a save doesn't load.

Slot change

Changing the slot saves the slot you leave and loads the one you enter, in one call.

package step your code runs done or ready stopped or not ready note Slot change from slot A to slot B Slot or Slots.GlobalSlot is set to B 1 Slot A is saved (AutoSave on) OnBeforeSave, OnAfterSave 2 The asset is told it left slot A OnAfterScopeOut("A", false) 3 Anything still waiting for slot A is dropped a load of A still running is cancelled 4 IsReady false OnReadinessChanged 5 The asset is told it enters slot B OnBeforeScopeIn("B", false) 6 The asset returns to its authored values A's data never reaches B 7 The automatic load of B starts (AutoLoad on) OnBeforeLoad 8 The load finishes: IsReady true OnReadinessChanged, OnAfterLoad 9

The reset at step 7 happens even with AutoLoad off, so the old slot's values never show while the new one loads. For setting up slots, refer to Set up save slots.

Quitting mid-save

A player who quits while a save is running does not lose it: the game waits for the save to finish, up to the limits you set.

package step your code runs done or ready stopped or not ready note Quitting mid-save there is a save or clear to wait for The exit waits OnShutdownDrainStarted 1 Every asset in scope makes its final save (AutoSave on) OnBeforeSave, OnAfterSave 2 Each save gets up to its SaveTimeout 3 Other calls are ignored until the exit 4 The game quits, or Play mode ends OnShutdownDrainCompleted 5 Other outcomes Nothing to wait for: the game exits at once Max Drain Time or Force quit: what is left is cancelled a warning says data may be lost

Use the two events to show a "saving..." message. A game killed by the operating system gets no wait, so only saves that already finished are kept. For the settings, refer to Save on quit.

Versioning and migration

Two tools keep a player's save working across your releases. IUpgradable handles a change to the data, such as a renamed or merged field. An import copy handles a change to where or how the save is stored, such as a new file name or a new serializer.

package step check your code runs done or ready A player opens a new build, and the asset loads Does the current storage hold a usable save? no The old setup's save is moved over from its import copy (below) yes The save is read into the asset Written at an older SaveVersion? yes Upgrade(N, oldData) Written by a newer build? yes Downgrade(N) Two devices' saves in conflict? yes IMergeable merges them The asset is told the save is in OnAfterDeserialize() IsReady true OnReadinessChanged, OnAfterLoad

What the version check decides

Raise SaveVersion whenever a release changes the data in a way the serializer cannot follow on its own.

check your code runs done or ready stopped or not ready note Which version was the save written at? older the same newer Upgrade(N, oldData) returns The next save writes the new format throws Saving is blocked until a fixed build Nothing to do Downgrade(N) false Saving is blocked, the newer save stays intact true Saves in this format; fields it does not know are lost Upgrade only runs on a save the serializer can still read: add fields rather than rename or retype them.

For a worked example, refer to Version your data. Reading oldData needs a serializer that supports it: refer to Choose a serializer.

How an import copy moves a save

An import copy moves each player's save to the new setup once, the first time the new setup finds nothing.

package step your code runs done or ready stopped or not ready note In the Editor you make a breaking change to a locked manager You can keep the old setup as an [Import N] copy 1 At runtime the new setup's first load finds no usable save The copies are tried newest first, each with its old settings 2 The first copy holding a save is read into the asset Upgrade(N, oldData) if older 3 A custom manager is told OnImportingFrom(source, slot) 4 The save is written with the new setup 5 The old copy's save is deleted 6 Moved once, for good OnAfterLoad reports Success 7 Other outcomes No copy holds a save: the asset keeps its values DynamicInitialize() A copy can't be read: the load fails, the old save is kept the next load tries again

A storage change and a data change can ship in the same release. For the Editor side, refer to Lock and migrate saves.

Module dependencies

Use this map to see what a module needs before you delete it, and which package turns an optional one on.

BUILT-INS NO CODE SCENE OBJECTS Essentials R E EM PM Newtonsoft R EM needs Newtonsoft Json Odin R EM needs Odin Serializer MemoryPack R EM needs MemoryPack Steam R E EM needs Steamworks.NET Ugs R E EM needs Cloud Save Shared R E EM PM the base the six above build on all six Prefs R E EM PM Demo R E EM needs uGUI Input R E EM needs Input System Localization R E EM needs Localization Persistence.UGUI E EM needs uGUI Variables R E EM PM Persistence R E EM PM SceneObjects.UGUI R E EM needs uGUI SceneObjects R E EM PM Core R E EM PM every assembly references Core R runtime E .Editor EM .EditModeTests PM .PlayModeTests needs X: compiles only with that package dashed: sample code, nothing references it

An assembly is named PersistentAsset.<Module>.<SubModule>, such as PersistentAsset.BuiltIns.Essentials, plus the suffix of its kind. Test assemblies never reach a player build.

Steam also builds only for the Editor, Windows, macOS, Linux and Android. For installing the optional modules, refer to Optional modules.