Save Manager
Save Manager takes care of loading and saving one JSON object. You point it at a JSON object, and it handles reading the save when the game starts, writing it when you ask, and picking somewhere sensible to put it on whatever platform the game is running on.
If you need to manage more than one JSON object, add more Save Manager objects, one per JSON object.
Setup
- Add a Save Manager object and a JSON object to your project.
- Set the Save Manager's JSON object property to your JSON object.
- Optionally set Default data to a
.json project file holding the starting values.
That's it. With Auto load on (the default), the save is read before your first layout starts, so your events can read the JSON object right away without waiting for anything.
Where saves go
On platforms with a file system, the save is <Save name>.<Extension>, inside <Folder>/<Subfolder>.
- Save name: leave blank and it uses the Save Manager object's name.
- Extension:
sav by default, but it can be anything as it is purely cosmetic, the content will always be a JSON file.
- Folder: App data (default), Home, or App folder.
- Subfolder: leave blank and it uses the project name.
If Local storage is used as the backend, there is no file and the save name is used as the storage key.
Default data and merging
When loading, the default data is read first, then the save is merged on top:
- Values in the save win.
- Nested objects are merged key by key, so new keys you add to your default file show up in old saves. That's how you add a setting in an update without breaking existing players save.
- Arrays are replaced whole, not merged.
New save loads only the default data. It doesn't write anything until you save.
Backends
| Method | Where it saves |
| Local storage | Browser storage. Works everywhere. |
| Pipelab | Real files, via Pipelab. |
| Webview | Real files, via Construct's File System plugin. |
| NW.js | Real files, via Node.js. |
| Custom | Wherever you want, see below. |
Auto picks the first one that's actually available and falls back to Local storage.
If you pick a method explicitly, it will not fall back. If you choose Pipelab and Pipelab isn't there, saving reports an error instead of writing somewhere else.
Two backends need setup:
Pipelab must be initialised before the save loads. Use the hook below because doing it in your own beforeprojectstart handler is too late if autoload is on.
globalThis.SaveManager.beforeLoad(async (runtime) => {
await runtime.objects.Pipelab.getFirstInstance()._Initialize();
});
Webview needs Construct's File System plugin in the project. Without it the native file support isn't included in the export and the backend can't work at all.
When a save can't be read
If the save file doesn't exist, the plugin automatically starts a new save with the default data.
If the save exists but can't be read or isn't valid JSON, Save Manager tries to keep a copy before anything overwrites it:
- It retries the read a few times first, in case the problem was temporary.
- If it still can't, it saves a backup next to the original as
<name>.bak.
- Then it loads the default data so the game is still playable, and fires On load error.
Only one backup is kept, and it's replaced the next time one is needed. Local storage can't copy a value it can't read, so in that case there's no backup.
Handle On load error if you want to tell the player something went wrong instead of dropping them into what looks like a fresh game.
Custom backend
Use this to save wherever you like: your own server, an encrypted file, a platform's cloud saves, anything you might need.
Register a handler from a script file, then set Method to Custom and Custom handler ID to the name you registered.
runOnStartup(async (runtime) => {
globalThis.SaveManager.register("demo", {
async save(name, text) {
await runtime.storage.setItem("demo:" + name, btoa(text));
},
async load(name) {
const v = await runtime.storage.getItem("demo:" + name);
return v ? atob(v) : null; // null means "no save yet"
},
async delete(name) {
await runtime.storage.removeItem("demo:" + name);
},
async reveal(name) {
// Optional. Decide what "the save's location" means for your backend. globalThis.open("https://example.com/saves/" + name);
},
});
});
save and load are required, delete is only needed for the Delete save action and reveal for the Reveal save location action. name is the resolved save name, and returning null from load means "no save yet".
Register it at the top level of a script or inside runOnStartup so it exists before the game starts.
Notes
- Operations are queued internally, so a save can never overlap a load or another save.
- Don't use Construct's own savegame system for player progress at the same time as this plugin. They're two separate save systems and mixing them will cause trouble.