Mark what to keep with [Saved], then call SaveSystem.Save() and SaveSystem.Load(). Everything else is taken care of.
[Component]
class Player : Component
{
[Saved] public int32 coins;
[Saved] public Vector3 checkpoint;
[Saved] public List<int32> unlockedLevels = new .() ~ delete _;
[Saved] public float[4] bestLaps;
[Saved("hp")] public float health; // stored under "hp": rename the field later without breaking saves
public float speed; // not saved
}
SaveSystem.Save(); // the "default" slot
SaveSystem.Save("Castle"); // or any name a player types
SaveSystem.Load("Castle");What can be saved
| Kind | Types |
|---|---|
| Numbers and text | bool, every integer size, float, double, char8/16/32, String, enums |
| Engine types | Vector2, Vector3, Vector4, Int3, Quaternion, Color, Matrix4 |
| Scene references | GameObject, Transform, any component: saved as which object it is, and pointed at the right one again on loading |
| Your own types | structs and classes whose fields are marked [Saved] (a derived class keeps its base’s) |
| Collections | T[], T[,], T[,,], T[,,,], sized arrays T[N], List, Queue, HashSet, LinkedList, Dictionary, nested any way (List<Dictionary<String, Vector3[]>>) |
The code that saves and loads each field is written by the compiler from the field’s type when the game is built: there is no reflection while saving, and a field of a type that cannot be saved is a compile error that names it. Collections of objects own them: loading deletes what a collection held and makes new objects (references to scene objects excepted).
A save keeps values by name, not by position, so saves from older versions of a game load: a field the save does not have keeps its default, and a saved value with no field left for it is skipped.
Saving and loading a scene
Save gathers every [Saved] field of every component in the scene. Load:
- changes to the saved scene, if another one is playing, and carries on once it has started;
- takes away scene objects that had been destroyed when the game was saved;
- makes again the objects that were made while playing, from a prefab file or a registered network prefab, if they have something saved (a
[Saved]field or a Saved Transform): dropped items, placed blocks; - puts every field back, and points references at the objects as they are now;
- calls
OnLoaded()on each component it loaded, to rebuild what it makes from its fields.OnSaving()is called just before a save reads them.
Objects are known by their id in the scene file, and objects made while playing by the id they had and their place under it, so the same scene and prefabs always match up. Objects that belong to a connected player (their Network Identity has an owner) are left out: they are the player’s, not the save’s.
In a network session only the server saves and loads; what it loads reaches the clients through the usual networking.
Position and movement: Saved Transform
[Saved] cannot go on the engine’s Transform, so add a Saved Transform component to objects whose place should be kept. Choose what to keep, for less to store: position X, Y and Z separately, rotation, scale. With a Rigidbody or Character Controller it also keeps velocity and spin, so an object that was falling or rolling when the game was saved carries on when it is loaded. An object made while playing with a Saved Transform is made again where it was.
Slots and storage
Slots are names: “default”, “slot 2”, “My Castle: v2”. SaveSystem.Exists(name), Delete(name) and ListSlots(list) manage them.
SaveSystem.Storage decides where saves go. The default, FileSaveStorage, keeps one file per slot in GamePaths.PlayerData + "saves/", writing to a temporary file first so a crash never leaves half a save, and turning any name into a file name it can turn back. MemorySaveStorage keeps them in memory. For a server, a cloud service or a platform’s own saves, derive from SaveStorage:
class ServerStorage : SaveStorage
{
public override void Write(SaveTask task)
{
// Send task.data (bytes) under task.slot, then, from any thread:
task.Finish(true); // or task.Fail("why")
}
public override void Read(SaveTask task)
{
// Fetch into task.data, then:
task.Finish(true, true); // succeeded, and the slot had a save
}
}
SaveSystem.Storage = new ServerStorage(); // in GameSetup.RegisterSave and Load return a SaveHandle to ask about later (Done, Succeeded, Found, Error(text)), and the SaveSystem.Saved and SaveSystem.Loaded events fire when they finish.
To keep saves your own way, SaveSystem.Capture() returns the scene’s saved state as a SaveNode tree and SaveSystem.Apply(node) puts one back. A tree turns into compact checksummed bytes (ToBytes, FromBytes: a changed or damaged save is refused) or readable JSON (ToText, FromText).
Loading at start
Tick Load Save At Start in Project Settings (and choose the slot), or set SaveSystem.LoadAtStart = true in GameSetup.Register. The save is put back as the first scene starts: after its objects are made and before any script’s Awake, so scripts only ever see loaded values. With a storage that answers later, the scene waits for it, up to SaveSystem.StartTimeout seconds, and starts with its defaults if it does not come.
The player’s folder: GamePaths
GamePaths.PlayerData is the folder for the player’s own files on every platform, ending in a slash:
| Platform | Folder |
|---|---|
| Windows | %APPDATA%\Tallow\<Game>\ |
| Linux | ~/.local/share/Tallow/<Game>/ |
| macOS | ~/Library/Application Support/Tallow/<Game>/ |
| Web | the browser’s storage for the page, kept between visits |
let path = scope $"{GamePaths.PlayerData}notes.txt";<Game> is the project’s name; company = ... in the build’s manifest replaces “Tallow”. The game’s own folder is for reading only.
Data assets
A data asset is a file of data every script shares: settings, a table of weapons, the levels in order. Write a class, make the file, fill it in, and read it from anywhere:
[DataAsset]
class GameSettings : DataAsset
{
public float maxFieldOfView = 95; // set in the Inspector: the same for every player
[Saved] public float volume = 0.8f; // each player's own
[Saved] public String name = new .("Player") ~ delete _;
}
let settings = DataAsset.Get<GameSettings>(); // loaded once, shared by every script
settings.volume = 0.5f;
settings.Save(); // keeps this player's [Saved] fields- Making one: Project panel, + > Data Asset, choose the class. Click the
.tdatafile to edit it in the Inspector; changes are written to the file at once. - Finding it:
DataAsset.Get<T>()returns the project’s asset of that class (the first file made from it);Get<T>(path)a particular one;DataAsset.FilesOf(type, list)all of them. A class with no file still works, with the values written in the code. - The player’s copy: fields marked
[Saved]are the player’s to change.Save()keeps them inGamePaths.PlayerData + "data/"(orDataAsset.Storage), and the nextGetputs them over the file’s values and callsOnLoaded().Revert()forgets them and goes back to the file. The Inspector’s Forget my saved copy does the same on your machine. - In the editor: stopping play and reloading scripts drop loaded assets, so the next Play reads the files afresh.
DataAsset.CacheVersionchanges when that happens, for code that keeps an asset.
Data assets are kept apart from SaveSystem saves on purpose: loading an old save does not undo a player’s settings.
Tests
This guide is docs/saving.md in the engine’s repository. Spotted a mistake? Open an issue.