pin_drop当前位置:知识文库 ❯ 图文

游戏开发:游戏状态管理 - 从入门到实践详解

教程简介

游戏状态管理是游戏架构中最核心的部分之一,它决定了游戏在不同阶段(菜单、游戏中、暂停、结束等)的行为和表现。良好的状态管理能让游戏逻辑清晰、易于维护和扩展。本教程将全面讲解游戏状态机、场景管理、存档系统、关卡设计、游戏配置、UI状态和过渡动画,帮助你构建结构清晰、可维护的游戏状态系统。

核心概念

游戏状态机

游戏状态机(FSM)是最基本的状态管理模式,定义了游戏可以处于的状态以及状态之间的转换规则:

代码示例

class GameStateMachine {
    constructor() {
        this.states = new Map();
        this.currentState = null;
        this.currentStateName = '';
    }

    addState(name, state) {
        this.states.set(name, state);
    }

    changeState(name, data) {
        if (this.currentState && this.currentState.exit) {
            this.currentState.exit();
        }
        this.currentStateName = name;
        this.currentState = this.states.get(name);
        if (this.currentState && this.currentState.enter) {
            this.currentState.enter(data);
        }
    }

    update(dt) {
        if (this.currentState && this.currentState.update) {
            this.currentState.update(dt);
        }
    }

    render(ctx) {
        if (this.currentState && this.currentState.render) {
            this.currentState.render(ctx);
        }
    }
}

// 使用示例
const fsm = new GameStateMachine();
fsm.addState('menu', {
    enter: () => { console.log('进入菜单'); },
    update: (dt) => { /* 菜单逻辑 */ },
    render: (ctx) => { /* 菜单渲染 */ },
    exit: () => { console.log('离开菜单'); }
});
fsm.addState('playing', {
    enter: (data) => { console.log('开始游戏', data); },
    update: (dt) => { /* 游戏逻辑 */ },
    render: (ctx) => { /* 游戏渲染 */ },
    exit: () => { console.log('暂停游戏'); }
});
fsm.addState('paused', { /* ... */ });
fsm.addState('gameover', { /* ... */ });

场景管理

场景管理将游戏划分为独立的场景,每个场景有自己的资源和逻辑:

代码示例

class SceneManager {
    constructor() {
        this.scenes = new Map();
        this.currentScene = null;
        this.transition = null;
    }

    register(name, scene) {
        this.scenes.set(name, scene);
    }

    loadScene(name, data) {
        const scene = this.scenes.get(name);
        if (!scene) return;

        if (this.currentScene && this.currentScene.unload) {
            this.currentScene.unload();
        }

        this.currentScene = scene;
        if (scene.load) scene.load(data);
    }

    update(dt) {
        if (this.transition) {
            this.transition.update(dt);
            if (this.transition.isComplete()) {
                this.transition = null;
            }
            return;
        }
        if (this.currentScene && this.currentScene.update) {
            this.currentScene.update(dt);
        }
    }

    render(ctx) {
        if (this.currentScene && this.currentScene.render) {
            this.currentScene.render(ctx);
        }
        if (this.transition) {
            this.transition.render(ctx);
        }
    }
}

存档系统

存档系统允许玩家保存和加载游戏进度:

代码示例

class SaveSystem {
    constructor(storageKey = 'game_save') {
        this.storageKey = storageKey;
    }

    save(data) {
        try {
            const json = JSON.stringify(data);
            localStorage.setItem(this.storageKey, json);
            return true;
        } catch (e) {
            console.error('保存失败:', e);
            return false;
        }
    }

    load() {
        try {
            const json = localStorage.getItem(this.storageKey);
            return json ? JSON.parse(json) : null;
        } catch (e) {
            console.error('加载失败:', e);
            return null;
        }
    }

    delete() {
        localStorage.removeItem(this.storageKey);
    }

    hasSave() {
        return localStorage.getItem(this.storageKey) !== null;
    }
}

关卡设计

关卡配置将关卡数据与代码分离,便于设计和调整:

代码示例

