JS实现别踩白块小游戏:从需求到代码的全流程解析

“别踩白块”(又称“钢琴块”)是一款经典的休闲类小游戏,玩家需在方块下落过程中点击黑色方块,避开白色方块。通过JavaScript实现该游戏,可深入理解DOM操作事件处理动画原理性能优化等前端核心技术。本文将从需求分析、技术选型、代码实现到最佳实践,完整讲解游戏开发过程,适合前端入门者学习DOM交互与动画逻辑。

目录#

游戏需求分析#

核心规则#

  1. 界面组成:由多行多列的方块组成,每行仅1个黑块,其余为白块。
  2. 交互逻辑:玩家点击黑块得分,点击白块或黑块“漏过”(超出容器底部)则游戏结束。
  3. 动态效果:方块自动从顶部下落,速度随得分递增。
  4. 目标:尽可能获得更高分数,考验反应速度。

技术选型#

  • HTML:构建游戏界面结构(容器、得分、按钮、方块容器)。
  • CSS:实现视觉样式(布局、颜色、响应式适配)。
  • JavaScript:处理核心逻辑(方块生成、事件监听、动画、游戏状态管理)。
    (注:本文使用原生JS,不依赖框架,聚焦基础DOM与动画原理。)

实现步骤#

1. HTML结构设计#

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>别踩白块</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div class="game-container">
    <h1>别踩白块</h1>
    <div class="score">得分:<span id="score">0</span></div>
    <button id="start-btn">开始游戏</button>
    <div id="block-container"></div>
  </div>
  <script src="script.js"></script>
</body>
</html>
  • 语义化结构:用<div class="game-container">包裹游戏核心元素,<div id="block-container">承载下落的方块。
  • 可维护性:得分、按钮、方块容器分离,便于后续逻辑绑定。

2. CSS样式实现#

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
 
body {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: #f5f5f5;
  font-family: Arial, sans-serif;
}
 
.game-container {
  width: 90%;
  max-width: 400px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  padding: 20px;
  text-align: center;
  position: relative;
  overflow: hidden;
}
 
#block-container {
  width: 100%;
  height: 400px;
  position: relative;
  overflow: hidden; /* 隐藏超出容器的方块 */
}
 
.block-row {
  display: flex;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
}
 
.block {
  flex: 1;
  height: 80px;
  border: 1px solid #ccc;
  background: #fff;
  cursor: pointer;
}
 
