首页
/ 构建记忆灯光游戏:从状态管理到交互设计的完整实现指南

构建记忆灯光游戏:从状态管理到交互设计的完整实现指南

2026-03-10 04:17:03作者:廉彬冶Miranda

项目背景

记忆灯光游戏(Simon Game)作为经典的电子记忆挑战游戏,自1978年推出以来,一直是检验和训练记忆力的有效工具。该游戏通过生成随机的灯光序列,要求玩家重复并记忆越来越长的序列,直至达到预设难度或出现错误。本教程将系统讲解如何使用现代前端技术栈构建这一游戏,涵盖状态管理、用户交互、动画效果和性能优化等关键技术点,帮助开发者掌握复杂前端应用的设计与实现方法。

在当今Web开发中,记忆游戏不仅是学习前端交互逻辑的理想案例,也涉及到诸多核心技术挑战,如异步流程控制、状态同步、用户体验优化等。通过本项目的实践,开发者可以深入理解前端状态管理模式、事件处理机制以及动画与音频的协同工作原理。

团队协作开发场景

核心机制解析

游戏状态模型

记忆灯光游戏的核心在于精确的状态管理。我们采用面向对象的设计思想,通过GameController类封装所有游戏状态和行为:

class GameController {
  constructor() {
    // 游戏核心状态
    this.sequence = [];         // 生成的随机序列
    this.playerSequence = [];   // 玩家输入的序列
    this.round = 0;             // 当前回合数
    this.isStrict = false;      // 严格模式标志
    this.isPlaying = false;     // 游戏进行状态
    this.isSequencePlaying = false; // 序列播放中标志
  }

  // 初始化游戏
  init() {
    this.resetState();
    this.loadSettings();
    // 绑定DOM事件监听
    this.bindEvents();
  }

  // 重置游戏状态
  resetState() {
    this.sequence = [];
    this.playerSequence = [];
    this.round = 0;
    this.isPlaying = false;
    this.isSequencePlaying = false;
  }
  
  // 加载本地存储的游戏设置
  loadSettings() {
    const savedStrictMode = localStorage.getItem('simonStrictMode');
    this.isStrict = savedStrictMode === 'true';
    // 更新UI显示当前设置
    this.updateSettingsUI();
  }
}

这种封装方式提供了良好的状态隔离和代码组织,相比函数式实现更易于维护和扩展。

序列生成与验证机制

游戏的核心逻辑围绕随机序列的生成与验证展开:

class GameController {
  // 生成指定长度的随机序列
  generateSequence(length) {
    return Array.from({ length }, () => this.getRandomButton());
  }
  
  // 获取随机按钮索引(0-3)
  getRandomButton() {
    return Math.floor(Math.random() * 4);
  }
  
  // 验证玩家输入
  validateInput(buttonIndex) {
    if (!this.isPlaying || this.isSequencePlaying) return false;
    
    this.playerSequence.push(buttonIndex);
    const currentIndex = this.playerSequence.length - 1;
    
    // 检查当前输入是否正确
    if (this.playerSequence[currentIndex] !== this.sequence[currentIndex]) {
      this.handleError();
      return false;
    }
    
    // 检查是否完成当前回合
    if (this.playerSequence.length === this.sequence.length) {
      this.handleRoundComplete();
    }
    
    return true;
  }
}

游戏流程控制

游戏流程控制是确保游戏逻辑正确执行的关键:

class GameController {
  // 开始新游戏
  startGame() {
    if (this.isPlaying) return;
    
    this.resetState();
    this.isPlaying = true;
    this.nextRound();
  }
  
  // 进入下一回合
  nextRound() {
    this.round++;
    this.playerSequence = [];
    this.sequence = this.generateSequence(this.round);
    this.playSequence();
    this.updateRoundDisplay();
  }
  