const levelData = {
    levels: [
        {
            id: 1,
            name: '初入森林',
            width: 2000,
            height: 600,
            background: 'forest',
            platforms: [
                { x: 0, y: 550, w: 500, h: 50 },
                { x: 300, y: 420, w: 150, h: 20 },
                { x: 600, y: 350, w: 200, h: 20 }
            ],
            enemies: [
                { type: 'slime', x: 400, y: 500 },
                { type: 'bat', x: 700, y: 200 }
            ],
            items: [
                { type: 'coin', x: 350, y: 380 },
                { type: 'potion', x: 650, y: 310 }
            ],
            exit: { x: 1800, y: 400 },
            timeLimit: 120
        }
    ]
};

过渡动画

场景切换时的过渡动画提升用户体验:

代码示例

class Transition {
    constructor(type, duration, callback) {
        this.type = type;
        this.duration = duration;
        this.elapsed = 0;
        this.callback = callback;
        this.halfReached = false;
    }

    update(dt) {
        this.elapsed += dt;
        if (this.elapsed >= this.duration / 2 && !this.halfReached) {
            this.halfReached = true;
            if (this.callback) this.callback();
        }
    }

    isComplete() {
        return this.elapsed >= this.duration;
    }

    render(ctx, width, height) {
        const progress = this.elapsed / this.duration;
        switch (this.type) {
            case 'fade':
                const alpha = progress < 0.5 ? progress * 2 : 2 - progress * 2;
                ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
                ctx.fillRect(0, 0, width, height);
                break;
            case 'wipe':
                const x = progress < 0.5 ? progress * 2 * width : 0;
                const w = progress < 0.5 ? width : (2 - progress * 2) * width;
                ctx.fillStyle = '#000';
                ctx.fillRect(x, 0, w, height);
                break;
        }
    }
}

语法与用法

