Saves
Talo Saves allow you to persist your game's state across multiple sessions. Each object in your scene can be saved and restored from one of your player's saves.
Check out the SavesDemo sample (included with the Unity package) for a full example showing choosing saves, managing saves and persisting save data across different scenes.
Loadables
Loadables are GameObjects that automatically have their data saved and loaded. To make a GameObject loadable, create a new MonoBehavior, extend the Loadable
class and finally add your new component onto your GameObject.
Once a loadable has entered the scene, its OnEnable()
function registers it with the saves manager. We need to register all the Loadables in the scene so that when we load our save, we can match the content in the save file with the structure of the scene.
Importantly, each Loadable must have a unique Id
so that Talo knows which node to load with which data.
Saved objects
Save files are collections of "saved objects". Here's what a typical saved object looks like:
{
id: "level1cube2",
name: "Level1.Cubes.Cube2"
data: [
{
key: "x",
type: "System.Single",
value: "0.4036766"
},
{
key: "y",
type: "System.Single",
value: "0.4561406"
},
{
key: "z",
type: "System.Single",
value: "0"
}
],
},
You can visualise players' save files as node graphs in the Talo dashboard. Just go to the player's profile, click Saves
and choose the save you want to view.
Saved objects must have a unique id
: this comes from the loadable and is used to match the saved object with the correct loadable. The name
in a saved object refers to the name of the GameObject and names of all of its parents.
The most interesting part is the data
which contains the fields we register in the loadable (explained below) as well as the original type
of the data. Notice that all values are serialised into strings. This is so that when the data is loaded, it can be easily converted back into its original type.
Data hydration
When saves are created, all the loadable GameObjects in your scene will be serialised into saved objects. Similarly, when a save is loaded, each saved object is paired up with a matching loadable. Once paired up, the loadable is hydrated with the latest data from the saved object. Hydrating the loadable calls the loadable's OnLoaded()
function with the data containing each value converted back to its original type.
Registering fields
In order to save and load data, we need to tell Talo what we want to save. Here's an example of what that would look like for the cube saved object above:
public class LoadableCube : Loadable
{
public override void RegisterFields()
{
RegisterField("x", transform.position.x);
RegisterField("y", transform.position.y);
RegisterField("z", transform.position.z);
RegisterField("r.x", transform.rotation.x);
RegisterField("r.y", transform.rotation.y);
RegisterField("r.z", transform.rotation.z);
RegisterField("s.x", transform.localScale.x);
RegisterField("s.y", transform.localScale.y);
RegisterField("s.z", transform.localScale.z);
}
}
As you can see, each field needs to be registered with the RegisterField()
function. The RegisterFields()
function is called just before data is serialised to ensure we have the latest data.
Loading data
As described above, when a saved object is paired up with a loadable, the loadable is hydrated with the data from the saved object. This calls the OnLoaded()
function of the loadable, where you can set the properties of the GameObject:
public override void OnLoaded(Dictionary<string, object> data)
{
transform.position = new Vector3(
(float)data["x"],
(float)data["y"],
(float)data["z"]
);
transform.rotation = Quaternion.Euler(
(float)data["r.x"],
(float)data["r.y"],
(float)data["r.z"]
);
transform.localScale = new Vector3(
(float)data["s.x"],
(float)data["s.y"],
(float)data["s.z"]
);
}
Destroyed loadables
If an object is registered and then is found to be destroyed (i.e. if Destroy(gameObject)
was used), a meta.destroyed
key is saved as the only field for that object.
You can handle destroyed objects using the HandleDestroyed
function which will automatically destroy an object if it has the meta.destroyed
key:
public override void OnLoaded(Dictionary<string, object> data)
{
if (HandleDestroyed(data)) return;
transform.position = new Vector3(
(float)data["x"],
(float)data["y"],
(float)data["z"]
);
}
Loading saves
You can load saves using Talo.Saves.GetSaves()
. Once your saves have been fetched, the Talo.Saves.OnSavesLoaded
event is invoked.
Saves can be accessed using Talo.Saves.All
, Talo.Saves.Latest
or Talo.Saves.Current
. To load a save, use Talo.Saves.ChooseSave()
and pass in the save you want to load.
Once your save has been chosen, the Talo.Saves.OnSaveChosen
event will be invoked. Internally, this causes the OnLoaded()
function in your Loadables to be called.
Finally, when all your registered loadables have called their OnLoaded()
, an Talo.Saves.OnSaveLoadingCompleted()
event is invoked which can be used to hide your loading screen.
Creating saves
To create a save, use Talo.Saves.CreateSave()
. Your new save will automatically become your chosen save (although the OnSaveChosen
event will not invoke). Internally, this causes the RegisterFields()
function in your Loadables to be called.
Offline copies of saves are created in the Application.persistentDataPath
within a saves.json
.
Updating saves
To update a save, use Talo.Saves.UpdateSave()
. You can optionally pass in a new name for your save.
Deleting saves
You can delete a save by passing in the save's ID to Talo.Saves.DeleteSave()
.
Offline saves & syncing
Talo automatically creates offline versions of saves. When network access is restored, Talo will attempt to resync the saves, preferring the most recently updated save if an online AND offline save exists.
Additionally, if a save is only available offline then it will be synced as soon as a network connection is available.
Unloading saves
You can unload a save using Talo.Saves.UnloadCurrentSave()
- this is useful when you want to completely reset the game.
This invokes the Talo.Saves.OnSaveUnloaded
event and clears any saved object data, essentially reverting every scene back to its original state. The previously unloaded save's data will be unaffected.