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 (quickSave.HasBeenPressed()) SaveManager.SaveGame(0);
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.

Note

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 / scale

The entity's transform at save time.

velocity

The entity's current velocity, so physics momentum is preserved 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.

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:

public override CustomSaveData CaptureCustomData()
{
    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.

Note

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:

public override CustomSaveData CaptureCustomData()
{
    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:

// Set a flag
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.