完整的游戏状态管理演示

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>游戏状态管理</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #0a0a1a; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; font-family: 'Segoe UI', sans-serif; color: #fff; }
        canvas { border: 2px solid #2a2a4a; border-radius: 8px; }
    </style>
</head>
<body>
    <canvas id="gameCanvas" width="800" height="600"></canvas>
    <script>
        // ========== 过渡动画 ==========
        class TransitionEffect {
            constructor(type, duration) {
                this.type = type;
                this.duration = duration;
                this.elapsed = 0;
                this.phase = 'out'; // out: 渐出, in: 渐入
                this.onMidpoint = null;
                this._midpointCalled = false;
            }

            update(dt) {
                this.elapsed += dt;
                if (this.elapsed >= this.duration / 2 && !this._midpointCalled) {
                    this._midpointCalled = true;
                    this.phase = 'in';
                    if (this.onMidpoint) this.onMidpoint();
                }
            }

            isComplete() { return this.elapsed >= this.duration; }

            getProgress() { return Math.min(1, this.elapsed / this.duration); }

            render(ctx, w, h) {
                const p = this.getProgress();
                switch (this.type) {
                    case 'fade':
                        const alpha = p < 0.5 ? p * 2 : 2 - p * 2;
                        ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
                        ctx.fillRect(0, 0, w, h);
                        break;
                    case 'wipeRight':
                        if (p < 0.5) {
                            ctx.fillStyle = '#000';
                            ctx.fillRect(0, 0, p * 2 * w, h);
                        } else {
                            ctx.fillStyle = '#000';
                            ctx.fillRect((2 - p * 2) * w, 0, w, h);
                        }
                        break;
                    case 'circle':
                        const maxR = Math.sqrt(w * w + h * h) / 2;
                        if (p < 0.5) {
                            const r = maxR * (1 - p * 2);
                            ctx.fillStyle = '#000';
                            ctx.beginPath();
                            ctx.rect(0, 0, w, h);
                            ctx.arc(w / 2, h / 2, Math.max(0, r), 0, Math.PI * 2, true);
                            ctx.fill();
                        } else {
                            const r = maxR * (p * 2 - 1);
                            ctx.fillStyle = '#000';
                            ctx.beginPath();
                            ctx.rect(0, 0, w, h);
                            ctx.arc(w / 2, h / 2, Math.max(0, r), 0, Math.PI * 2, true);
                            ctx.fill();
                        }
                        break;
                }
            }
        }

        // ========== 存档系统 ==========
        class SaveSystem {
            constructor(key) { this.key = key; }
            save(data) { try { localStorage.setItem(this.key, JSON.stringify(data)); return true; } catch (e) { return false; } }
            load() { try { const d = localStorage.getItem(this.key); return d ? JSON.parse(d) : null; } catch (e) { return null; } }
            hasSave() { return !!localStorage.getItem(this.key); }
            delete() { localStorage.removeItem(this.key); }
        }

        // ========== 游戏状态机 ==========
        class GameStateMachine {
            constructor() {
                this.states = {};
                this.current = null;
                this.currentName = '';
                this.transition = null;
            }

            addState(name, state) { this.states[name] = state; }

            changeTo(name, data, transitionType) {
                if (transitionType) {
                    this.transition = new TransitionEffect(transitionType, 0.8);
                    this.transition.onMidpoint = () => {
                        this._switchState(name, data);
                    };
                } else {
                    this._switchState(name, data);
                }
            }

            _switchState(name, data) {
                if (this.current && this.current.exit) this.current.exit();
                this.currentName = name;
                this.current = this.states[name];
                if (this.current && this.current.enter) this.current.enter(data);
            }

            update(dt) {
                if (this.transition) {
                    this.transition.update(dt);
                    if (this.transition.isComplete()) this.transition = null;
                    return;
                }
                if (this.current && this.current.update) this.current.update(dt);
            }

            render(ctx, w, h) {
                if (this.current && this.current.render) this.current.render(ctx, w, h);
                if (this.transition) this.transition.render(ctx, w, h);
            }
        }

        // ========== 主游戏 ==========
        class Game {
            constructor() {
                this.canvas = document.getElementById('gameCanvas');
                this.ctx = this.canvas.getContext('2d');
                this.width = 800;
                this.height = 600;
                this.fsm = new GameStateMachine();
                this.save = new SaveSystem('state_demo_save');
                this.keys = {};
                this.mouseX = 0;
                this.mouseY = 0;
                this.lastTime = 0;
                this.gameTime = 0;

                // 游戏数据
                this.gameData = { score: 0, level: 1, lives: 3, coins: 0 };

                this.setupInput();
                this.setupStates();
                this.fsm.changeTo('menu');
            }

            setupInput() {
                document.addEventListener('keydown', (e) => {
                    this.keys[e.code] = true;
                    if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) e.preventDefault();
                });
                document.addEventListener('keyup', (e) => { this.keys[e.code] = false; });
                this.canvas.addEventListener('mousemove', (e) => {
                    const rect = this.canvas.getBoundingClientRect();
                    this.mouseX = (e.clientX - rect.left) * (this.width / rect.width);
                    this.mouseY = (e.clientY - rect.top) * (this.height / rect.height);
                });
                this.canvas.addEventListener('click', (e) => {
                    if (this.fsm.current && this.fsm.current.handleClick) {
                        this.fsm.current.handleClick(this.mouseX, this.mouseY);
                    }
                });
            }

            setupStates() {
                // ===== 菜单状态 =====
                this.fsm.addState('menu', {
                    enter: () => {},
                    update: (dt) => {
                        this.gameTime += dt;
                    },
                    render: (ctx, w, h) => {
                        // 背景
                        const grad = ctx.createLinearGradient(0, 0, 0, h);
                        grad.addColorStop(0, '#0a0a2a');
                        grad.addColorStop(1, '#1a1a4a');
                        ctx.fillStyle = grad;
                        ctx.fillRect(0, 0, w, h);

                        // 星星
                        for (let i = 0; i < 50; i++) {
                            const sx = (i * 137.5 + this.gameTime * 10) % w;
                            const sy = (i * 73.7) % h;
                            ctx.fillStyle = `rgba(255,255,255,${0.3 + Math.sin(this.gameTime + i) * 0.2})`;
                            ctx.beginPath();
                            ctx.arc(sx, sy, 1, 0, Math.PI * 2);
                            ctx.fill();
                        }

                        // 标题
                        ctx.fillStyle = '#e94560';
                        ctx.font = 'bold 48px Arial';
                        ctx.textAlign = 'center';
                        ctx.fillText('HTML5 游戏', w / 2, 150);

                        ctx.fillStyle = '#4a90d9';
                        ctx.font = '20px Arial';
                        ctx.fillText('状态管理演示', w / 2, 190);

                        // 按钮
                        this.drawButton(ctx, w / 2, 300, 200, 50, '开始游戏', '#4a90d9', 'start');
                        if (this.save.hasSave()) {
                            this.drawButton(ctx, w / 2, 370, 200, 50, '继续游戏', '#16c79a', 'continue');
                        }
                        this.drawButton(ctx, w / 2, this.save.hasSave() ? 440 : 370, 200, 50, '设置', '#f0a500', 'settings');

                        // 版本
                        ctx.fillStyle = '#444';
                        ctx.font = '12px monospace';
                        ctx.fillText('v1.0.0', w / 2, h - 20);
                    },
                    handleClick: (mx, my) => {
                        if (this.isButtonHit(mx, my, this.width / 2, 300, 200, 50)) {
                            this.gameData = { score: 0, level: 1, lives: 3, coins: 0 };
                            this.fsm.changeTo('playing', this.gameData, 'fade');
                        }
                        if (this.save.hasSave() && this.isButtonHit(mx, my, this.width / 2, 370, 200, 50)) {
                            this.gameData = this.save.load();
                            this.fsm.changeTo('playing', this.gameData, 'fade');
                        }
                        const settingsY = this.save.hasSave() ? 440 : 370;
                        if (this.isButtonHit(mx, my, this.width / 2, settingsY, 200, 50)) {
                            this.fsm.changeTo('settings', null, 'wipeRight');
                        }
                    }
                });

                // ===== 游戏状态 =====
                this.fsm.addState('playing', {
                    player: null,
                    coins: [],
                    enemies: [],
                    enter: (data) => {
                        if (data) this.gameData = { ...data };
                        this.fsm.states.playing.player = { x: 100, y: 400, vx: 0, vy: 0, w: 30, h: 40, onGround: false };
                        this.fsm.states.playing.coins = [];
                        this.fsm.states.playing.enemies = [];
                        for (let i = 0; i < 5 + this.gameData.level * 2; i++) {
                            this.fsm.states.playing.coins.push({
                                x: 150 + Math.random() * 600, y: 200 + Math.random() * 300,
                                collected: false, bobOffset: Math.random() * Math.PI * 2
                            });
                        }
                        for (let i = 0; i < this.gameData.level; i++) {
                            this.fsm.states.playing.enemies.push({
                                x: 300 + Math.random() * 400, y: 450, vx: 60 + Math.random() * 40,
                                dir: 1, w: 30, h: 30
                            });
                        }
                    },
                    update: (dt) => {
                        this.gameTime += dt;
                        const p = this.fsm.states.playing.player;
                        const speed = 200;
                        const jumpForce = -450;
                        const gravity = 900;

                        // 移动
                        if (this.keys['ArrowLeft'] || this.keys['KeyA']) p.vx = -speed;
                        else if (this.keys['ArrowRight'] || this.keys['KeyD']) p.vx = speed;
                        else p.vx *= 0.8;

                        if ((this.keys['ArrowUp'] || this.keys['KeyW'] || this.keys['Space']) && p.onGround) {
                            p.vy = jumpForce;
                            p.onGround = false;
                        }

                        p.vy += gravity * dt;
                        p.x += p.vx * dt;
                        p.y += p.vy * dt;

                        // 地面
                        if (p.y + p.h > 500) { p.y = 500 - p.h; p.vy = 0; p.onGround = true; }
                        p.x = Math.max(0, Math.min(p.x, this.width - p.w));

                        // 金币收集
                        for (const coin of this.fsm.states.playing.coins) {
                            if (coin.collected) continue;
                            const dx = p.x + p.w / 2 - coin.x;
                            const dy = p.y + p.h / 2 - coin.y;
                            if (Math.sqrt(dx * dx + dy * dy) < 25) {
                                coin.collected = true;
                                this.gameData.coins++;
                                this.gameData.score += 100;
                            }
                        }

                        // 敌人
                        for (const enemy of this.fsm.states.playing.enemies) {
                            enemy.x += enemy.vx * enemy.dir * dt;
                            if (enemy.x < 50 || enemy.x > this.width - 50) enemy.dir *= -1;

                            // 碰撞检测
                            if (p.x < enemy.x + enemy.w && p.x + p.w > enemy.x &&
                                p.y < enemy.y + enemy.h && p.y + p.h > enemy.y) {
                                this.gameData.lives--;
                                if (this.gameData.lives <= 0) {
                                    this.fsm.changeTo('gameover', this.gameData, 'circle');
                                } else {
                                    p.x = 100; p.y = 400; p.vx = 0; p.vy = 0;
                                }
                            }
                        }

                        // 检查通关
                        if (this.fsm.states.playing.coins.every(c => c.collected)) {
                            this.gameData.level++;
                            this.save.save(this.gameData);
                            this.fsm.changeTo('levelComplete', this.gameData, 'fade');
                        }

                        // ESC暂停
                        if (this.keys['Escape']) {
                            this.keys['Escape'] = false;
                            this.fsm.changeTo('paused');
                        }
                    },
                    render: (ctx, w, h) => {
                        // 背景
                        ctx.fillStyle = '#1a2a1a';
                        ctx.fillRect(0, 0, w, h);

                        // 地面
                        ctx.fillStyle = '#2a4a2a';
                        ctx.fillRect(0, 500, w, 100);
                        ctx.fillStyle = '#3a5a3a';
                        ctx.fillRect(0, 495, w, 8);

                        // 金币
                        for (const coin of this.fsm.states.playing.coins) {
                            if (coin.collected) continue;
                            const bob = Math.sin(this.gameTime * 3 + coin.bobOffset) * 5;
                            ctx.fillStyle = '#f0a500';
                            ctx.beginPath();
                            ctx.arc(coin.x, coin.y + bob, 10, 0, Math.PI * 2);
                            ctx.fill();
                            ctx.fillStyle = '#ffd700';
                            ctx.beginPath();
                            ctx.arc(coin.x - 2, coin.y + bob - 2, 4, 0, Math.PI * 2);
                            ctx.fill();
                        }

                        // 敌人
                        for (const enemy of this.fsm.states.playing.enemies) {
                            ctx.fillStyle = '#e94560';
                            ctx.fillRect(enemy.x, enemy.y, enemy.w, enemy.h);
                            ctx.fillStyle = '#fff';
                            ctx.fillRect(enemy.x + 6, enemy.y + 8, 6, 6);
                            ctx.fillRect(enemy.x + 18, enemy.y + 8, 6, 6);
                            ctx.fillStyle = '#111';
                            ctx.fillRect(enemy.x + (enemy.dir > 0 ? 9 : 6), enemy.y + 10, 3, 3);
                            ctx.fillRect(enemy.x + (enemy.dir > 0 ? 21 : 18), enemy.y + 10, 3, 3);
                        }

                        // 玩家
                        const p = this.fsm.states.playing.player;
                        ctx.fillStyle = '#4a90d9';
                        ctx.fillRect(p.x, p.y, p.w, p.h);
                        ctx.fillStyle = '#fff';
                        ctx.fillRect(p.x + 6, p.y + 8, 6, 6);
                        ctx.fillRect(p.x + 18, p.y + 8, 6, 6);

                        // HUD
                        ctx.fillStyle = 'rgba(0,0,0,0.5)';
                        ctx.fillRect(0, 0, w, 40);
                        ctx.fillStyle = '#fff';
                        ctx.font = '14px monospace';
                        ctx.textAlign = 'left';
                        ctx.fillText(`分数: ${this.gameData.score}`, 10, 26);
                        ctx.fillText(`关卡: ${this.gameData.level}`, 160, 26);
                        ctx.fillText(`金币: ${this.gameData.coins}`, 280, 26);
                        ctx.fillText(`生命: ${''.repeat(this.gameData.lives)}`, 400, 26);
                        ctx.textAlign = 'right';
                        ctx.fillStyle = '#888';
                        ctx.fillText('ESC暂停', w - 10, 26);
                    }
                });

                // ===== 暂停状态 =====
                this.fsm.addState('paused', {
                    enter: () => {},
                    update: (dt) => {
                        if (this.keys['Escape']) {
                            this.keys['Escape'] = false;
                            this.fsm.changeTo('playing');
                        }
                    },
                    render: (ctx, w, h) => {
                        // 先渲染游戏画面
                        if (this.fsm.states.playing.render) this.fsm.states.playing.render(ctx, w, h);
                        // 半透明遮罩
                        ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
                        ctx.fillRect(0, 0, w, h);
                        // 暂停菜单
                        ctx.fillStyle = '#fff';
                        ctx.font = 'bold 36px Arial';
                        ctx.textAlign = 'center';
                        ctx.fillText('暂停', w / 2, 200);
                        this.drawButton(ctx, w / 2, 300, 200, 50, '继续游戏', '#4a90d9', 'resume');
                        this.drawButton(ctx, w / 2, 370, 200, 50, '保存并退出', '#e94560', 'quit');
                    },
                    handleClick: (mx, my) => {
                        if (this.isButtonHit(mx, my, this.width / 2, 300, 200, 50)) {
                            this.fsm.changeTo('playing');
                        }
                        if (this.isButtonHit(mx, my, this.width / 2, 370, 200, 50)) {
                            this.save.save(this.gameData);
                            this.fsm.changeTo('menu', null, 'fade');
                        }
                    }
                });

                // ===== 通关状态 =====
                this.fsm.addState('levelComplete', {
                    enter: () => {},
                    update: (dt) => {},
                    render: (ctx, w, h) => {
                        ctx.fillStyle = '#0a1a0a';
                        ctx.fillRect(0, 0, w, h);
                        ctx.fillStyle = '#16c79a';
                        ctx.font = 'bold 40px Arial';
                        ctx.textAlign = 'center';
                        ctx.fillText(`关卡 ${this.gameData.level - 1} 完成!`, w / 2, 200);
                        ctx.fillStyle = '#fff';
                        ctx.font = '20px Arial';
                        ctx.fillText(`分数: ${this.gameData.score} | 金币: ${this.gameData.coins}`, w / 2, 260);
                        this.drawButton(ctx, w / 2, 380, 200, 50, '下一关', '#4a90d9', 'next');
                    },
                    handleClick: (mx, my) => {
                        if (this.isButtonHit(mx, my, this.width / 2, 380, 200, 50)) {
                            this.fsm.changeTo('playing', this.gameData, 'wipeRight');
                        }
                    }
                });

                // ===== 游戏结束状态 =====
                this.fsm.addState('gameover', {
                    enter: () => {},
                    update: (dt) => {},
                    render: (ctx, w, h) => {
                        ctx.fillStyle = '#1a0a0a';
                        ctx.fillRect(0, 0, w, h);
                        ctx.fillStyle = '#e94560';
                        ctx.font = 'bold 48px Arial';
                        ctx.textAlign = 'center';
                        ctx.fillText('游戏结束', w / 2, 200);
                        ctx.fillStyle = '#fff';
                        ctx.font = '20px Arial';
                        ctx.fillText(`最终分数: ${this.gameData.score}`, w / 2, 270);
                        this.drawButton(ctx, w / 2, 380, 200, 50, '重新开始', '#4a90d9', 'restart');
                        this.drawButton(ctx, w / 2, 450, 200, 50, '返回菜单', '#888', 'menu');
                    },
                    handleClick: (mx, my) => {
                        if (this.isButtonHit(mx, my, this.width / 2, 380, 200, 50)) {
                            this.gameData = { score: 0, level: 1, lives: 3, coins: 0 };
                            this.fsm.changeTo('playing', this.gameData, 'fade');
                        }
                        if (this.isButtonHit(mx, my, this.width / 2, 450, 200, 50)) {
                            this.fsm.changeTo('menu', null, 'fade');
                        }
                    }
                });

                // ===== 设置状态 =====
                this.fsm.addState('settings', {
                    enter: () => {},
                    update: (dt) => {
                        if (this.keys['Escape']) {
                            this.keys['Escape'] = false;
                            this.fsm.changeTo('menu', null, 'wipeRight');
                        }
                    },
                    render: (ctx, w, h) => {
                        ctx.fillStyle = '#0a0a2a';
                        ctx.fillRect(0, 0, w, h);
                        ctx.fillStyle = '#f0a500';
                        ctx.font = 'bold 36px Arial';
                        ctx.textAlign = 'center';
                        ctx.fillText('设置', w / 2, 120);

                        ctx.fillStyle = '#aaa';
                        ctx.font = '16px Arial';
                        ctx.fillText('(演示 - 实际项目中可调整音量、画质等)', w / 2, 170);

                        this.drawButton(ctx, w / 2, 280, 200, 50, '清除存档', '#e94560', 'clearSave');
                        this.drawButton(ctx, w / 2, 360, 200, 50, '返回', '#4a90d9', 'back');

                        ctx.fillStyle = '#666';
                        ctx.font = '13px monospace';
                        ctx.fillText(`存档状态: ${this.save.hasSave() ? '有存档' : '无存档'}`, w / 2, 440);
                    },
                    handleClick: (mx, my) => {
                        if (this.isButtonHit(mx, my, this.width / 2, 280, 200, 50)) {
                            this.save.delete();
                        }
                        if (this.isButtonHit(mx, my, this.width / 2, 360, 200, 50)) {
                            this.fsm.changeTo('menu', null, 'wipeRight');
                        }
                    }
                });
            }

            drawButton(ctx, x, y, w, h, text, color, id) {
                const isHover = this.isButtonHit(this.mouseX, this.mouseY, x, y, w, h);
                ctx.fillStyle = isHover ? color + '88' : color + '33';
                ctx.strokeStyle = isHover ? color : color + '88';
                ctx.lineWidth = 2;
                const rx = x - w / 2, ry = y - h / 2;
                ctx.beginPath();
                ctx.roundRect(rx, ry, w, h, 8);
                ctx.fill();
                ctx.stroke();
                ctx.fillStyle = isHover ? '#fff' : color;
                ctx.font = '16px Arial';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(text, x, y);
                ctx.textBaseline = 'alphabetic';
            }

            isButtonHit(mx, my, bx, by, bw, bh) {
                return mx > bx - bw / 2 && mx < bx + bw / 2 && my > by - bh / 2 && my < by + bh / 2;
            }

            start() {
                this.lastTime = performance.now();
                requestAnimationFrame((t) => this.loop(t));
            }

            loop(currentTime) {
                const dt = Math.min((currentTime - this.lastTime) / 1000, 0.1);
                this.lastTime = currentTime;

                this.fsm.update(dt);
                this.fsm.render(this.ctx, this.width, this.height);

                requestAnimationFrame((t) => this.loop(t));
            }
        }

        const game = new Game();
        game.start();
    </script>
