SDK / Unity

Saves

ExperimentalUnity 6000.6+

Brain keeps each player's saves with their account, so their progress follows them to every device and sign-in. A player who reinstalls and signs in with Game Center, Play Games, Steam, Apple or Google gets their progress back, and a guest who links an account keeps theirs.

Turn saves on in brain/game.toml:

[saves]

How saves work

  • A save is a JSON object in a named slot. A game with one save uses the slot main; a game with save files names its own (1 to 16 characters of a-z, 0-9 and _).
  • At most 64 KiB per save, plus a summary of at most 1 KiB that a file-select screen shows.
  • The device first. A save writes the device at once and the account a few seconds later. A burst of saves goes as one. Offline or signed out, the game plays on and the save goes once it can.
  • Your format's version is schema, a number you raise whenever the save's shape changes. Brain never reads your data, but it refuses a write with an older schema than the stored save's, so an old build can't overwrite what a newer one wrote.
  • Nothing is overwritten silently. When two devices save the same slot from the same starting point, they conflict. The save with more progress wins, or your game merges the two itself.
  • Seasons follow your game's stage. Saves made while the game is in playtest don't carry over to launch: players start fresh.

Keep purchases out of saves. A save is the player's own claim, so what they bought comes from Brain (Purchases). The exception is a deliver product, which hands an amount to your save once.

Platform cloud saves

Brain is your game's cloud save. Don't also sync progress through Steam Cloud, iCloud or Play Games Saved Games: two clouds fight over one save, and those services join no account linking, export or deletion.

Set up, load and save

Unity has no Brain.Saves facade yet. The save commands are available one level down, on Brain.Services.Saves, and their results arrive as JSON on Brain.Output:

using System;
using Ase.Brain;

[Serializable] public class GameSave { public int level; public int xp; }
[Serializable] public class SaveSummary { public int level; }

GameSave save = new GameSave();
bool setUp;

void Start()
{
    Brain.Output += OnBrainOutput;
}

void Update()
{
    if (!Brain.Ready || setUp) return;
    // Ok means Brain took the command; Full and NotReady mean try again next frame.
    if (Brain.Services.Saves.Setup(3, false) != BrainStatus.Ok) return;
    Brain.Services.Saves.Load("main");
    setUp = true;
}

void SaveGame()
{
    Brain.Services.Saves.Put("main", save, new SaveSummary { level = save.level }, save.xp);
}

void OnBrainOutput(string json)
{
    // Load and List answers, and the Save events below, arrive here as JSON text.
}

Put takes the slot, the data, the summary and the save's progress. Data and summary are objects JsonUtility serializes, so its rules apply: [Serializable] classes and fields, no dictionaries.

Command Does
Setup(schema, merges) Your format's version, and whether your game merges conflicts itself
Load(slot) Reads a slot; the answer arrives on Brain.Output
Put(slot, data, summary, progress) Saves a slot
Erase(slot) Empties a slot
List() Lists the slots that hold a save
Flush() Sends waiting saves now, when the app pauses or quits
Migrated(slot, data) Hands back a save you migrated to the current schema
Merged(slot, data, summary, progress, baseRevision, mergedAt) Hands back your merge of two saves
MergeFailed(slot) Your game couldn't merge; the device keeps its copy

Save events on Brain.Output carry a Save object holding one of Changed, Merge, TooNew or Refused. BrainSaveDocument<T> reads a loaded {schema, data} document into your own type.

Migrations

Add one step per version, from each old version to the next. Steps run in order on every older save, so never delete one: a player may come back after many versions. A missing step is an error, and the save stays as it is.

Migrate the loaded data in your own code, then hand it back with Brain.Services.Saves.Migrated(slot, data).

Conflicts and merges

Two devices that saved the same slot from the same revision conflict. Without a merge, the save with more progress wins, and on a tie the newer one. Give a progress whenever saves can conflict, and merge yourself when losing either side's progress would hurt: take the max of each counter, the union of unlocks.

When two accounts merge (a guest links an account that already has saves), your merge also folds in the save the other account had.

Call Setup(schema, true). A Merge save event then hands you both saves; answer with Merged(...), or MergeFailed(slot) when you can't.

Saves that change under you

Another device's newer save, a merge, or an emptied slot (erased elsewhere, a new season, the account deleted) replaces your game's copy. Use the new one:

  • Unity: the Changed save event on Brain.Output.

When the player's save is from a newer version of your game, ask them to update. Nothing of that save is sent or overwritten until then:

  • Unity: the TooNew save event.