Pixi.js 游戏开发教程 / Pixi.js 存档系统(LocalStorage)
存档系统(LocalStorage)
为你的 Pixi.js 游戏实现完整的本地存档与云端存档功能。
一、Web Storage 基础
1.1 LocalStorage vs SessionStorage
| 特性 | LocalStorage | SessionStorage |
|---|
| 生命周期 | 永久,除非手动清除 | 标签页关闭即清除 |
| 存储容量 | ~5-10 MB | ~5 MB |
| 同源共享 | 同域下所有页面共享 | 仅当前标签页 |
| 适用场景 | 游戏存档、用户设置 | 临时状态、会话数据 |
1.2 基本操作
// 存储
localStorage.setItem('playerName', '勇者');
localStorage.setItem('level', '3');
// 读取
const name = localStorage.getItem('playerName'); // '勇者'
const level = parseInt(localStorage.getItem('level') || '1');
// 删除
localStorage.removeItem('playerName');
// 清空所有
localStorage.clear();
// 获取键名
const key = localStorage.key(0); // 第一个键名
// 总条目数
console.log('存档数量:', localStorage.length);
⚠️ 注意:LocalStorage 只能存储字符串,对象必须序列化。
二、JSON 序列化游戏状态
2.1 定义存档数据结构
interface SaveData {
version: number; // 存档版本,用于迁移
timestamp: number; // 存档时间
playTime: number; // 游戏时长(秒)
player: {
name: string;
level: number;
hp: number;
maxHp: number;
x: number;
y: number;
inventory: { id: string; count: number }[];
equipped: Record<string, string>;
};
progress: {
currentStage: number;
completedStages: number[];
score: number;
coins: number;
};
settings: {
musicVolume: number;
sfxVolume: number;
language: string;
};
}
2.2 保存与读取
class SaveManager {
private readonly STORAGE_KEY = 'game_save';
private readonly CURRENT_VERSION = 2;
save(data: SaveData): boolean {
try {
data.version = this.CURRENT_VERSION;
data.timestamp = Date.now();
const json = JSON.stringify(data);
localStorage.setItem(this.STORAGE_KEY, json);
console.log(`存档成功,大小: ${json.length} 字节`);
return true;
} catch (e) {
console.error('存档失败:', e);
return false;
}
}
load(): SaveData | null {
try {
const json = localStorage.getItem(this.STORAGE_KEY);
if (!json) return null;
const data = JSON.parse(json) as SaveData;
// 版本迁移
return this.migrate(data);
} catch (e) {
console.error('读取存档失败:', e);
return null;
}
}
delete(): void {
localStorage.removeItem(this.STORAGE_KEY);
}
exists(): boolean {
return localStorage.getItem(this.STORAGE_KEY) !== null;
}
private migrate(data: SaveData): SaveData {
// 版本 1 → 版本 2:添加 equipped 字段
if (data.version < 2) {
(data.player as any).equipped = {};
data.version = 2;
}
// 未来版本迁移在这里添加
return data;
}
}
三、IndexedDB 大容量存储
3.1 为什么使用 IndexedDB
| 特性 | LocalStorage | IndexedDB |
|---|
| 容量 | ~5 MB | 数百 MB 甚至更大 |
| API | 同步 | 异步(不阻塞主线程) |
| 数据类型 | 字符串 | 任意 JS 对象 |
| 索引 | 无 | 支持索引查询 |
| 事务 | 无 | 支持事务 |
3.2 IndexedDB 封装
class IndexedDBStorage {
private dbName: string;
private storeName: string;
private db: IDBDatabase | null = null;
constructor(dbName = 'GameDB', storeName = 'saves') {
this.dbName = dbName;
this.storeName = storeName;
}
async open(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, { keyPath: 'id' });
}
};
request.onsuccess = (event) => {
this.db = (event.target as IDBOpenDBRequest).result;
resolve();
};
request.onerror = () => reject(request.error);
});
}
async save(id: string, data: any): Promise<void> {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
const request = store.put({ id, data, timestamp: Date.now() });
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async load(id: string): Promise<any | null> {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(this.storeName, 'readonly');
const store = tx.objectStore(this.storeName);
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result?.data ?? null);
};
request.onerror = () => reject(request.error);
});
}
async delete(id: string): Promise<void> {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async listAll(): Promise<string[]> {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(this.storeName, 'readonly');
const store = tx.objectStore(this.storeName);
const request = store.getAllKeys();
request.onsuccess = () => resolve(request.result as string[]);
request.onerror = () => reject(request.error);
});
}
}
四、存档槽管理(多存档)
4.1 多存档系统
class SlotSaveManager {
private storageKey = 'game_saves';
private maxSlots = 3;
// 获取所有存档槽
getSlots(): (SaveData | null)[] {
const raw = localStorage.getItem(this.storageKey);
if (!raw) return new Array(this.maxSlots).fill(null);
const slots = JSON.parse(raw) as (SaveData | null)[];
// 确保长度正确
while (slots.length < this.maxSlots) slots.push(null);
return slots;
}
// 保存到指定槽位
saveToSlot(slotIndex: number, data: SaveData): boolean {
if (slotIndex < 0 || slotIndex >= this.maxSlots) return false;
const slots = this.getSlots();
data.timestamp = Date.now();
slots[slotIndex] = data;
try {
localStorage.setItem(this.storageKey, JSON.stringify(slots));
return true;
} catch (e) {
console.error('存档失败:', e);
return false;
}
}
// 从指定槽位读取
loadFromSlot(slotIndex: number): SaveData | null {
const slots = this.getSlots();
return slots[slotIndex] ?? null;
}
// 删除指定槽位
deleteSlot(slotIndex: number): void {
const slots = this.getSlots();
slots[slotIndex] = null;
localStorage.setItem(this.storageKey, JSON.stringify(slots));
}
// 获取存档摘要(用于 UI 显示)
getSlotSummary(slotIndex: number): string {
const data = this.loadFromSlot(slotIndex);
if (!data) return '空存档';
const date = new Date(data.timestamp).toLocaleString();
const playTime = Math.floor(data.playTime / 60);
return `Lv.${data.player.level} | ${playTime}分钟 | ${date}`;
}
}
4.2 存档槽 UI
import { Container, Text, Graphics } from 'pixi.js';
class SaveSlotUI extends Container {
private slots: Container[] = [];
private manager: SlotSaveManager;
constructor(app: Application) {
super();
this.manager = new SlotSaveManager();
// 标题
const title = new Text({ text: '选择存档', style: { fill: 0xffffff, fontSize: 32 } });
title.anchor.set(0.5, 0);
title.x = app.screen.width / 2;
title.y = 40;
this.addChild(title);
// 创建 3 个存档槽
for (let i = 0; i < 3; i++) {
const slot = this.createSlot(i, app.screen.width);
slot.y = 120 + i * 100;
this.addChild(slot);
this.slots.push(slot);
}
}
private createSlot(index: number, screenWidth: number): Container {
const container = new Container();
// 背景
const bg = new Graphics();
bg.roundRect(0, 0, 400, 80, 8).fill({ color: 0x333333 });
bg.x = (screenWidth - 400) / 2;
container.addChild(bg);
// 槽位编号
const label = new Text({
text: `存档 ${index + 1}`,
style: { fill: 0xffffff, fontSize: 18 },
});
label.x = bg.x + 16;
label.y = 10;
container.addChild(label);
// 存档信息
const info = new Text({
text: this.manager.getSlotSummary(index),
style: { fill: 0xcccccc, fontSize: 14 },
});
info.x = bg.x + 16;
info.y = 40;
container.addChild(info);
// 点击交互
const hitArea = new Graphics();
hitArea.roundRect(bg.x, 0, 400, 80, 8).fill({ color: 0xffffff, alpha: 0 });
hitArea.eventMode = 'static';
hitArea.cursor = 'pointer';
hitArea.on('pointertap', () => {
this.onSlotClick(index);
});
container.addChild(hitArea);
return container;
}
private onSlotClick(index: number) {
const data = this.manager.loadFromSlot(index);
if (data) {
// 加载存档
loadGameState(data);
} else {
// 新游戏
startNewGame(index);
}
}
}
五、自动存档
5.1 自动存档触发
class AutoSave {
private manager: SaveManager;
private intervalId: number | null = null;
private getState: () => SaveData;
constructor(getState: () => SaveData) {
this.manager = new SaveManager();
this.getState = getState;
}
// 定时自动存档
startInterval(minutes: number = 5) {
this.stopInterval();
this.intervalId = window.setInterval(() => {
this.trigger();
}, minutes * 60 * 1000);
}
stopInterval() {
if (this.intervalId !== null) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
// 关键节点存档
trigger() {
const state = this.getState();
const success = this.manager.save(state);
if (success) {
showNotification('游戏已自动保存');
}
}
}
// 使用
const autoSave = new AutoSave(() => currentGameState);
autoSave.startInterval(3); // 每 3 分钟自动存档
// 在关键节点触发
function onBossDefeated() {
autoSave.trigger();
}
function onStageClear() {
autoSave.trigger();
}
// 页面关闭前存档
window.addEventListener('beforeunload', () => {
autoSave.trigger();
});
5.2 存档提示 UI
import { Text, Container } from 'pixi.js';
class SaveIndicator extends Container {
private text: Text;
private fadeTimer: number = 0;
constructor() {
super();
this.text = new Text({
text: '💾 已保存',
style: { fill: 0x00ff00, fontSize: 16 },
});
this.text.alpha = 0;
this.addChild(this.text);
}
show(message: string = '💾 已保存') {
this.text.text = message;
this.text.alpha = 1;
this.fadeTimer = 2; // 2 秒后淡出
}
update(delta: number) {
if (this.fadeTimer > 0) {
this.fadeTimer -= delta;
if (this.fadeTimer <= 0.5) {
this.text.alpha = this.fadeTimer / 0.5;
}
if (this.fadeTimer <= 0) {
this.text.alpha = 0;
}
}
}
}
六、存档压缩
6.1 JSON 压缩策略
// 移除不必要的字段,使用短键名
function compressSaveData(data: SaveData): string {
const compressed = {
v: data.version,
t: data.timestamp,
pt: data.playTime,
p: {
n: data.player.name,
l: data.player.level,
h: data.player.hp,
mh: data.player.maxHp,
x: Math.round(data.player.x),
y: Math.round(data.player.y),
inv: data.player.inventory,
},
g: {
s: data.progress.currentStage,
cs: data.progress.completedStages,
sc: data.progress.score,
c: data.progress.coins,
},
};
return JSON.stringify(compressed);
}
// LZ-String 压缩(推荐)
// npm install lz-string
import LZString from 'lz-string';
function saveCompressed(key: string, data: SaveData) {
const json = JSON.stringify(data);
const compressed = LZString.compressToUTF16(json);
localStorage.setItem(key, compressed);
// 对比大小
console.log(`原始: ${json.length} 字节, 压缩后: ${compressed.length} 字节`);
console.log(`压缩率: ${(compressed.length / json.length * 100).toFixed(1)}%`);
}
function loadCompressed(key: string): SaveData | null {
const compressed = localStorage.getItem(key);
if (!compressed) return null;
const json = LZString.decompressFromUTF16(compressed);
return JSON.parse(json!);
}
七、云端存档接口设计
7.1 云端存档 API
interface CloudSaveAPI {
upload(userId: string, slot: number, data: SaveData): Promise<boolean>;
download(userId: string, slot: number): Promise<SaveData | null>;
listSlots(userId: string): Promise<{ slot: number; timestamp: number }[]>;
delete(userId: string, slot: number): Promise<boolean>;
}
class CloudSaveClient implements CloudSaveAPI {
private baseUrl: string;
private token: string;
constructor(baseUrl: string, token: string) {
this.baseUrl = baseUrl;
this.token = token;
}
async upload(userId: string, slot: number, data: SaveData): Promise<boolean> {
const response = await fetch(`${this.baseUrl}/saves/${userId}/${slot}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`,
},
body: JSON.stringify(data),
});
return response.ok;
}
async download(userId: string, slot: number): Promise<SaveData | null> {
const response = await fetch(`${this.baseUrl}/saves/${userId}/${slot}`, {
headers: { 'Authorization': `Bearer ${this.token}` },
});
if (!response.ok) return null;
return response.json();
}
async listSlots(userId: string): Promise<{ slot: number; timestamp: number }[]> {
const response = await fetch(`${this.baseUrl}/saves/${userId}`, {
headers: { 'Authorization': `Bearer ${this.token}` },
});
return response.json();
}
async delete(userId: string, slot: number): Promise<boolean> {
const response = await fetch(`${this.baseUrl}/saves/${userId}/${slot}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${this.token}` },
});
return response.ok;
}
}
7.2 本地 + 云端同步
class SyncSaveManager {
private local: SlotSaveManager;
private cloud: CloudSaveClient;
private userId: string;
constructor(userId: string, cloud: CloudSaveClient) {
this.local = new SlotSaveManager();
this.cloud = cloud;
this.userId = userId;
}
// 保存时同时上传云端
async save(slot: number, data: SaveData): Promise<boolean> {
const localOk = this.local.saveToSlot(slot, data);
if (!localOk) return false;
// 异步上传,不阻塞游戏
this.cloud.upload(this.userId, slot, data).catch(err => {
console.warn('云端同步失败:', err);
});
return true;
}
// 加载时优先本地,本地没有则从云端下载
async load(slot: number): Promise<SaveData | null> {
let data = this.local.loadFromSlot(slot);
if (!data) {
data = await this.cloud.download(this.userId, slot);
if (data) {
this.local.saveToSlot(slot, data);
}
}
return data;
}
}
八、存档迁移
8.1 版本迁移系统
type Migration = (data: any) => any;
const migrations: Record<number, Migration> = {
// v1 → v2: 添加装备系统
1: (data) => {
data.player.equipped = {};
data.version = 2;
return data;
},
// v2 → v3: 重构背包格式
2: (data) => {
data.player.inventory = data.player.inventory.map((item: any) => ({
id: item.itemId,
count: item.quantity,
}));
data.version = 3;
return data;
},
};
function migrateSave(data: SaveData, targetVersion: number): SaveData {
let current = data.version;
while (current < targetVersion) {
const migration = migrations[current];
if (!migration) {
throw new Error(`找不到版本 ${current} 的迁移函数`);
}
current = migration(data).version;
}
return data;
}
九、完整 SaveManager 实现
class GameSaveManager {
private static instance: GameSaveManager;
private local: SlotSaveManager;
private autoSave: AutoSave;
private db: IndexedDBStorage;
static getInstance(): GameSaveManager {
if (!this.instance) {
this.instance = new GameSaveManager();
}
return this.instance;
}
constructor() {
this.local = new SlotSaveManager();
this.db = new IndexedDBStorage();
this.autoSave = new AutoSave(() => this.captureGameState());
}
// 捕获当前游戏状态
private captureGameState(): SaveData {
return {
version: 2,
timestamp: Date.now(),
playTime: gamePlayTime,
player: {
name: player.name,
level: player.level,
hp: player.hp,
maxHp: player.maxHp,
x: player.x,
y: player.y,
inventory: player.inventory.serialize(),
equipped: player.equipped.serialize(),
},
progress: {
currentStage: progressManager.currentStage,
completedStages: progressManager.completed,
score: gameScore,
coins: playerCoins,
},
settings: {
musicVolume: audioManager.musicVolume,
sfxVolume: audioManager.sfxVolume,
language: i18n.language,
},
};
}
// 保存到槽位
saveToSlot(slot: number): boolean {
const state = this.captureGameState();
return this.local.saveToSlot(slot, state);
}
// 从槽位加载
loadFromSlot(slot: number): boolean {
const data = this.local.loadFromSlot(slot);
if (!data) return false;
this.applyGameState(data);
return true;
}
// 应用存档数据到游戏
private applyGameState(data: SaveData) {
player.name = data.player.name;
player.level = data.player.level;
player.hp = data.player.hp;
player.maxHp = data.player.maxHp;
player.x = data.player.x;
player.y = data.player.y;
player.inventory.deserialize(data.player.inventory);
player.equipped.deserialize(data.player.equipped);
progressManager.currentStage = data.progress.currentStage;
progressManager.completed = data.progress.completedStages;
gameScore = data.progress.score;
playerCoins = data.progress.coins;
audioManager.musicVolume = data.settings.musicVolume;
audioManager.sfxVolume = data.settings.sfxVolume;
i18n.language = data.settings.language;
}
// 启动自动存档
startAutoSave(intervalMinutes: number = 3) {
this.autoSave.startInterval(intervalMinutes);
}
// 设置保存
saveSettings(settings: SaveData['settings']) {
localStorage.setItem('game_settings', JSON.stringify(settings));
}
loadSettings(): SaveData['settings'] | null {
const raw = localStorage.getItem('game_settings');
return raw ? JSON.parse(raw) : null;
}
}
游戏开发场景
| 场景 | 存储方案 |
|---|
| 玩家进度存档 | LocalStorage 多槽位 |
| 大型地图数据 | IndexedDB |
| 游戏设置 | LocalStorage 单独键 |
| 排行榜缓存 | SessionStorage |
| 离线数据队列 | IndexedDB + 在线同步 |
扩展阅读