Save games for anything a player comes back to: levels, unlocks, a run, a story. A save is a record of named fields in a slot. Slot 0 is the autosave, 1..31 the player's slots. The engine keeps the slots in the browser's storage on the web and in the per-user data folder natively; nothing goes to a server, so a save stays on the device and browser it was made on.
Nothing is saved for you. The engine does not snapshot the scene, physics, animation or audio: the game writes the state it needs to rebuild them.
Writing and reading a save
fn write_save(slot: i32) {
gander::save_begin();
gander::save_put("v", 1); // your save format version
gander::save_put("level", level); // i32, i64, f32, f64, bool
gander::save_put("pos", hero_pos); // float2, float3, float4
gander::save_put("yaw", hero_yaw);
gander::save_put("hp", hp);
gander::save_put("name", hero_name); // text
let ok = gander::save_end(slot, str("Level ", level, ", ", zone_name)); // label on the slot screen
}
fn read_save(slot: i32) -> bool {
if !gander::save_open(slot) { return false; } // empty slot
level = gander::save_get("level", 1); // the default is used when the field is missing
hero_pos = gander::save_get("pos", level_start(level));
hero_yaw = gander::save_get("yaw", 0.0 as f32);
hp = gander::save_get("hp", hp); // current value as the default keeps the type
var nm: u8[>..] = [];
if gander::save_get("name", nm) { hero_name = nm; }
true
}
| Call | Does |
|---|---|
save_begin() |
starts a record |
save_put(key, value) |
one field; keys are text without = or line breaks, not starting with @ |
save_end(slot, label) -> bool |
writes it; false when the storage refused it (full, private window) |
save_open(slot) -> bool |
opens a slot for reading; false when empty |
save_get(key, default) |
the field as the default's type (i32, f32, f64, bool, float2/3/4), or the default when missing or unreadable. An integer literal default reads an i32; an i64 field: save_get_i(key, def) |
save_get(key, out: u8[>..]&) -> bool |
text, appended to out |
save_has(key) -> bool |
the open save has the field |
save_exists(slot), save_delete(slot), save_latest() -> i32 (newest slot, -1 none) |
|
save_label(slot, out&), save_ago(slot, out&) ("12 min ago"), save_time(slot) -> i64 (unix seconds) |
for your own slot UI |
unix_time() -> i64 |
wall clock (daily rewards, timers across sessions) |
Defaults are typed: float literals are f64, so save_get("hp", 0.0) returns
an f64. Pass the current value (save_get("hp", hp)) or cast
(0.0 as f32).
What to save
Save what you cannot rebuild, and rebuild the rest from it on load:
- Progress: level or chapter, checkpoint id, unlocked items, collected pickups, quest steps, money, score, time played.
- The player: position and yaw (at a checkpoint is fine), health, ammo, inventory, equipped item.
- The world, only what changed: opened doors, killed bosses, picked up items. Loading a level and then applying the changes beats saving every entity.
Do not save handles (Model, Texture, Sound, Body...: they are only
valid in this run), physics velocities, animation times, particles or camera
smoothing. On load, reset the round first (as for a new game), then apply the
save, then put physics bodies where the save says (body_set_pos).
Lists: save the count, then one field per item with the index in the key.
gander::save_put("items", inventory.len as i32);
for it, i in inventory {
gander::save_put(str("item", i, ".kind"), it.kind);
gander::save_put(str("item", i, ".count"), it.count);
}
// reading
let n = gander::save_get("items", 0);
for i: i32 in 0..n {
inventory.push(Item { kind: gander::save_get(str("item", i, ".kind"), 0), count: gander::save_get(str("item", i, ".count"), 1) });
}
Booleans per thing (doors, collectibles) can share one text field: a string
of 0 and 1, or a comma list of ids.
Versions: the game will change after players have saves. Never reuse a
key for a different meaning; add new keys (old saves read their default).
Save "v" and branch on it when a change needs converting.
The slot screen
ui_save_slots(mode, count) -> i32 is the whole screen: one card per slot
with its label and "12 min ago", Delete on each, a confirm before
overwriting or deleting, Back. It works with mouse, touch, keys and pads.
gander::SLOTS_SAVElists slots 1..count (save into one),gander::SLOTS_LOADthe autosave too, when there is one.- Returns the slot picked this frame,
SLOTS_OPEN(-1) while open,SLOTS_BACK(-2) when closed. It only picks: you save or load.
With Flow (the usual way)
Flow { saves: 3, .. } adds the save menus to the ready-made game flow: the
main menu gets Continue (the newest save, autosave included), New game, Load
game and Settings once a save exists; the pause menu gets Save game and Load
game. Flow sets save_to or load_from to the slot on the frame the player
picks it (-1 otherwise), and toasts "Game saved" the frame after, or "Could not
save the game" when nothing was written. On desktop and Android the pause
menu also has Quit: on that frame save_to is 0 (the autosave) and
flow.quitting is true, and the game ends after the frame.
var flow = gander::Flow { saves: 3, .. };
fn main() {
gander::config("Iron Harbor", 1280, 720, 0);
gander::flow_init(flow);
}
export "gander_frame" fn frame(dt: f32) {
let playing = gander::flow_update(flow, "IRON HARBOR", "Hold the docks until dawn");
if flow.started { reset_round(); } // New game, Continue, Load, Play again
if flow.load_from >= 0 { let ok = read_save(flow.load_from); }
if flow.save_to >= 0 { write_save(flow.save_to); }
if playing { simulate(dt); }
draw_world();
}
Without Flow, open the screen from your own menus:
if screen == SCREEN_SAVE {
let k = gander::ui_save_slots(gander::SLOTS_SAVE, 3);
if k >= 0 { write_save(k); gander::ui_toast("Game saved", 2.0); screen = SCREEN_PAUSED; }
if k == gander::SLOTS_BACK { screen = SCREEN_PAUSED; }
}
Autosave
Write slot 0 at natural moments: a checkpoint, a level cleared, a shop closed, every few minutes of play. Never every frame: each save is a synchronous storage write.
fn checkpoint() {
write_save(0);
gander::ui_toast("Checkpoint", 1.5);
}
Short arcade games with no progress beyond a high score do not need slots:
use the high score boards (GANDER 0.1 API reference, Analytics and high scores), and
save(key, value) / load(key, out) for a single setting or best time.
Testing
gander test starts with empty storage every run (gander test --native
keeps its saves in the per-user folder between runs). Save and load in the
same run: drive the menus with --input, or call write_save(1) and
read_save(1) at fixed frames, and probe the values that should come back.