.block.black {
  background: #000;
}
  • 布局技巧
    • 方块行(.block-row)用flex布局,使方块自动平分宽度。
    • 方块容器(#block-container)设为overflow: hidden,隐藏超出底部的方块。
  • 响应式:通过max-width和百分比宽度适配不同设备,后续可结合媒体查询优化小屏幕体验。

3. JS核心逻辑#

(1)游戏初始化#

定义游戏配置(行数、列数、速度、得分等),并初始化界面:

const config = {
  cols: 4,       // 列数
  blockHeight: 80, // 方块高度(px)
  speed: 5,      // 初始下落速度(px/帧)
  score: 0,
  isPlaying: false,
  timer: null    // 动画定时器
};
 
const blockContainer = document.getElementById('block-container');
const scoreElement = document.getElementById('score');
const startBtn = document.getElementById('start-btn');
 
// 初始化游戏
function initGame() {
  config.score = 0;
  config.isPlaying = true;
  scoreElement.textContent = config.score;
  blockContainer.innerHTML = ''; // 清空旧方块
 
  // 生成初始行(从顶部堆叠)
  for (let i = 0; i < 5; i++) { // 初始5行
    createBlockRow(i);
  }
 
  startFalling(); // 启动下落动画
}

(2)方块生成与渲染#

随机生成一行方块(含1个黑块),并添加到容器:

// 创建一行方块(rowIndex:行的初始垂直位置)
function createBlockRow(rowIndex) {
  const row = document.createElement('div');
  row.className = 'block-row';
  // 初始位置:top = rowIndex * 方块高度(从顶部堆叠)
  row.style.top = `${rowIndex * config.blockHeight}px`;
 
  // 随机选择一个黑块的列索引
  const blackCol = Math.floor(Math.random() * config.cols);
 
  for (let col = 0; col < config.cols; col++) {
    const block = document.createElement('div');
    block.className = 'block';
    if (col === blackCol) {
      block.classList.add('black');
    }
    // 绑定点击事件
    block.addEventListener('click', () => handleBlockClick(block, row));
    row.appendChild(block);
  }
 
  blockContainer.appendChild(row);
}
  • 随机黑块:通过Math.random()生成0~(cols-1)的随机数,标记该列为黑块。
  • 事件绑定:为每个方块绑定点击事件,点击时调用handleBlockClick处理逻辑。

(3)事件监听与交互#

点击黑块得分、移除方块;点击白块或游戏结束时终止游戏:

// 处理方块点击
function handleBlockClick(block, row) {
  if (!config.isPlaying) return; // 游戏已结束则不响应
 
  if (block.classList.contains('black')) {
    // 点击黑块:得分+1,移除该行,生成新行
    config.score++;
    scoreElement.textContent = config.score;
    row.remove(); // 移除当前行
    // 生成新行(从顶部上方进入)
    createBlockRow(-1); // rowIndex=-1 → 初始top=-80px
 
    // 难度递增(每得10分,速度+1)
    if (config.score % 10 === 0) {
      config.speed += 1;
      restartFalling(); // 重启动画以应用新速度
    }
  } else {
    // 点击白块:游戏结束
    gameOver();
  }
}

(4)自动下落与游戏结束#

requestAnimationFrame实现平滑下落,检测方块是否“漏过”底部:

// 启动下落动画
function startFalling() {
  let position = 0; // 总下落距离(px)
  config.timer = requestAnimationFrame(function fall() {
    if (!config.isPlaying) return; // 游戏结束则停止
 
    position += config.speed;
    // 所有行向下移动(通过transform实现,性能更优)
    const rows = document.querySelectorAll('.block-row');
    rows.forEach(row => {
      // 行的当前垂直位置 = 初始top + 总下落距离
      const currentTop = parseInt(row.style.top) + position;
 
      // 检测是否超出容器底部
      if (currentTop + config.blockHeight > blockContainer.clientHeight) {
        // 检查该行是否有黑块
        const hasBlack = Array.from(row.children).some(
          block => block.classList.contains('black')
        );
        if (hasBlack) {
          gameOver(); // 漏过黑块,游戏结束
        } else {
          row.remove(); // 漏过白块,移除该行
        }
      }
    });
 
    config.timer = requestAnimationFrame(fall);
  });
}
 
// 重启动画(调整速度后调用)
function restartFalling() {
  cancelAnimationFrame(config.timer); // 停止旧动画
  startFalling(); // 启动新动画
}
 
// 游戏结束
function gameOver() {
  config.isPlaying = false;
  cancelAnimationFrame(config.timer); // 停止动画
  alert(`游戏结束!得分:${config.score}`);
  startBtn.disabled = false; // 重新启用开始按钮
}

(5)开始按钮绑定#

startBtn.addEventListener('click', () => {
  startBtn.disabled = true; // 防止重复点击
  initGame(); // 初始化并启动游戏
});

最佳实践#

1. 性能优化#

  • 动画选择:用requestAnimationFrame代替setInterval,与浏览器刷新率同步(通常60FPS),避免卡顿。
  • DOM操作优化
    • 批量操作:生成一行方块后再添加到容器(如createBlockRow中先构建行,再appendChild)。
    • 避免重排:用transform: translateY代替top属性(本文示例为简化用top,实际可改为transform,减少重排)。

2. 代码模块化#

将游戏逻辑拆分为独立函数(initGamecreateBlockRowhandleBlockClick等),降低耦合度:

  • 新增功能(如音效、关卡)可通过扩展函数实现。
  • 维护时只需修改对应函数,不影响其他逻辑。

3. 响应式与移动端适配#

  • 触摸支持:为方块添加touchstart事件,兼容移动端点击:
    block.addEventListener('click', () => handleBlockClick(block, row));
    block.addEventListener('touchstart', () => handleBlockClick(block, row));
  • 自适应布局:通过媒体查询调整小屏幕的列数和方块大小:
    @media (max-width: 360px) {
      .block {
        height: 60px; /* 减小方块高度 */
      }
      .game-container {
        max-width: 320px;
      }
      .block-row {
        /* 小屏幕列数改为3 */
        /* 需配合JS动态修改cols参数 */
      }
    }

常见问题及解决方案#

1. 方块点击无响应#

  • 原因:事件监听未正确绑定,或方块被其他元素遮挡。
  • 解决:检查createBlockRow中是否为每个方块添加了addEventListener,并确保方块的z-index无冲突。

2. 游戏结束逻辑错误(漏过黑块未触发结束)#

  • 原因:hasBlack判断逻辑错误,或行的位置计算错误。
  • 解决:确保遍历行内所有方块,正确判断是否含black类;调试currentTop的计算逻辑,确保与容器高度比较正确。

3. 性能卡顿(动画不流畅)#

  • 原因:setInterval动画、频繁DOM操作。
  • 解决:改用requestAnimationFrame,并批量操作DOM(如生成一行后再添加到容器)。

总结#

通过实现“别踩白块”,我们掌握了:

  • DOM操作:动态生成、删除元素,事件监听与响应。
  • 动画原理requestAnimationFrame实现平滑下落,结合transform优化性能。
  • 游戏逻辑:状态管理(得分、速度、游戏状态)、难度递增、边界检测。

该项目可扩展方向:添加音效、多关卡、本地存储记录最高分、自定义皮肤等。通过实践,你将更深入理解前端交互与动画的核心原理。

参考文献#