Save Async is BUCK's Unity package for asynchronously saving and loading data in the background using Unity's Awaitable
class. It includes a simple API that makes it easy to capture and restore state without interrupting Unity's main render thread. That means smoother framerates and fewer load screens!
- ⌚ Asynchronous: All methods are asynchronously "awaitable" meaning the game can continue running while it waits for a response from the storage device, even if that storage device is slow (like HDDs, external storage, and some consoles)
- 🧵 Background Threading: All file I/O occurs on background threads which helps avoid dips in framerate
- âš¡ SaveManager API: Simple API that can be called from anywhere with methods like
SaveManager.Save()
- 💾 ISaveable Interface: Implement this interface on any class to give it the ability to be saved and loaded
- 📒 JSON Serialization: Data is saved to JSON automatically using Unity's own Newtonsoft Json Unity Package
Note
This package works with Unity 2023.1 and above as it requires Unity's Awaitable
class which is not available in earlier versions.>
📺 Watch the Save Async Tutorial video or follow the instructions below.
- Copy the git URL of this repository:
https://github.com/buck-co/save-async.git
- In Unity, open the Package Manager from the menu by going to
Window > Package Manager
- Click the plus icon in the upper left and choose
Add package from git URL...
- Paste the git URL into the text field and click the
Add
button.
- Copy this git URL:
https://github.com/applejag/Newtonsoft.Json-for-Unity.Converters.git#upm
- In Unity, open the Package Manager from the menu by going to
Window > Package Manager
- Click the plus icon in the upper left and choose
Add package from git URL...
- Paste the git URL into the text field and click the
Add
button.
Save Async depends on Unity's Json.NET package for serializing data, which is already included as a package dependency and will be installed automatically. However, Unity types like Vector3 don't serialize to JSON very nicely, and can include ugly recursive property loops, like this:
{
"x": 0,
"y": 1,
"z": 0,
"normalized": {
"x": 0,
"y": 1,
"z": 0,
"normalized": {
"x": 0,
"y": 1,
"z": 0,
"normalized": {
"x": 0,
"y": 1,
"z": 0,
"normalized": {
...
}
}
}
}
}
Yikes! Installing the Unity Converters for Newtonsoft.Json package takes care of these issues, as well as many more.
Once you've done this, Json.NET should be able to convert Unity's built-in types. In the future, we'll try to include this as a package dependency, but currently the Unity Package Manager only allows packages to have dependencies that come from the official Unity registry.
After installing the package...
- Add the
SaveManager
component to a GameObject in your scene. - Implement the
ISaveable
interface on at least one class (more detail on how to do this is available below). - Register the ISaveable by calling
SaveManager.RegisterSaveable(mySaveableObject);
This is usually done inMonoBehaviour.Awake()
- Call SaveManager API methods like
SaveManager.Save()
from elsewhere in your project, such as from a Game Manager class. Do this after all your ISaveable implementations are registered.
This package includes a sample project which you can install from the Unity Package Manager by selecting the package from the list and then selecting the Samples
tab on the right. Then click Import
. Examining the sample can help you understand how to use the package in your own project.
Any class that should save or load data needs to implement the ISaveable
interface.
- Key Property: Each
ISaveable
must have a globally unique string for distinguishing it when saving and loading data. - Filename Property: Each
ISaveable
must have a filename string that identifies which file it should be saved in. - CaptureState Method: This method captures and returns the current state of the object in a serializable format.
- RestoreState Method: This method restores the object's state from the provided data.
-
Implement
ISaveable
in Your Class
Your class should inherit fromISaveable
from theBuck.SaveAsync
namespace.using Buck.SaveAsync public class YourClass : MonoBehaviour, ISaveable { // Your code here... }
-
Choose a Filename
This is the file name where this object's data will be saved.public string Filename => Files.GameData;
It is recommended to use a static class to store file paths as strings to avoid typos.
public static class Files { public const string GameData = "GameData.dat"; public const string SomeFile = "SomeFile.dat"; }
-
Generate and Store a Unique Serializable Key
Ensure that your class has a globally unique string key, such as "GameDataExample".public string Key => "GameDataExample";
Optionally, you can generate and use a serializable Guid to uniquely identify your objects. Use
SaveManager.GetSerializableGuid()
in MonoBehaviour.OnValidate() to get the Guid and then store it as a serialized byte array (since the System.Guid type itself cannot be serialized).[SerializeField, HideInInspector] byte[] m_guidBytes; public string Key => new Guid(m_guidBytes).ToString(); void OnValidate() => SaveManager.GetSerializableGuid(ref m_guidBytes);
-
Register Your Object with
SaveManager
Register the object withSaveManager
. Generally it's best to do this in yourAwake
method or during initialization. Make sure you do this before calling any save or load methods in the SaveManager class or your saveables won't be picked up!void Awake() { SaveManager.RegisterSaveable(this); }
-
Define Your Data Structure
Create a struct or class that represents the data you want to save. This structure needs to be serializable.[Serializable] public struct MyCustomData { // Custom data fields public string playerName; public int playerHealth; public Vector3 position; public Dictionary<int, Item> inventory; }
-
Implement
CaptureState
andRestoreState
Methods
Implement theCaptureState
method to capture and return the current state of your object. Then implement theRestoreState
method to restore your object's state from the saved data. Both of these methods will be called by theSaveManager
class when you call its save and load methods.public object CaptureState() { return new MyCustomData { playerName = m_playerName, playerHealth = m_playerHealth, position = m_position, inventory = m_inventory }; } public void RestoreState(object state) { var s = (MyCustomData)state; m_playerName = s.playerName; m_playerHealth = s.playerHealth; m_position = s.position; m_inventory = s.inventory; }
For a complete example, check out this ISaveable implementation in the sample project.
SaveManager
methods can be called anywhere in your game's logic that you want to save or load, such as in a Game Manager class or a main menu screen. You should add the SaveManager component to a GameObject in your scene. Below you'll find the public interface for interacting with the SaveManager class, along with short code examples.
Note
The SaveManager
class is in the Buck.SaveAsync
namespace. Be sure to include this line at the top of any files that make calls to SaveManager methods or implement the ISaveable interface.
using Buck.SaveAsync
Indicates whether or not the SaveManager class is currently busy with a file operation. This can be useful if you want to wait for one operation to finish before doing another, although because file operations are queued, this generally is only necessary for benchmarking and testing purposes.
Usage Example:
while (SaveManager.IsBusy)
await Awaitable.NextFrameAsync();
Registers an ISaveable with the SaveManager class for saving and loading.
Usage Example:
SaveManager.RegisterSaveable(mySaveableObject);
Asynchronously saves the files at the specified array of paths or filenames.
Usage Example:
await SaveManager.Save(new string[] {"MyFile.dat"});
Asynchronously loads the files at the specified array of paths or filenames.
Usage Example:
await SaveManager.Load(new string[] {"MyFile.dat"});
Asynchronously deletes the files at the specified array of paths or filenames.
Usage Example:
await SaveManager.Delete(new string[] {"MyFile.dat"});
Asynchronously erases the files at the specified paths or filenames, leaving them empty but still on disk.
Usage Example:
await SaveManager.Erase(new string[] {"MyFile.dat"});
Sets the given Guid byte array to a new Guid byte array if it is null, empty, or an empty Guid. The guidBytes
parameter is a byte array (passed by reference) that you would like to fill with a serializable guid.
Usage Example:
[SerializeField, HideInInspector] byte[] m_guidBytes;
public string Key => new Guid(m_guidBytes).ToString();
void OnValidate() => SaveManager.GetSerializableGuid(ref m_guidBytes);
If you want to prevent mischievous gamers from tampering with your save files, you can encrypt them using XOR encryption. To turn it on, use the encryption dropdown menu on the SaveManager component in your scene and create a password. XOR is very basic and can be hacked using brute force methods, but it is very fast. AES encryption is planned!
Figuring out how to save and load your game data can be tricky, but what's even more challenging is deciding when to save your game data. Not only is there the issue of data serialization and file I/O, but in addition, save and load operations often end up happening synchronously on Unity's main thread which will cause framerate dips. That's because Unity's renderer is also on the main thread! Furthermore, while most desktops have fast SSDs, sometimes file I/O can take longer than the time it takes to render a frame, especially if you're running your game on a gaming console or a computer with an HDD.
We hit these pain points on our game Let's! Revolution! and we wanted to come up with a better approach. By combining async
and await
with Unity's Awaitable
class (available in Unity 2023.1 and up), it is now possible to do file operations both asynchronously and on background threads. That means you can save and load data in the background while your game continues to render frames seamlessly. Nice! However, there's still a good bit to learn about how multithreading works in the context of Unity and how to combine that with a JSON serializer and other features like encryption. The Save Async package aims to take care of these complications and make asynchronous saving and loading data in Unity a breeze!
While Coroutines have served us well for many years, the Task-based asynchronous pattern (TAP) enabled by async/await and Unity's Awaitable
class has many advantages. Coroutines can execute piece by piece over time, but they still process on Unity's single main thread. If a Coroutine attempts a long-running operation (like accessing a file) it can cause the whole application to freeze for several frames. For a good overview of the differences between async/await and Coroutines, check out this Unite talk Best practices: Async vs. coroutines - Unite Copenhagen.
If you have any trouble using the package, feel free to open an issue. And if you're interested in contributing, create a pull request and we'll take a look!
- Nick Pettit - nickpettit
See also the list of contributors who participated in this project.
- Thanks to Tarodev for this tutorial on using async and await in Unity using the Awaitable class. It gave me the idea for creating an async save system.
- Thanks to Dapper Dino for this tutorial which demonstrated how a form of the inversion of control design pattern could be used to make saving and loading easier.
- Thanks to Bronson Zgeb at Unity for this Unite talk which shows many of the pieces necessary for building a save system in Unity.
MIT License - Copyright (c) 2024 BUCK Design LLC buck-co
BUCK is a global creative company that brings brands, stories, and experiences to life through art, design, and technology. If you're a Game Developer or Creative Technologist or want to get involved with our work, reach out and say hi via Github, Instagram or our Careers page. 👋