Skip to content

Commit 6b051a9

Browse files
committed
feat: initial commit
0 parents  commit 6b051a9

File tree

125 files changed

+7172
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

125 files changed

+7172
-0
lines changed

.docfx/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/api/
2+
/_site/
3+
/node_modules/
4+
/_exported_templates/

.docfx/.prettierrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"bracketSpacing": false,
3+
"singleQuote": true,
4+
"printWidth": 80,
5+
"trailingComma": "all",
6+
"arrowParens": "avoid",
7+
"proseWrap": "always"
8+
}

.docfx/CNAME

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
safekeeper.aarthificial.com

.docfx/docfx.json

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"metadata": [
3+
{
4+
"src": [
5+
{
6+
"src": "..",
7+
"files": [".project/Aarthificial.Safekeeper.csproj"]
8+
}
9+
],
10+
"dest": "api/runtime",
11+
"includePrivateMembers": false,
12+
"disableGitFeatures": false,
13+
"disableDefaultFilter": false,
14+
"noRestore": false,
15+
"namespaceLayout": "flattened",
16+
"memberLayout": "samePage",
17+
"allowCompilationErrors": false
18+
},
19+
{
20+
"src": [
21+
{
22+
"src": "..",
23+
"files": [".project/Aarthificial.Safekeeper.Editor.csproj"]
24+
}
25+
],
26+
"dest": "api/editor",
27+
"includePrivateMembers": false,
28+
"disableGitFeatures": false,
29+
"disableDefaultFilter": false,
30+
"noRestore": false,
31+
"namespaceLayout": "flattened",
32+
"memberLayout": "samePage",
33+
"allowCompilationErrors": false
34+
}
35+
],
36+
"build": {
37+
"globalMetadata": {
38+
"_lang": "en",
39+
"_appFaviconPath": "images/favicon.svg",
40+
"_appLogoPath": "images/logo.svg",
41+
"_appLogoDarkPath": "images/logo-dark.svg",
42+
"_appBannerUrl": "https://safekeeper.aarthificial.com/images/banner.png",
43+
"_appName": "Safekeeper",
44+
"_appTitle": "Safekeeper",
45+
"_appFooter": "Made with <a href=\"https://dotnet.github.io/docfx\">docfx</a></span>",
46+
"_enableSearch": true,
47+
"_gitContribute": {
48+
"repo": "https://github.com/aarthificial-gamedev/safekeeper",
49+
"branch": "main",
50+
"apiSpecFolder": ".project/apispec"
51+
}
52+
},
53+
"content": [
54+
{
55+
"files": ["api/**.yml"]
56+
},
57+
{
58+
"files": ["editor/index.md", "runtime/index.md"],
59+
"src": "docs/api",
60+
"dest": "api"
61+
},
62+
{
63+
"files": ["docs/**.md", "docs/**/toc.yml", "toc.yml"],
64+
"exclude": ["docs/api/**", "docs/index.md"]
65+
},
66+
{
67+
"files": ["index.md"],
68+
"src": "docs",
69+
"dest": "."
70+
}
71+
],
72+
"resource": [
73+
{
74+
"files": ["images/**", "CNAME"]
75+
}
76+
],
77+
"overwrite": [
78+
{
79+
"files": ["docs/api/*.md"],
80+
"exclude": ["docs/api/runtime/index.md", "docs/api/editor/index.md"]
81+
}
82+
],
83+
"output": "_site",
84+
"globalMetadataFiles": [],
85+
"fileMetadataFiles": [],
86+
"template": ["default", "modern", "template"],
87+
"xref": ["https://normanderwan.github.io/UnityXrefMaps/xrefmap.yml"],
88+
"xrefService": ["https://xref.docs.microsoft.com/query?uid={uid}"],
89+
"postProcessors": [],
90+
"keepFileLink": false,
91+
"disableGitFeatures": false
92+
}
93+
}

.docfx/docs/api/editor/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Editor API

.docfx/docs/api/runtime/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Runtime API

.docfx/docs/concepts.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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

.docfx/docs/example.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Full example
2+
3+
Below is a minimal example of a scene manager that uses safekeeper. Presumably,
4+
this MonoBehaviour is attached to a game object that persists throughout the
5+
whole game:
6+
7+
```csharp
8+
public class SceneManager : MonoBehaviour {
9+
private SaveControllerBase _controller;
10+
11+
private void Awake() {
12+
_controller = new SaveControllerBase(
13+
new FileSaveLoader("slot1")
14+
);
15+
_controller.Initialize();
16+
}
17+
18+
public IEnumerator LoadScene(string scenePath) {
19+
// Save the current state to memory
20+
yield return WaitForTask(_controller.Save());
21+
// Switch the scene
22+
yield return SceneManager.LoadSceneAsync(scenePath);
23+
// Load the state from memory
24+
yield return WaitForTask(_controller.Load());
25+
}
26+
27+
public IEnumerator SaveGame() {
28+
// Save the current state to memory and commit it to the persistent storage
29+
yield return WaitForTask(_controller.Save(SaveMode.Full));
30+
}
31+
32+
public IEnumerator ResetGame() {
33+
// Reset memory with the data from the persistent storage
34+
yield return WaitForTask(_controller.Load(SaveMode.PersistentOnly));
35+
// Reload the scene
36+
yield return SceneManager.LoadSceneAsync(
37+
SceneManager.GetActiveScene().path
38+
);
39+
// Load the state from memory
40+
yield return WaitForTask(_controller.Load());
41+
}
42+
43+
private IEnumerator WaitForTask(Task task) {
44+
while (!task.IsCompleted) {
45+
yield return null;
46+
}
47+
48+
if (task.IsFaulted) {
49+
ExceptionDispatchInfo.Capture(task.Exception).Throw();
50+
}
51+
}
52+
}
53+
```
54+
55+
With this setup, you can easily save the state of your game by implementing the
56+
[`ISaveStore`](xref:Aarthificial.Safekeeper.Stores.ISaveStore) interface and
57+
registering it with the
58+
[`SaveStoreRegistry`](xref:Aarthificial.Safekeeper.Stores.SaveStoreRegistry):
59+
60+
```csharp
61+
public class SavedTransform : MonoBehaviour, ISaveStore {
62+
private class StoredData {
63+
public Vector3 position;
64+
public Quaternion rotation;
65+
}
66+
67+
[ObjectLocation]
68+
[SerializeField]
69+
private SaveLocation _location;
70+
private StoredData _data = new();
71+
72+
public void OnEnable() {
73+
SaveStoreRegistry.Register(this);
74+
}
75+
76+
public void OnDisable() {
77+
SaveStoreRegistry.Unregister(this);
78+
}
79+
80+
// OnLoad will be invoked right after the scene is loaded.
81+
// Before `Start` but after `OnEnable`.
82+
public void OnLoad(SaveControllerBase save) {
83+
if (save.Data.Read(_location, _data)) {
84+
transform.position = _data.position;
85+
transform.rotation = _data.rotation;
86+
}
87+
}
88+
89+
// OnLoad will be invoked right before the scene in unloaded or whenever
90+
// the game is saved.
91+
public void OnSave(SaveControllerBase save) {
92+
_data.position = transform.position;
93+
_data.rotation = transform.rotation;
94+
save.Data.Write(_location, data);
95+
}
96+
}
97+
```

0 commit comments

Comments
 (0)