</body>
</html>

浏览器兼容性

API Chrome Firefox Safari Edge 移动端Chrome 移动端Safari
localStorage 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
sessionStorage 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
IndexedDB 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
Canvas roundRect 完全支持 完全支持 完全支持 完全支持 完全支持 部分支持
requestAnimationFrame 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
Page Visibility API 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持

注意事项与最佳实践

  1. 状态与逻辑分离:每个状态只负责自己的逻辑和渲染,状态之间通过数据传递通信。

  2. 避免全局状态:尽量减少全局变量,将状态数据封装在状态对象或专门的DataManager中。

  3. 过渡动画统一管理:场景切换的过渡动画应由状态机统一管理,而非各状态自行处理。

  4. 存档数据版本化:存档数据应包含版本号,便于后续升级时做数据迁移。

  5. 自动存档:关键节点(通关、获得重要道具)自动存档,避免玩家因意外丢失进度。

  6. 状态栈:对于需要返回的场景(如暂停覆盖游戏),使用状态栈而非简单的状态切换。

  7. 数据不可变:状态切换时传递数据副本而非引用,避免状态间数据污染。

  8. 配置外置:关卡配置、平衡性参数等应外置为JSON数据,便于调整和热更新。

代码规范示例

代码示例

/**
 * 游戏状态基类
 * 所有游戏状态应继承此类
 */
