强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Pixi.js 游戏开发教程 / Pixi.js 存档系统(LocalStorage)

存档系统(LocalStorage)

为你的 Pixi.js 游戏实现完整的本地存档与云端存档功能。


一、Web Storage 基础

1.1 LocalStorage vs SessionStorage

特性LocalStorageSessionStorage
生命周期永久,除非手动清除标签页关闭即清除
存储容量~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

特性LocalStorageIndexedDB
容量~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 + 在线同步

扩展阅读