The Save System
How It Works
When you save a game, the engine captures a snapshot of the entire world: the active map, every entity’s position, rotation, velocity, and output connections, global state flags, and any custom data your controllers provide. On load, the map is reloaded fresh and every entity from the snapshot is re-spawned with its saved state restored.
Save files are written to the OS application data folder under your game’s name, as set in GameSettings.GameName. Saves are identified by a string name and a save type that determines the file extension: manual saves use .msav, quick saves use .qsav, and autosaves use .asav.
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 modifying world state mid-update:
// Save
SaveManager.SaveGame(“slot1”, SaveManager.SaveType.Quick);
// Load
SaveManager.LoadGame(“slot1”);
SaveGame takes a string name and an optional SaveType (defaults to Manual). LoadGame takes a name and finds the most recently written save file for that name across all three types. Call these from anywhere: a save point entity, an autosave trigger, a menu button, or a player controller hotkey.
What Gets Saved Automatically
The engine saves the following for every entity without any code on your part:
position / rotation / scale The entity’s transform at save time.
velocity The entity’s current velocity, preserving physics momentum across loads.
isSimulated Whether the physics body was active at save time.
entityOutputs All output connections set up in the editor, so map logic state is preserved.
properties The entity’s editor properties.
name The entity’s target name from the map.
Global state flags set via GlobalState are also saved and restored.
Saving Fields with [SaveValue]
For any data beyond position and velocity, apply [SaveValue(“key”)] directly to fields or properties on your entity or controller. The engine handles serialization automatically:
[SaveValue(“hp”)]
private int HP = 100;
[SaveValue(“is_dead”)]
private bool IsDead = false;
[SaveValue(“chasing”)]
private bool chasing = false;
The string key is what gets written into the save file. Any field or property, public or private, on an entity or its controller can be tagged. The attribute walks the full inheritance chain, so values on a base class are included automatically.
Type conversion on restore is handled by Convert.ChangeType. If a key is missing from the save data (for example, a save made before the field existed), that field is simply left at its default value and no error is thrown.
[SaveValue] works on both WorldEntity subclasses and EntityController subclasses. The engine captures and restores tagged members from both in the same pass.
Running Code on Restore
If you need to run side-effect logic when loading (re-applying collision bounds for a dead enemy, for instance), override RestoreCustomData in your controller and call base.RestoreCustomData(o) first so the [SaveValue] fields are restored before your logic runs:
[SaveValue(“is_dead”)] private bool IsDead = false;
public override void RestoreCustomData(CustomSaveData? o)
{
base.RestoreCustomData(o);
if (IsDead)
{
entity.isSimulated = false;
entity.ignoreCollision = true;
entity.bounds = new BoundingBox();
}
}
RestoreCustomData is called immediately after OnSpawn() when loading, so by the time it runs your entity is fully initialized and ready to accept state changes.
Manual Save Data
For data that can’t be expressed as a plain field write, override CaptureCustomData() alongside RestoreCustomData(). CustomSaveData wraps a Dictionary<string, object>:
public override CustomSaveData CaptureCustomData()
{
var data = base.CaptureCustomData();
data.vals[“my_key”] = myValue;
return data;
}
public override void RestoreCustomData(CustomSaveData? o)
{
base.RestoreCustomData(o);
if (o?.vals?.TryGetValue(“my_key”, out var val) == true)
{
myValue = Convert.ToInt32(val);
}
}
Any serializable value can go in the dictionary. For most cases [SaveValue] is simpler, but the manual override is useful when you need to transform or validate data on restore.
Global State
For flags not tied to any specific entity, use GlobalState. It holds a Dictionary<string, bool> saved and restored alongside entity data. Useful for tracking whether a cutscene has played, a key has been collected, or a one-time event has fired:
// Set a flag
GlobalState.states[“bossDefeated”] = true;
// Read it back
if (GlobalState.states.TryGetValue(“bossDefeated”, out bool val) && val)
{
// skip the boss intro
}
Entities Spawned at Runtime
Entities spawned dynamically during gameplay (not placed in the map editor) are saved and restored correctly as long as their class has an [EntityDescriptor] attribute. The save system uses the class name to reconstruct them via reflection. If you spawn an entity type that doesn’t have [EntityDescriptor], it won’t survive a save/load cycle.
Starting a New Game
When starting a new game session, call SaveManager.ClearSessionMapStates() to wipe any cached map states from a previous session. Otherwise, if the player previously visited a map and saved, the old state will be restored when they enter that map again in the new session. The map console command calls this automatically, which is why loading a map via the console always starts fresh.
Current Limitations
Ragdoll state is not yet saved. Entities that have ragdolled will restore to their saved position, but the ragdoll physics won’t carry over.
HP and death state in LivingActor are not saved automatically by the engine. Add [SaveValue] to your HP and IsDead fields, then override RestoreCustomData to re-apply any side effects of being dead. The Grunt example in Writing Your First Entity shows exactly this pattern.