class GameState {
    /**
     * @param {string} name - 状态名称
     */
    constructor(name) {
        this.name = name;
        this._isActive = false;
    }

    /** 状态是否激活 */
    get isActive() { return this._isActive; }

    /**
     * 进入状态时调用
     * @param {Object} data - 传入数据
     */
    enter(data) {
        this._isActive = true;
    }

    /**
     * 退出状态时调用
     */
    exit() {
        this._isActive = false;
    }

    /**
     * 每帧更新
     * @param {number} dt - 帧间隔时间
     */
    update(dt) {}

    /**
     * 每帧渲染
     * @param {CanvasRenderingContext2D} ctx
     * @param {number} width
     * @param {number} height
     */
    render(ctx, width, height) {}

    /**
     * 处理输入事件
     * @param {string} type - 事件类型
     * @param {Object} data - 事件数据
     */
    handleEvent(type, data) {}
}

/**
 * 状态管理器
 * 管理游戏状态栈和过渡
 */
class StateManager {
    constructor() {
        this._states = new Map();
        this._stack = [];
        this._transition = null;
    }

    /**
     * 注册状态
     * @param {GameState} state
     */
    register(state) {
        this._states.set(state.name, state);
    }

    /**
     * 切换状态(替换栈顶)
     * @param {string} name - 目标状态名
     * @param {Object} data - 传入数据
     */
    switchTo(name, data = {}) {
        if (this._stack.length > 0) {
            this._stack.pop().exit();
        }
        const state = this._states.get(name);
        if (state) {
            this._stack.push(state);
            state.enter(data);
        }
    }