  // 播放序列
  async playSequence() {
    this.isSequencePlaying = true;
    this.disableInput();
    
    for (const buttonIndex of this.sequence) {
      await this.activateButton(buttonIndex);
      await this.delay(this.getSequenceDelay());
    }
    
    this.enableInput();
    this.isSequencePlaying = false;
  }
  
  // 获取序列播放延迟时间(随回合增加而减少)
  getSequenceDelay() {
    // 基础延迟1000ms,每5回合减少100ms,最低300ms
    return Math.max(1000 - Math.floor(this.round / 5) * 100, 300);
  }
  
  // 延迟函数
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

实现步骤

1. 项目初始化与基础结构搭建

首先创建项目基础文件结构:

simon-game/
├── index.html         # 游戏页面
├── css/
│   └── styles.css     # 样式文件
├── js/
│   ├── GameController.js  # 游戏核心逻辑
│   ├── audio.js       # 音频处理
│   └── main.js        # 入口文件
└── sounds/            # 音频文件
    ├── button-0.mp3
    ├── button-1.mp3
    ├── button-2.mp3
    ├── button-3.mp3
    └── error.mp3

HTML基础结构:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>记忆灯光游戏</title>
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <div class="game-container">
    <h1>记忆灯光挑战</h1>
    <div class="game-info">
      <div class="round-display">回合: <span id="round">0</span></div>
      <div class="controls">
        <button id="start-btn" class="btn">开始</button>
        <label class="strict-mode">
          <input type="checkbox" id="strict-mode"> 严格模式
        </label>
      </div>
    </div>
    
    <div class="game-board">
      <div class="game-button" data-index="0" id="btn-0"></div>
      <div class="game-button" data-index="1" id="btn-1"></div>
      <div class="game-button" data-index="2" id="btn-2"></div>
      <div class="game-button" data-index="3" id="btn-3"></div>
    </div>
  </div>
  
