LocalStorage Game Save in Phaser: A Working Example
LocalStorage Game Save in Phaser: A Working Example
Direct answer: Use localStorage for anything small and key-based — best scores, settings, unlocked levels, per-mode stats. Wrap it in one Storage module with typed getters/setters instead of calling localStorage.setItem all over your scenes. It syncs automatically to Phaser’s this.registry if you need reactive updates.
Merge Fish 2048 saves all persistent state this way. Here is the pattern that works.
1. The key map: one source of truth
Never scatter string keys through your code. Define them once:
export const STORAGE_KEYS = {
BEST_SCORE: 'mergefish_best_score',
MUSIC_ON: 'mergefish_music_on',
SFX_ON: 'mergefish_sfx_on',
GAME_STATS: 'mergefish_game_stats', // per-mode play counts
};
A typo like mergefish_best_socre silently creates a second, empty key and your “best score” feature just stops persisting. Centralizing keys turns that class of bug from invisible into impossible.
2. The Storage wrapper
export const Storage = {
get(key, fallback = null) {
try {
const v = localStorage.getItem(key);
return v === null ? fallback : JSON.parse(v);
} catch { return fallback; }
},
set(key, value) {
try { localStorage.setItem(key, JSON.stringify(value)); }
catch { /* private mode / quota — fail soft */ }
},
};
Two details matter:
- JSON serialization everywhere. Numbers stored via
JSON.stringifycome back as numbers. Store5, andget('best_score') + 1is6, not"51". - Fail soft. In Safari private mode or with storage disabled,
setItemthrows. Our wrapper catches it so the game still runs — it just doesn’t persist.
3. Where it hooks into gameplay
- Best score: written at game over, read on menu, shown on rank screen. One module, three scenes, zero duplication.
- Settings:
MusicScene-independent —AudioManagerreadsstorage.get('music_on')at boot and applies it before the first note plays. Players who muted you yesterday should not be blasted today. - Per-mode stats:
GAME_STATSholds an object like{ endless: { plays: 3, best: 640 }, level: {...} }. One key, one JSON object — no key explosion.
4. When localStorage is not enough
| Need | Use |
|---|---|
| Small key-value state | localStorage (this article) |
| Bigger structured data (levels, map saves) | IndexedDB |
| Sync across devices | A backend / leaderboard API |
| Save encryption | Not worth it client-side — see our encryption article |
If you store images or entire level maps, localStorage’s 5MB limit will bite. For scores and settings, it is exactly right.
Common mistakes
JSON.parseon raw values:localStoragestores strings. Parse on read, stringify on write — or use a wrapper like above.- Writing on every change: on a busy game frame this thrashes the storage engine. Write on discrete events: game over, settings toggle, level complete.
- Forgetting the try/catch: private browsing mode is where save features die silently. Wrap every access.
Related reading
- Phaser Scene Management: How We Structure 9 Scenes
- IndexedDB vs localStorage for Game Saves
- High Score Leaderboards for Web Games
Written by ruofan, independent HTML5 game developer. Keys and patterns come from Merge Fish 2048 (Phaser 3.90 + Vite). API reference: MDN localStorage.