    /**
     * 推入状态(覆盖当前)
     * @param {string} name
     * @param {Object} data
     */
    push(name, data = {}) {
        const state = this._states.get(name);
        if (state) {
            this._stack.push(state);
            state.enter(data);
        }
    }

    /**
     * 弹出状态(返回上一个)
     * @param {Object} data - 返回数据
     */
    pop(data = {}) {
        if (this._stack.length > 1) {
            this._stack.pop().exit();
            this._stack[this._stack.length - 1].enter(data);
        }
    }

    /** 获取当前状态 */
    get current() { return this._stack[this._stack.length - 1] || null; }

    update(dt) {
        if (this.current) this.current.update(dt);
    }

    render(ctx, w, h) {
        // 从底到顶渲染所有状态
        for (const state of this._stack) {
            state.render(ctx, w, h);
        }
    }
}

常见问题与解决方案

问题1:状态切换时数据丢失

原因:状态切换时没有正确传递数据。

解决方案:使用专门的DataManager管理共享数据,或在状态切换时显式传递。

问题2:localStorage存储空间不足

原因:localStorage通常限制5MB。

解决方案:使用IndexedDB存储大量数据,或压缩存档数据。

问题3:暂停状态恢复后时间跳跃

原因:暂停期间游戏时间继续累积。

