The Save System
How It Works
When you save a game, the engine captures a snapshot of the entire world. This includes the active map, every entity's position, rotation, velocity, and output connections, global state flags, and any custom data your controllers choose to provide. On load, the map is reloaded fresh and every entity from the snapshot is re-spawned with its saved state restored.
The save file is written as JSON to the OS application data folder under your game's name, as set in
GameSettings.gameName. The current API uses integer slots, so saves are stored as
s0.sav, s1.sav, and so on. Named saves are planned for a future update.
Triggering a Save or Load
Saving and loading are deferred — calling either method queues the action for the next frame rather than executing immediately. This avoids any issues with modifying world state mid-update. The FPS template already wires up quick save and quick load in the player controller:
if (quickLoad.HasBeenPressed()) SaveManager.LoadGame(0);
You can call these from anywhere. If you want a dedicated save point entity or an autosave trigger, just
call SaveManager.SaveGame(slot) from that entity's input handler.
Always return early after calling LoadGame() if you're inside a controller's update loop.
The load will invalidate the current world state on the next frame, and continuing to run code against
the old state can cause issues.
What Gets Saved Automatically
The engine saves the following for every entity without any code on your part:
position / rotation / scaleThe entity's transform at save time.
velocityThe entity's current velocity, so physics momentum is preserved across loads.
isSimulatedWhether the physics body was active at save time.
entityOutputsAll output connections set up in the editor, so map logic state is preserved.
propertiesThe entity's editor properties.
nameThe entity's target name from the map.
Brush entities additionally save which brush they are tied to, so moving geometry like doors lands back in
the right place. Global state flags set via GlobalState are also saved and restored, referring
to game state data that persists across levels.
Saving Custom Controller Data
Anything beyond position and velocity needs to be saved manually. Override CaptureCustomData()
and RestoreCustomData() in your controller to participate in the save system. The player
controller does this for camera angles and crouch state:
{
CustomSaveData d = new CustomSaveData();
d.vals = new Dictionary<string, object>
{
{ "pitch", pitch },
{ "yaw", yaw },
{ "crouched", crouched },
};
return d;
}
public override void RestoreCustomData(CustomSaveData? o)
{
if (!o.HasValue) return;
crouched = (bool)o.Value.vals["crouched"];
entity.bounds = crouched ? crouchedBounds : uncrouchedBounds;
pitch = (float)(double)o.Value.vals["pitch"];
yaw = (float)(double)o.Value.vals["yaw"];
}
CustomSaveData is a simple struct wrapping a Dictionary<string, object>.
You can store any JSON-serializable value in it. RestoreCustomData is called immediately
after OnSpawn() when loading, so by the time it runs your entity is fully initialized and
ready to accept state.
Because the data round-trips through JSON, numeric types can deserialize as a different type than you
stored them. Floats come back as double, which is why the player controller casts through
(float)(double). Always cast defensively when restoring numeric values.
A Practical Example
Here's how you'd add save support to the Wandering Grunt from the first entity guide. The things worth saving are HP, whether it was chasing the player, and the wander timer:
{
CustomSaveData d = new CustomSaveData();
d.vals = new Dictionary<string, object>
{
{ "hp", HP },
{ "isDead", IsDead },
{ "chasing", chasing },
{ "wanderTimer", wanderTimer },
};
return d;
}
public override void RestoreCustomData(CustomSaveData? o)
{
if (!o.HasValue) return;
HP = (int)(long)o.Value.vals["hp"];
IsDead = (bool)o.Value.vals["isDead"];
chasing = (bool)o.Value.vals["chasing"];
wanderTimer = (float)(double)o.Value.vals["wanderTimer"];
if (IsDead)
{
entity.isSimulated = false;
entity.ignoreCollision = true;
entity.bounds = new BoundingBox();
}
}
Note the dead check at the end of RestoreCustomData. Because OnSpawn runs first
and sets the entity up as a living enemy, we need to re-apply the dead state ourselves if the grunt was
dead when saved. Position and velocity are already handled by the engine, so the grunt will be right where
it was when you saved.
Global State
For flags that aren't tied to a specific entity, use GlobalState. It holds a
Dictionary<string, bool> that is saved and restored automatically alongside entity data.
This is useful for things like tracking whether a cutscene has played, a door has been permanently opened,
or a one-time event has fired:
GlobalState.states["bossDefeated"] = true;
// Read it back
if (GlobalState.states.TryGetValue("bossDefeated", out bool val) && val)
{
// skip the boss intro
}
Current Limitations
A few things are not yet fully covered by the save system and are worth being aware of:
Ragdoll state is not yet saved on the engine side. If an entity has ragdolled at save time it will be restored to its saved position but the ragdoll physics state won't carry over. This is planned for a future engine update.
LivingActor state (HP, death state) is not saved automatically by the template. You need
to save it manually in your controller's CaptureCustomData as shown in the example above.
A future template update will likely add this to a base class so you don't have to repeat it everywhere.
Named saves are not yet implemented. The current API uses integer slots, which limits flexibility for things like multiple save files or autosave slots with descriptive names. This is planned for a future update.
Entities spawned at runtime (not placed in the map editor) are saved and restored
correctly as long as their class has an [EntityDescriptor] attribute, since the save system
uses the class name to reconstruct them via reflection.