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>浏览器兼容性
注意事项与最佳实践
状态与逻辑分离:每个状态只负责自己的逻辑和渲染,状态之间通过数据传递通信。
避免全局状态:尽量减少全局变量,将状态数据封装在状态对象或专门的DataManager中。
过渡动画统一管理:场景切换的过渡动画应由状态机统一管理,而非各状态自行处理。
存档数据版本化:存档数据应包含版本号,便于后续升级时做数据迁移。
自动存档:关键节点(通关、获得重要道具)自动存档,避免玩家因意外丢失进度。
状态栈:对于需要返回的场景(如暂停覆盖游戏),使用状态栈而非简单的状态切换。
数据不可变:状态切换时传递数据副本而非引用,避免状态间数据污染。
配置外置:关卡配置、平衡性参数等应外置为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基础的初学者。
游戏状态管理的核心要点是什么?
核心要点包括教程简介等内容,建议按教程顺序逐步学习。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别