解决方案:暂停时记录暂停时间,恢复时从暂停时刻继续。

问题4:状态间耦合过紧

原因:状态直接引用其他状态的数据。

解决方案:通过事件系统或数据管理器通信,避免直接引用。

问题5:过渡动画中用户输入未屏蔽

原因:过渡动画期间仍处理输入事件。

解决方案:在过渡动画期间屏蔽输入,或设置输入锁定标志。

总结

本教程全面讲解了游戏状态管理的核心技术。我们学习了游戏状态机的实现,场景管理器的场景加载和切换,存档系统的保存和加载,关卡数据的配置化设计,过渡动画的实现,以及UI状态的管理。

关键要点:

  • 游戏状态机管理不同游戏阶段的行为和转换

  • 场景管理将游戏划分为独立的场景

  • 存档系统使用localStorage或IndexedDB持久化数据

  • 关卡配置与代码分离,便于设计和调整

  • 过渡动画提升场景切换的用户体验

  • 状态栈支持暂停等覆盖场景

  • 数据版本化便于存档升级

  • 过渡动画期间应屏蔽用户输入

掌握状态管理技术后,你将能够构建结构清晰、可维护的游戏架构,支持菜单、游戏、暂停、结束等多种状态的流畅切换。

常见问题

什么是游戏状态管理?

游戏状态管理是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。

如何学习游戏状态管理的实际应用?

教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。

游戏状态管理有哪些注意事项?

常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。

游戏状态管理适合初学者吗?

本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。

游戏状态管理的核心要点是什么?

核心要点包括教程简介等内容,建议按教程顺序逐步学习。

标签: 碰撞检测 HTML5游戏 Canvas 物理引擎 键盘输入 场景管理 触摸控制 游戏开发

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 游戏开发:游戏音效与Web Audio - 从入门到实践详解 下一篇: 游戏开发:2D平台跳跃游戏 - 从入门到实践详解

poll相关推荐