|
| 1 | +# Basic concepts |
| 2 | + |
| 3 | +To understand how Safekeeper works, it's important to think about where the save |
| 4 | +data is stored. |
| 5 | + |
| 6 | +The following diagram illustrates the complete process of first loading the data |
| 7 | +and then saving it back: |
| 8 | + |
| 9 | +```mermaid |
| 10 | +sequenceDiagram |
| 11 | + participant P as Persistent Storage |
| 12 | + participant M as Memory |
| 13 | + participant G as Game State |
| 14 | + P->>M: Load(SaveMode.PersistentOnly) |
| 15 | + Note over P,M: Fetching the data |
| 16 | + M->>G: Load(SaveMode.MemoryOnly) |
| 17 | + G->>M: Save(SaveMode.MemoryOnly) |
| 18 | + M->>P: Save(SaveMode.PersistentOnly) |
| 19 | + Note over P,M: Committing the data |
| 20 | +``` |
| 21 | + |
| 22 | +### Persistent storage |
| 23 | + |
| 24 | +The persistent storage is the place where the actual save data is stored. It can |
| 25 | +be a file on the disk, a browser's local storage, a cloud, etc. Once the data is |
| 26 | +stored here, it will persist even after the game is closed. From the player's |
| 27 | +perspective, the game has been saved only if it has been committed to the |
| 28 | +persistent storage. |
| 29 | + |
| 30 | +### Memory |
| 31 | + |
| 32 | +The memory is a staging area for our save data. When saving the game state we |
| 33 | +start by serializing it into the memory. The game can then continue as usual |
| 34 | +while the data is asynchronously committed to the persistent storage. |
| 35 | + |
| 36 | +### Game state |
| 37 | + |
| 38 | +As the name suggests, the game state is the current state of our game as |
| 39 | +represented by game objects. Once the save data is loaded into the memory, we |
| 40 | +can deserialize it and apply the values to the objects in the scene. |
| 41 | + |
| 42 | +## Saving and loading |
| 43 | + |
| 44 | +In the diagram above, `loading` means moving the save data from left to right. |
| 45 | +Analogically, `saving` means moving the date in the opposite direction. Note |
| 46 | +that we don't need to always go all the way. For example, sometimes it may be |
| 47 | +useful to save the data only in the memory. |
| 48 | + |
| 49 | +Imagine the following scenario: The player is in `Scene A` and knocks over a |
| 50 | +vase. They then leave `Scene A` and move to `Scene B`. We'd like to store the |
| 51 | +information about the vase so that when the player returns to `Scene A` it's in |
| 52 | +the same state they left it in. At the same time, our game uses a checkpoint |
| 53 | +system, where the game is saved only in key moments of the story. We don't want |
| 54 | +to save it each time the player switches scenes. |
| 55 | + |
| 56 | +In this case, saving the game only in the memory would be a good solution. When |
| 57 | +the player returns to `Scene A`, we can load the data from the memory and apply |
| 58 | +it to the vase. But only commit it to the persistent storage when the player |
| 59 | +reaches a checkpoint. |
| 60 | + |
| 61 | +Additionally, we can allow the player to go back to the previous checkpoint by |
| 62 | +purging the in-memory data and loading it again from the persistent storage. |
| 63 | + |
| 64 | +## Save Controllers |
| 65 | + |
| 66 | +In Safekeeper, each save slot is represented by an instance of the |
| 67 | +[`SaveControllerBase`](xref:Aarthificial.Safekeeper.SaveControllerBase) class. |
| 68 | +It serves as a handle to the underlying data and provides methods to |
| 69 | +[save](<xref:Aarthificial.Safekeeper.SaveControllerBase.Save(Aarthificial.Safekeeper.SaveMode)>), |
| 70 | +[load](<xref:Aarthificial.Safekeeper.SaveControllerBase.Load(Aarthificial.Safekeeper.SaveMode)>), |
| 71 | +and [clear](xref:Aarthificial.Safekeeper.SaveControllerBase.Delete) the slot. |
| 72 | + |
| 73 | +When instantiating a new save controller, you need to provide it with an |
| 74 | +[`ISaveLoader`](xref:Aarthificial.Safekeeper.Loaders.ISaveLoader). The loader is |
| 75 | +responsible for communicating with the persistent storage. Safekeeper comes with |
| 76 | +a few built-in loaders, but you can also implement your own: |
| 77 | + |
| 78 | +- [`FileSaveLoader`](xref:Aarthificial.Safekeeper.Loaders.FileSaveLoader) - |
| 79 | + Loads and saves the data to a file on the disk using Unity's persistent |
| 80 | + storage path. |
| 81 | +- [`DummySaveLoader`](xref:Aarthificial.Safekeeper.Loaders.DummySaveLoader) - |
| 82 | + Does nothing. Useful for testing. |
| 83 | + |
| 84 | +After the save controller is instantiated, you need to initialize it by calling |
| 85 | +the [`Initialize`](xref:Aarthificial.Safekeeper.SaveControllerBase.Initialize) |
| 86 | +method. This is an asynchronous operation, so it may take a while for the |
| 87 | +controller to be ready. You can await the returned task or check its status by |
| 88 | +reading the |
| 89 | +[`IsLoading`](xref:Aarthificial.Safekeeper.SaveControllerBase.IsLoading) |
| 90 | +property. |
| 91 | + |
| 92 | +> [!NOTE] |
| 93 | +> |
| 94 | +> The controller uses a semaphore so its safe to call other methods before the |
| 95 | +> initialization is complete. They will be queued and executed once the |
| 96 | +> controller is ready. |
| 97 | +
|
| 98 | +## Save data |
| 99 | + |
| 100 | +Once the save controller has been initialized and the persistent data has been |
| 101 | +fetched for the first time, you can access the in-memory data by reading the |
| 102 | +[`Data`](xref:Aarthificial.Safekeeper.SaveControllerBase.Data) property. |
| 103 | + |
| 104 | +It provides a few method for reading and writing the data, similarly to Unity's |
| 105 | +`JsonUtility`: |
| 106 | + |
| 107 | +```csharp |
| 108 | +class StoredData { |
| 109 | + public int health; |
| 110 | + public int mana; |
| 111 | +} |
| 112 | + |
| 113 | +var location = new SaveLocation("global", "player"); |
| 114 | + |
| 115 | +// Reading the data as a new instance: |
| 116 | +var data = controller.Data.Read<StoredData>(location); |
| 117 | + |
| 118 | +// Reading the data into an existing instance: |
| 119 | +controller.Data.Read(location, data); |
| 120 | + |
| 121 | +// Writing the data: |
| 122 | +controller.Data.Write(location, data); |
| 123 | +``` |
| 124 | + |
| 125 | +### Save locations |
| 126 | + |
| 127 | +In order to read and write the data, we need to specify the |
| 128 | +[`SaveLocation`](xref:Aarthificial.Safekeeper.SaveLocation). It's a simple |
| 129 | +structure that contains two strings: `chunkId` and `objectId`. For |
| 130 | +organizational purposes, objects store in the save data are grouped into chunks. |
| 131 | +This allows us to reuse Unity's [`GlobalObjectId`][GlobalObjectId] to |
| 132 | +automatically generate unique locations for our objects. |
| 133 | + |
| 134 | +If you want to automatically generate the location for a component, add |
| 135 | +[`ObjectLocationAttribute`](xref:Aarthificial.Safekeeper.ObjectLocationAttribute) |
| 136 | +to a serialized `SaveLocation` field: |
| 137 | + |
| 138 | +```csharp |
| 139 | +public class MyComponent : MonoBehaviour { |
| 140 | + [ObjectLocation] |
| 141 | + public SaveLocation Location; |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +For ScriptableObjects, you can use |
| 146 | +[`AssetLocationAttribute`](xref:Aarthificial.Safekeeper.AssetLocationAttribute): |
| 147 | + |
| 148 | +```csharp |
| 149 | +public class MyScriptableObject : ScriptableObject { |
| 150 | + [AssetLocation("my-assets")] |
| 151 | + public SaveLocation Location; |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +[GlobalObjectId]: https://docs.unity3d.com/ScriptReference/GlobalObjectId.html |
0 commit comments