Save data
Persist per-player saves and per-game shared data — without writing backend code.
If your game needs to remember things between sessions — high scores, unlocked levels, settings, progress — you don't need to set up a backend. Umicat ships a save-data system the agent wires into your game with a few requests.
There are two save surfaces:
- Per-player saves (
saves.*) — each signed-in player has their own data per game. Anonymous players save to localStorage automatically. - Per-game shared data (
gameData.*) — one shared bucket per game, visible to all players. Public reads, owner-only writes.
When to use which
| Need | Use |
|---|---|
| Player's high score, progress, settings | Per-player saves |
| Save slots / "load game" UI | Per-player saves |
| Game-wide leaderboard or daily challenge | Per-game shared data |
| Asset paths the game references at runtime | Per-game shared data |
| Anything the game owner sets and players only read | Per-game shared data |
Asking the agent
You don't write any save code yourself. Ask:
When the player dies, save their final score as the player's high score.
When the game starts, load the saved high score and show it in the HUD.The agent will:
- Pick a save key (e.g.
'highScore'). - Wire a load on scene start.
- Wire a save on the death event.
- Update the HUD widget to read the loaded value.
You'll see the agent's tool calls in the activity strip. On reload, the game persists.
Per-player saves — the contract
The agent has access to a saves skill with three operations:
| Operation | What it does |
|---|---|
saves.get(key) | Read a value for the current player. Returns null if not set. |
saves.set(key, value) | Write a value for the current player. Any JSON-serializable shape. |
saves.delete(key) | Remove a value. |
Values can be any JSON: numbers, strings, objects, arrays. Reasonable size limit per key: ~64 KB.
Signed in vs anonymous
- Signed in: saves go to the Umicat backend, keyed by player account + game. Survives device changes — the player's progress travels with their account.
- Anonymous: the SDK's local transport stores saves in the browser's
localStorage. Player keeps progress on this device only. If they later sign in, anonymous saves do not auto-migrate (yet).
Both paths use the same saves.get/set/delete API — the agent writes
code that doesn't know or care which transport is active.
Per-game shared data — the contract
For game-wide values readable by all players (or game-internal config the owner sets):
| Operation | Who can call | What it does |
|---|---|---|
gameData.get(key) | Anyone (public read) | Read a value for the game. |
gameData.list(prefix?) | Anyone (public read) | List all keys under an optional prefix. |
gameData.set(key, value) | Owner only (signed in as project owner) | Write a value. |
gameData.delete(key) | Owner only | Remove a value. |
Common uses:
- Daily challenge seed. Owner sets
gameData.set('dailyChallenge', { seed: '2026-05-24', enemies: [...] }). Every player reads it on start. - Public leaderboard. Owner appends
gameData.set('leaderboard.top10', [...])on a cron via an external script (advanced). - Game-internal config the agent might tune without rebuilding — difficulty curves, item drop tables, level definitions.
Example flow — high-score persistence
Tell the agent:
Remember the player's all-time high score. Show "High score: X" in the
HUD top-right. When the player dies, compare current score to the high
score and update if it's higher.Behind the scenes the agent writes something like:
// On scene start
const highScore = (await saves.get('highScore')) ?? 0;
scene.registry.set('highScore', highScore);
// On player death
const finalScore = scene.registry.get('score');
const currentHigh = scene.registry.get('highScore');
if (finalScore > currentHigh) {
await saves.set('highScore', finalScore);
scene.registry.set('highScore', finalScore);
}The HUD's high-score text widget is bound to registry 'highScore', so
it updates the moment the registry value changes.
Example flow — settings persistence
Add a settings menu accessible from a gear icon in the HUD. Sliders for
master volume (0-100), music volume (0-100), and a toggle for sound
effects. Persist the settings across sessions.Agent wires:
const settings = (await saves.get('settings')) ?? {
masterVolume: 80,
musicVolume: 60,
sfxEnabled: true,
};
// ... apply settings ...
// On change in the settings UI
await saves.set('settings', settings);Anonymous-friendly games
If your game's design assumes players can play without signing up (arcade-style, embedded, one-time experiences), the localStorage fallback gives them a save that works for them — they just lose it if they clear their browser data or switch devices.
If your game's design requires identity (cross-device progress, leaderboards, real names), tell the agent:
Require sign-in to play. Show a "Sign in to play" splash if the player
is anonymous.The agent reads the user from the SDK handshake and gates accordingly.
What about save slots?
For "Save 1 / Save 2 / Save 3" UI, use namespaced keys:
saves.set('slot1', { progress: ... })
saves.set('slot2', { progress: ... })
saves.set('slot3', { progress: ... })The agent can wire a save-slot menu that lists keys via a known prefix and lets the player overwrite any slot.
Limits and guarantees
- Size: each value can be up to ~64 KB; the per-player total is capped at ~1 MB per game. If you need more, talk to support.
- Rate: 20 RPC/sec per iframe per player.
- Durability: signed-in saves are stored server-side with daily backups. localStorage saves live on the player's device only.
- Privacy: per-player saves are visible only to that player; they
are NOT visible to the game owner. If you want owner-visible data,
build it through
gameData.*instead.
Security model
Save calls never pass through the running game code. The game iframe posts an RPC to Umicat's host, which validates the call against the player's session and only then hits the backend. Tokens never reach your game code.
This means you don't have to (and shouldn't) try to authenticate things yourself in game logic — trust the SDK's API surface.