关于 phaser-saveload
phaser-saveload 是用于持久化 Phaser 4 游戏状态的技能,涵盖浏览器存储选择、存档组织、版本迁移、自动保存及云存档架构概要。
主要功能
- 01localStorage 与 IndexedDB 模式
- 02类型化存档数据和默认值
- 03多槽位存档组织
- 04根据游戏事件自动保存
- 05存档版本管理与迁移
- 06云存档架构概要
适用场景
- 01保存玩家进度与设置
- 02管理多个存档槽位
- 03保持旧存档兼容
- 04规划基于后端的云存档
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yakoub-ai/ phaser4-gamedev --skill phaser-saveload安装后在 Claude Code 中输入/phaser-saveload确认已加载。
附带文件references/save-patterns.md
详细介绍
Phaser 4 存档与加载
Phaser 没有内置的存档系统。可以使用 localStorage(简单、同步且始终可用)或 IndexedDB(异步,可存储更大的数据)持久化游戏状态。本指南涵盖这两种方式,以及迁移、多存档槽和自动存档模式。
保存什么,重建什么
保存以下内容:
- 玩家进度:当前关卡、分数、物品栏、货币
- 标记:已查看的教程、已解锁的区域、已获得的成就
- 设置:音量、全屏偏好、无障碍选项
- 元数据:上次存档时间戳、存档版本号、总游戏时长
从存档数据重建(不要直接序列化):
- 场景状态 — 重启场景,并在
create()中重新应用已保存的数据 - 实体位置 — 只保存有意义的状态(例如
bossDefeated: true),不要保存原始坐标 - 物理实体 — 永远无法序列化;根据已保存的配置重新创建
永远不要保存以下内容:
Phaser.GameObjects.*实例(精灵、图像、文本等)Phaser.Physics.*实体或世界- 场景引用或任何包含循环引用的内容
localStorage — 简单存档
localStorage 是同步的,只能存储字符串,容量限制约为 5 MB。它适合用于游戏存档。
// Save
localStorage.setItem('game-save', JSON.stringify(saveData));
// Load
const raw = localStorage.getItem('game-save');
const saveData: SaveData | null = raw ? JSON.parse(raw) : null;
// Delete
localStorage.removeItem('game-save');
// Check if a save exists
const hasSave = localStorage.getItem('game-save') !== null;
始终在 try/catch 中执行 JSON.parse。损坏或过时的存档数据会导致错误,游戏必须能够优雅地恢复。
SaveData 接口
为存档数据定义类型化接口。每个字段都需要默认值。
interface SaveData {
version: number; // increment when save schema changes
level: number; // current level index (1-based)
score: number; // current run score
hiScore: number; // all-time high score
totalPlayTime: number; // total seconds played
settings: {
musicVolume: number; // 0–1
sfxVolume: number; // 0–1
fullscreen: boolean;
};
unlockedLevels: number[]; // array of unlocked level indices
flags: Record<string, boolean>; // flexible flags: tutorials, events, etc.
lastSaved: number; // Date.now() timestamp
}
const DEFAULT_SAVE: SaveData = {
version: 1,
level: 1,
score: 0,
hiScore: 0,
totalPlayTime: 0,
settings: { musicVolume: 0.7, sfxVolume: 1.0, fullscreen: false },
unlockedLevels: [1],
flags: {},
lastSaved: 0,
};
SaveManager 类
使用静态 SaveManager 集中处理所有存档和加载逻辑,并管理版本控制。
const SAVE_KEY = 'my-game-v1';
class SaveManager {
/** Load save data. Returns DEFAULT_SAVE if none exists or data is corrupt. */
static load(): SaveData {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return { ...DEFAULT_SAVE };
const data = JSON.parse(raw) as SaveData;
return SaveManager.migrate(data);
} catch {
console.warn('SaveManager: failed to parse save data, using defaults');
return { ...DEFAULT_SAVE };
}
}
/** Merge partial update into existing save and write to localStorage. */
static save(partial: Partial<SaveData>): void {
const current = SaveManager.load();
const updated: SaveData = { ...current, ...partial, lastSaved: Date.now() };
// Always update hi-score
if ((partial.score ?? 0) > current.hiScore) {
updated.hiScore = partial.score!;
}
localStorage.setItem(SAVE_KEY, JSON.stringify(updated));
}
/** Delete all saved data. */
static delete(): void {
localStorage.removeItem(SAVE_KEY);
}
/** Check whether a save exists. */
static exists(): boolean {
return localStorage.getItem(SAVE_KEY) !== null;
}
/** Migrate old save formats to the current version. */
static migrate(data: SaveData): SaveData {
let d = { ...data };
// v1 → v2: added `flags` field
if (d.version < 2) {
d.flags = {};
d.version = 2;
}
// v2 → v3: added `totalPlayTime`
if (d.version < 3) {
d.totalPlayTime = 0;
d.version = 3;
}
// Always ensure all required fields have defaults (defensive merge)
return { ...DEFAULT_SAVE, ...d };
}
}
多存档槽
type SlotId = 1 | 2 | 3;
class SlotSaveManager {
private static key(slot: SlotId): string {
return `my-game-slot-${slot}`;
}
static load(slot: SlotId): SaveData {
try {
const raw = localStorage.getItem(SlotSaveManager.key(slot));
if (!raw) return { ...DEFAULT_SAVE };
return SaveManager.migrate(JSON.parse(raw) as SaveData);
} catch {
return { ...DEFAULT_SAVE };
}
}
static save(slot: SlotId, partial: Partial<SaveData>): void {
const current = SlotSaveManager.load(slot);
const updated = { ...current, ...partial, lastSaved: Date.now() };
localStorage.setItem(SlotSaveManager.key(slot), JSON.stringify(updated));
}
static delete(slot: SlotId): void {
localStorage.removeItem(SlotSaveManager.key(slot));
}
/** Get metadata for all slots (for save select screen). */
static listSlots(): Array<{ slot: SlotId; data: SaveData | null }> {
return ([1, 2, 3] as SlotId[]).map(slot => ({
slot,
data: localStorage.getItem(SlotSaveManager.key(slot))
? SlotSaveManager.load(slot)
: null,
}));
}
}
自动存档模式
在有意义的游戏事件发生时自动存档:关卡完成、到达检查点、场景关闭,以及按计时器定时存档。
class GameScene extends Phaser.Scene {
private currentLevel = 1;
private lastAutoSave = 0;
create(): void {
// Auto-save on scene shutdown (handles Alt+F4, tab close, etc.)
this.events.on(Phaser.Scenes.Events.SHUTDOWN, () => this.autoSave());
this.events.on(Phaser.Scenes.Events.PAUSE, () => this.autoSave());
// Periodic auto-save every 5 minutes
this.time.addEvent({
delay: 300_000,
callback: this.autoSave,
callbackScope: this,
loop: true,
});
}
private autoSave(): void {
SaveManager.save({
level: this.currentLevel,
score: this.registry.get('score') as number,
totalPlayTime: (this.registry.get('totalPlayTime') as number) +
Math.floor((Date.now() - this.lastAutoSave) / 1000),
});
this.lastAutoSave = Date.now();
}
private onLevelComplete(): void {
this.currentLevel++;
SaveManager.save({
level: this.currentLevel,
unlockedLevels: [...(SaveManager.load().unlockedLevels), this.currentLevel],
});
// Also save immediately on meaningful event
this.autoSave();
}
}
Registry + SaveManager 集成
使用 Phaser 的 this.registry 作为内存中的运行时状态,并使用 SaveManager 作为持久化存储。在启动和关闭时进行同步。
// --- In BootScene or GameScene.create() ---
// Load save → populate registry
const save = SaveManager.load();
this.registry.set('score', save.score);
this.registry.set('hiScore', save.hiScore);
this.registry.set('level', save.level);
this.registry.set('musicVolume', save.settings.musicVolume);
this.registry.set('sfxVolume', save.settings.sfxVolume);
// Apply settings immediately
this.sound.volume = save.settings.musicVolume;
// --- On game over / scene end ---
// Read registry → save
SaveManager.save({
score: this.registry.get('score') as number,
level: this.registry.get('level') as number,
});
为什么使用 registry 作为中间层?
Phaser 的 registry 在各个场景间共享(可在任何地方通过 this.registry 访问)。它避免每帧读取 localStorage,并确保所有场景都能看到相同的实时状态。
设置:与游戏进度分开保存
将设置(音量、全屏)保存在单独的 localStorage 键中,这样重置游戏进度时就不会丢失设置。
const SETTINGS_KEY = 'my-game-settings';
interface GameSettings {
musicVolume: number;
sfxVolume: number;
fullscreen: boolean;
}
const DEFAULT_SETTINGS: GameSettings = {
musicVolume: 0.7,
sfxVolume: 1.0,
fullscreen: false,
};
class SettingsManager {
static load(): GameSettings {
try {
const raw = localStorage.getItem(SETTINGS_KEY);
return raw ? { ...DEFAULT_SETTINGS, ...JSON.parse(raw) } : { ...DEFAULT_SETTINGS };
} catch {
return { ...DEFAULT_SETTINGS };
}
}
static save(settings: Partial<GameSettings>): void {
const current = SettingsManager.load();
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ ...current, ...settings }));
}
}
最高分榜
存储一个分数条目数组。只保留前 N 个条目,并按分数降序排列。
interface ScoreEntry {
name: string;
score: number;
date: string; // ISO string
}
const HISCORE_KEY = 'my-game-hiscores';
const MAX_ENTRIES = 10;
class HiScoreManager {
static load(): ScoreEntry[] {
try {
const raw = localStorage.getItem(HISCORE_KEY);
return raw ? JSON.parse(raw) as ScoreEntry[] : [];
} catch {
return [];
}
}
static submit(name: string, score: number): ScoreEntry[] {
const entries = HiScoreManager.load();
entries.push({ name, score, date: new Date().toISOString() });
const sorted = entries
.sort((a, b) => b.score - a.score)
.slice(0, MAX_ENTRIES);
localStorage.setItem(HISCORE_KEY, JSON.stringify(sorted));
return sorted;
}
static isHighScore(score: number): boolean {
const entries = HiScoreManager.load();
if (entries.length < MAX_ENTRIES) return true;
return score > (entries[MAX_ENTRIES - 1]?.score ?? 0);
}
}
存档数据版本控制
始终包含 version 字段和 migrate() 函数,以避免新版本发布后旧存档无法使用。
- 不要从存档中移除字段 — 将其标记为已弃用并停止使用
- 不要更改字段类型 — 改为添加新字段
- 添加新的必填字段时,在
migrate()中提供默认值 - 将
version存储为整数,并在架构变更时递增
云存档架构(概述)
云存档需要后端。Phaser 不提供云存档 API。
模式:
- 保存时:发送
POST /api/save,请求体包含会话令牌和JSON.stringify(saveData) - 加载时:使用会话令牌请求
GET /api/save→ 解析响应体 - 离线回退:如果 fetch 失败,则回退到
localStorage - 冲突解决:比较
lastSaved时间戳;使用最新的存档
冲突解决说明:“最后写入者胜出”简单明了,适用于大多数单人游戏。对于合作游戏或跨设备同时编辑的情况,则需要合并策略。
更多资源
参考文件
references/save-patterns.md— 完整的 TypeScript SaveManager(含泛型)、SettingsManager、HiScoreManager、存档迁移系统、基础混淆(btoa/atob),以及用于大型数据的 IndexedDB 模式