  <script src="js/audio.js"></script>
  <script src="js/GameController.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

2. 样式设计与响应式布局

CSS实现(关键部分):

.game-container {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.game-board {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 15px;
  margin-top: 30px;
  position: relative;
  width: 100%;
  aspect-ratio: 1/1;
  max-width: 500px;
  margin-left: auto;
  margin-right: auto;
}

.game-button {
  border-radius: 15px;
  cursor: pointer;
  transition: all 0.2s ease;
  box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}

/* 四个彩色按钮 */
#btn-0 { background-color: #4CAF50; } /* 绿色 */
#btn-1 { background-color: #F44336; } /* 红色 */
#btn-2 { background-color: #FFC107; } /* 黄色 */
#btn-3 { background-color: #2196F3; } /* 蓝色 */

/* 按钮激活状态 */
.game-button.active {
  opacity: 0.6;
  transform: scale(0.98);
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

/* 响应式调整 */
@media (max-width: 500px) {
  .game-board {
    gap: 10px;
  }
}

3. 音频系统实现

创建专用的音频管理器处理声音播放:

// audio.js
export class AudioManager {
  constructor() {
    // 预加载所有音频
    this.sounds = {
      buttons: [
        new Audio('sounds/button-0.mp3'),
        new Audio('sounds/button-1.mp3'),
        new Audio('sounds/button-2.mp3'),
        new Audio('sounds/button-3.mp3')
      ],
      error: new Audio('sounds/error.mp3')
    };
    
    // 配置音频属性
    this.setupAudio();
  }
  
  setupAudio() {
    // 设置所有按钮音频的音量
    this.sounds.buttons.forEach(sound => {
      sound.volume = 0.7;
    });
    
    this.sounds.error.volume = 0.5;
  }
  
  playButtonSound(index) {
    if (index < 0 || index >= this.sounds.buttons.length) return;
    
    const sound = this.sounds.buttons[index];
    // 重置音频以允许连续播放
    sound.currentTime = 0;
    sound.play().catch(e => {
      console.warn('音频播放失败:', e);
      // 处理浏览器自动播放限制
      this.handleAutoplayRestriction();
    });
  }
  
  playErrorSound() {
    this.sounds.error.currentTime = 0;
    this.sounds.error.play();
  }
  
  // 处理浏览器自动播放限制
  handleAutoplayRestriction() {
    // 可以在这里显示提示,要求用户交互以启用音频
  }
}

4. 游戏核心逻辑实现

完整实现GameController类,整合状态管理、序列处理和用户交互:

// GameController.js
import { AudioManager } from './audio.js';

export class GameController {
  constructor() {
    // 游戏状态初始化
    this.sequence = [];
    this.playerSequence = [];
    this.round = 0;
    this.isStrict = false;
    this.isPlaying = false;
    this.isSequencePlaying = false;
    
    // 依赖组件初始化
    this.audioManager = new AudioManager();
    
    // DOM元素引用
    this.elements = {
      buttons: document.querySelectorAll('.game-button'),
      startButton: document.getElementById('start-btn'),
      strictModeCheckbox: document.getElementById('strict-mode'),
      roundDisplay: document.getElementById('round')
    };
    
    // 绑定事件处理
    this.bindEvents();
  }
  
  // 绑定DOM事件
  bindEvents() {
    // 开始按钮点击事件
    this.elements.startButton.addEventListener('click', () => this.startGame());
    
    // 严格模式切换事件
    this.elements.strictModeCheckbox.addEventListener('change', (e) => {
      this.isStrict = e.target.checked;
      localStorage.setItem('simonStrictMode', this.isStrict);
    });
    
    // 游戏按钮点击事件
    this.elements.buttons.forEach(button => {
      button.addEventListener('click', () => {
        const index = parseInt(button.dataset.index);
        this.handleButtonClick(index);
      });
      
      // 触摸设备支持
      button.addEventListener('touchstart', (e) => {
        e.preventDefault(); // 防止触摸事件冒泡
        const index = parseInt(button.dataset.index);
        this.handleButtonClick(index);
      });
    });
  }
  
  // 处理按钮点击
  handleButtonClick(index) {
    if (!this.isPlaying || this.isSequencePlaying) return;
    
    // 播放按钮声音和动画
    this.activateButton(index, true);
    
    // 验证输入
    const isValid = this.validateInput(index);
    
    if (!isValid) {
      this.handleError();
    }
  }
  
  // 激活按钮(播放动画和声音)
  async activateButton(index, isUserInput = false) {
    const button = this.elements.buttons[index];
    
    // 播放声音
    this.audioManager.playButtonSound(index);
    
    // 添加激活状态
    button.classList.add('active');
    
    // 维持激活状态
    await this.delay(isUserInput ? 200 : 500);
    
    // 移除激活状态
    button.classList.remove('active');
    
    // 短暂延迟确保动画完成
    await this.delay(50);
  }
  
  // 处理错误情况
  async handleError() {
    this.isSequencePlaying = true;
    this.disableInput();
    this.audioManager.playErrorSound();
    
    // 显示错误视觉反馈
    this.flashAllButtons();
    
    if (this.isStrict) {
      // 严格模式:游戏结束
      await this.delay(1000);
      this.gameOver();
    } else {
      // 非严格模式:重放序列
      await this.delay(1000);
      this.playSequence();
    }
  }
  
  // 所有按钮闪烁效果
  async flashAllButtons() {
    for (let i = 0; i < 3; i++) {
      this.elements.buttons.forEach(button => button.classList.add('active'));
      await this.delay(150);
      this.elements.buttons.forEach(button => button.classList.remove('active'));
      await this.delay(150);
    }
  }
  
  // 处理回合完成
  handleRoundComplete() {
    // 检查是否达到胜利条件(20回合)
    if (this.round >= 20) {
      this.victory();
      return;
    }
    
    // 进入下一回合
    setTimeout(() => this.nextRound(), 1000);
  }
  
  // 游戏胜利处理
  async victory() {
    this.isPlaying = false;
    this.updateRoundDisplay();
    
    // 播放胜利动画
    for (let i = 0; i < 3; i++) {
      await this.playSequence();
      await this.delay(500);
    }
    
    alert('恭喜你胜利了!');
  }
  
  // 游戏结束处理
  gameOver() {
    this.isPlaying = false;
    this.updateRoundDisplay();
    alert(`游戏结束!你达到了第${this.round}回合`);
  }
  
  // 更新回合显示
  updateRoundDisplay() {
    this.elements.roundDisplay.textContent = this.round;
  }
  
  // 禁用/启用输入
  disableInput() {
    this.elements.buttons.forEach(button => button.style.pointerEvents = 'none');
  }
  
  enableInput() {
    this.elements.buttons.forEach(button => button.style.pointerEvents = 'auto');
  }
  
  // 延迟函数
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

5. 应用入口与初始化

// main.js
import { GameController } from './GameController.js';

// 等待DOM加载完成
document.addEventListener('DOMContentLoaded', () => {
  // 初始化游戏控制器
  const game = new GameController();
  
  // 暴露到全局以便调试(生产环境可移除)
  window.simonGame = game;
});

优化策略

1. 性能优化:动画与事件处理

问题:快速连续点击可能导致动画不同步和性能问题。

解决方案:实现节流机制控制按钮点击频率:

// 添加到GameController类
constructor() {
  // ...其他初始化代码
  this.isButtonProcessing = false;
}

// 修改handleButtonClick方法
handleButtonClick(index) {
  if (!this.isPlaying || this.isSequencePlaying || this.isButtonProcessing) return;
  
  this.isButtonProcessing = true;
  this.activateButton(index, true)
    .then(() => {
      const isValid = this.validateInput(index);
      if (!isValid) {
        this.handleError();
      }
      this.isButtonProcessing = false;
    });
}

性能对比

  • 优化前:连续快速点击会导致动画叠加、声音混乱
  • 优化后:确保每次点击完成后才处理下一次点击,动画和声音播放有序

2. 音频播放优化

问题:移动设备上音频播放延迟或失败。

解决方案:使用Web Audio API替代原生Audio对象:

// Web Audio API实现的音频管理器
export class WebAudioManager {
  constructor() {
    this.audioContext = null;
    this.buffers = {};
    this.init();
  }
  
  async init() {
    try {
      // 检查Web Audio API支持
      window.AudioContext = window.AudioContext || window.webkitAudioContext;
      this.audioContext = new AudioContext();
      
      // 加载所有音频文件
      await this.loadSounds();
    } catch (e) {
      console.error('Web Audio API初始化失败:', e);
    }
  }
  
  async loadSounds() {
    const soundUrls = [
      'sounds/button-0.mp3',
      'sounds/button-1.mp3',
      'sounds/button-2.mp3',
      'sounds/button-3.mp3',
      'sounds/error.mp3'
    ];
    
    for (const url of soundUrls) {
      const response = await fetch(url);
      const arrayBuffer = await response.arrayBuffer();
      const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
      
      // 提取文件名作为键
      const key = url.split('/').pop().split('.')[0];
      this.buffers[key] = audioBuffer;
    }
  }
  
  playSound(key) {
    if (!this.audioContext || !this.buffers[key]) return;
    
    // 检查音频上下文状态
    if (this.audioContext.state === 'suspended') {
      this.audioContext.resume();
    }
    
    const source = this.audioContext.createBufferSource();
    source.buffer = this.buffers[key];
    source.connect(this.audioContext.destination);
    source.start(0);
  }
  
  playButtonSound(index) {
    this.playSound(`button-${index}`);
  }
  
  playErrorSound() {
    this.playSound('error');
  }
}

优势:Web Audio API提供更低延迟的音频播放和更精细的控制,尤其在移动设备上表现更好。

3. 状态持久化与恢复

实现游戏状态的本地存储,允许玩家暂停后继续游戏:

// 添加到GameController类
saveGameState() {
  const state = {
    sequence: this.sequence,
    playerSequence: this.playerSequence,
    round: this.round,
    isStrict: this.isStrict,
    timestamp: Date.now()
  };
  
  localStorage.setItem('simonGameState', JSON.stringify(state));
}

loadGameState() {
  const savedState = localStorage.getItem('simonGameState');
  if (!savedState) return false;
  
  try {
    const state = JSON.parse(savedState);
    // 检查状态是否过期(超过1小时)
    if (Date.now() - state.timestamp > 3600000) {
      this.clearSavedState();
      return false;
    }
    
    this.sequence = state.sequence;
    this.playerSequence = state.playerSequence;
    this.round = state.round;
    this.isStrict = state.isStrict;
    
    // 更新UI
    this.elements.strictModeCheckbox.checked = this.isStrict;
    this.updateRoundDisplay();
    
    return true;
  } catch (e) {
    console.error('加载游戏状态失败:', e);
    this.clearSavedState();
    return false;
  }
}

clearSavedState() {
  localStorage.removeItem('simonGameState');
}

扩展方向

1. 高级难度系统

实现基于玩家表现的动态难度调整:

// 动态难度系统实现
class DifficultySystem {
  constructor(gameController) {
    this.game = gameController;
    this.baseSpeed = 1000; // 基础速度(ms)
    this.speedFactor = 1;  // 速度因子(越小越快)
    this.mistakeCount = 0;
  }
  
  // 根据玩家表现调整难度
  adjustDifficulty(isCorrect) {
    if (isCorrect) {
      // 连续正确,增加速度
      if (this.game.round % 3 === 0) {
        this.speedFactor = Math.max(0.3, this.speedFactor - 0.05);
      }
      this.mistakeCount = 0;
    } else {
      // 错误,降低速度
      this.mistakeCount++;
      if (this.mistakeCount >= 2) {
        this.speedFactor = Math.min(1, this.speedFactor + 0.1);
        this.mistakeCount = 0;
      }
    }
  }
  
  // 获取当前速度
  getCurrentSpeed() {
    return this.baseSpeed * this.speedFactor;
  }
}

2. 多玩家对战模式

实现本地多人轮流挑战模式:

class MultiplayerSystem {
  constructor(gameController) {
    this.game = gameController;
    this.players = [];
    this.currentPlayer = 0;
  }
  
  // 添加玩家
  addPlayer(name) {
    this.players.push({
      name,
      score: 0,
      highRound: 0
    });
  }
  
  // 切换到下一个玩家
  nextPlayer() {
    // 保存当前玩家成绩
    if (this.players.length > 0) {
      this.players[this.currentPlayer].highRound = Math.max(
        this.players[this.currentPlayer].highRound,
        this.game.round
      );
      
      // 计算得分(每回合10分)
      this.players[this.currentPlayer].score += this.game.round * 10;
    }
    
    // 切换玩家
    this.currentPlayer = (this.currentPlayer + 1) % this.players.length;
    this.updatePlayerDisplay();
    
    // 重置游戏状态但保持设置
    const strictMode = this.game.isStrict;
    this.game.resetState();
    this.game.isStrict = strictMode;
    this.game.startGame();
  }
  
  // 更新玩家显示
  updatePlayerDisplay() {
    const playerInfo = document.getElementById('player-info');
    if (playerInfo) {
      playerInfo.textContent = `玩家 ${this.players[this.currentPlayer].name} 的回合`;
    }
  }
  
  // 获取玩家排名
  getRanking() {
    return [...this.players].sort((a, b) => b.score - a.score);
  }
}

3. 技术选型对比分析

技术方案 优势 劣势 适用场景
原生JavaScript 无依赖、轻量、学习曲线低 代码组织复杂、缺乏高级特性 简单游戏、教学项目
React框架 组件化、状态管理成熟 有学习成本、包体积大 复杂UI、需要频繁状态更新
Vue框架 易上手、模板系统直观 生态相对较小 中等复杂度应用
TypeScript 类型安全、更好的IDE支持 需要额外编译步骤 团队协作、大型项目

对于记忆灯光游戏这类中等复杂度的交互应用,原生JavaScript配合面向对象设计已足够满足需求,同时保持代码的简洁和可维护性。如果需要扩展为更复杂的游戏平台,则可以考虑迁移到React等框架以获得更好的状态管理和组件复用能力。

开发陷阱与解决方案

  1. 音频自动播放限制

    问题:现代浏览器限制未交互页面的音频自动播放。

    解决方案:添加"点击开始游戏"交互,在首次用户交互时初始化音频上下文:

    // 修改startGame方法
    startGame() {
      if (this.isPlaying) return;
      
      // 确保音频上下文已初始化
      if (this.audioManager instanceof WebAudioManager && 
          this.audioManager.audioContext.state === 'suspended') {
        this.audioManager.audioContext.resume();
      }
      
      this.resetState();
      this.isPlaying = true;
      this.nextRound();
    }
    
  2. 移动端触摸事件延迟

    问题:移动设备上触摸事件存在300ms延迟。

    解决方案:使用触摸事件并添加视口元标签优化:

    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    

    同时在CSS中添加:

    .game-button {
      touch-action: manipulation; /* 优化触摸行为 */
    }
    
  3. 状态同步问题

    问题:序列播放与用户输入可能不同步。

    解决方案:使用状态标志严格控制游戏流程:

    // 确保序列播放时禁用用户输入
    playSequence() {
      this.isSequencePlaying = true;
      this.disableInput();
      
      // 序列播放逻辑...
      
      // 播放完成后重新启用输入
      .then(() => {
        this.isSequencePlaying = false;
        this.enableInput();
      });
    }
    

通过本教程,我们系统地实现了一个功能完整的记忆灯光游戏,涵盖了从基础结构到高级功能的各个方面。这个项目不仅展示了前端游戏开发的核心技术,也提供了可扩展的架构设计,使开发者能够在此基础上继续添加新功能和优化。无论是作为学习前端交互逻辑的案例,还是作为理解状态管理的实践项目,记忆灯光游戏都为开发者提供了丰富的学习价值。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
27
13
docsdocs
OpenHarmony documentation | OpenHarmony开发者文档
Dockerfile
644
4.2 K
Dora-SSRDora-SSR
Dora SSR 是一款跨平台的游戏引擎,提供前沿或是具有探索性的游戏开发功能。它内置了Web IDE,提供了可以轻轻松松通过浏览器访问的快捷游戏开发环境,特别适合于在新兴市场如国产游戏掌机和其它移动电子设备上直接进行游戏开发和编程学习。
C++
57
7
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Java
69
21
RuoYi-Vue3RuoYi-Vue3
🎉 (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统
Vue
1.52 K
873
flutter_flutterflutter_flutter
暂无简介
Dart
888
212
nop-entropynop-entropy
Nop Platform 2.0是基于可逆计算理论实现的采用面向语言编程范式的新一代低代码开发平台,包含基于全新原理从零开始研发的GraphQL引擎、ORM引擎、工作流引擎、报表引擎、规则引擎、批处理引引擎等完整设计。nop-entropy是它的后端部分,采用java语言实现,可选择集成Spring框架或者Quarkus框架。中小企业可以免费商用
Java
12
1
giteagitea
喝着茶写代码!最易用的自托管一站式代码托管平台,包含Git托管,代码审查,团队协作,软件包和CI/CD。
Go
24
0
pytorchpytorch
Ascend Extension for PyTorch
Python
481
580
cherry-studiocherry-studio
🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端
TypeScript
1.29 K
105