经典打砖块(Breakout)游戏

一、游戏核心机制

目标:用挡板反弹球,击碎画面上所有砖块即获胜;球掉落底部则损失一条生命,3条生命耗尽游戏结束。

操作方式:

  • ← / A 或 → / D:左右移动挡板

  • 鼠标移动 / 手指滑动:挡板跟随光标(更直观)

  • 空格键:发射球 / 暂停游戏

  • "重开"按钮:随时重新开始

得分与生命:

  • 每击碎一个砖块得 10分

  • 初始 3条生命,球掉出底部扣1条

  • 生命归零显示 GAME OVER ,砖块全清显示 胜利


二、技术实现亮点

1. Canvas 2D 渲染

  • 整个游戏画面通过 <canvas> 绘制,尺寸 800×500

  • 砖块使用圆角矩形 + 高光 + 底部阴影,做出立体质感

  • 球体使用径向渐变 + 发光阴影(shadowBlur),呈现霓虹光效

  • 挡板使用线性渐变模拟金属质感

2. 物理碰撞逻辑

  • 墙壁反弹:检测球与左右边界、顶部的碰撞,反转对应方向的速度分量

  • 挡板反弹:根据球击中挡板的位置(左/中/右)动态计算反弹角度------击中边缘角度大,击中中间角度小,增加操控策略性

  • 砖块碰撞:采用 AABB(轴对齐包围盒)检测,通过比较水平/垂直方向的重叠量,判断从哪个方向反弹,避免球卡在砖块中

3. 游戏状态管理

  • 用 bricks[r][c] = 1/0 的二维数组记录砖块存活状态

  • ballLaunched 控制球是否已发射(未发射时球跟随挡板)

  • paused / gameOver / gameWin 三个标志位控制游戏流程

  • 用 requestAnimationFrame 驱动游戏主循环,保证流畅度

4. 交互细节

  • 键盘方向键和空格均做了 preventDefault,防止页面滚动

  • 支持鼠标和触摸屏操作,移动端也可玩

  • 挡板位置做了边界钳制,不会移出画布


三、视觉与界面设计

  • 复古街机风格:深色背景 + 网格线 + 霓虹光效,模拟老式街机屏幕

  • 信息面板:顶部显示得分、生命(❤️图标)和重开按钮,采用内阴影和立体按钮效果

  • 状态提示:未发射时提示"按空格发射球",暂停/结束/胜利均有居中文字提示

  • 配色方案:6行砖块从红→橙→黄→绿→蓝→紫渐变,层次分明


四、代码结构概览

text

复制代码
init()
 ├── initBrickColors()       // 初始化砖块颜色
 ├── resetGame()            // 重置分数/生命/砖块/球
 ├── initEvents()           // 绑定键盘/鼠标/触摸/按钮事件
 └── gameLoop()             // 主循环
      ├── update()          // 逻辑更新(移动、碰撞、计分)
      └── draw()            // 渲染所有元素

核心函数说明:

函数 作用
launchBall() 给球随机角度并赋予初速度
handleBrickCollision() 砖块碰撞检测与反弹处理
checkWin() 遍历砖块数组判断是否全清
resetBallAndPaddle() 球掉落或重开时重置球和挡板位置

五、可扩展方向

如果想进一步练习或改造,可以尝试:

  • 增加关卡:砖块全清后提升球速、增加砖块行数

  • 道具系统:掉落加长挡板、多球、穿透球等道具

  • 音效:用 Web Audio API 添加击打音效

  • 排行榜 :用 localStorage 保存最高分

  • 移动端优化:增加虚拟按钮或更大的触摸热区

六、html

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>经典打砖块 · 复古街机</title>
  <style>
    * {
      box-sizing: border-box;
      user-select: none;
    }
    body {
      margin: 0;
      min-height: 100vh;
      background: linear-gradient(145deg, #1a2a3a, #0e1a24);
      display: flex;
      justify-content: center;
      align-items: center;
      font-family: 'Courier New', Courier, monospace;
    }
    .game-wrapper {
      background: #1e2b36;
      padding: 20px 25px 25px 25px;
      border-radius: 32px;
      box-shadow: 0 16px 0 #0b1115, 0 8px 24px rgba(0,0,0,0.8), inset 0 0 0 2px #5f7a8c;
    }
    canvas {
      display: block;
      margin: 0 auto;
      border-radius: 16px;
      background: #0c141c;
      box-shadow: inset 0 0 20px #000000, 0 8px 0 #080e12;
      cursor: none;
      width: 800px;
      height: 500px;
    }
    .info-panel {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin: 10px 8px 4px 8px;
      color: #bfd9e8;
      text-shadow: 0 2px 0 #0b1115;
      font-weight: bold;
      font-size: 22px;
      letter-spacing: 2px;
    }
    .score-box, .lives-box {
      background: #16222b;
      padding: 8px 24px;
      border-radius: 40px;
      box-shadow: inset 0 0 0 2px #3d5666, 0 5px 0 #0b1115;
      display: flex;
      align-items: center;
      gap: 12px;
    }
    .lives-box span {
      color: #ff6b7c;
      font-size: 26px;
      letter-spacing: 4px;
    }
    .restart-btn {
      background: #3d5666;
      border: none;
      color: #e9f2fa;
      font-family: inherit;
      font-weight: bold;
      font-size: 18px;
      padding: 8px 28px;
      border-radius: 40px;
      box-shadow: 0 5px 0 #1d2c36, 0 4px 12px black;
      letter-spacing: 1px;
      transition: 0.08s linear;
      cursor: pointer;
    }
    .restart-btn:active {
      transform: translateY(5px);
      box-shadow: 0 0 0 #1d2c36, 0 4px 12px black;
    }
    .restart-btn:hover {
      background: #4e6d81;
    }
    .controls {
      text-align: center;
      color: #7d9aab;
      margin-top: 8px;
      font-size: 15px;
      letter-spacing: 1px;
    }
  </style>
</head>
<body>
<div class="game-wrapper">
  <canvas id="gameCanvas" width="800" height="500"></canvas>
  <div class="info-panel">
    <div class="score-box">得分 <span id="scoreDisplay">0</span></div>
    <div class="lives-box">❤️ <span id="livesDisplay">3</span></div>
    <button class="restart-btn" id="restartButton">↻ 重开</button>
  </div>
  <div class="controls">← → 移动挡板 | 空格 发射/暂停</div>
</div>

<script>
  (function() {
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    const scoreSpan = document.getElementById('scoreDisplay');
    const livesSpan = document.getElementById('livesDisplay');
    const restartBtn = document.getElementById('restartButton');

    // 游戏尺寸常量
    const CW = 800;
    const CH = 500;

    // 挡板
    const PADDLE_WIDTH = 140;
    const PADDLE_HEIGHT = 16;
    const PADDLE_Y = CH - 45;
    let paddleX = (CW - PADDLE_WIDTH) / 2;

    // 球
    const BALL_RADIUS = 8;
    let ballX = CW / 2;
    let ballY = CH - 70;
    let ballDX = 0;
    let ballDY = 0;
    const BASE_SPEED = 6.2;

    // 砖块
    const BRICK_ROWS = 6;
    const BRICK_COLS = 10;
    const BRICK_WIDTH = 68;
    const BRICK_HEIGHT = 22;
    const BRICK_GAP = 6;
    const BRICKS_OFFSET_X = (CW - (BRICK_COLS * (BRICK_WIDTH + BRICK_GAP) - BRICK_GAP)) / 2;
    const BRICKS_OFFSET_Y = 50;

    let bricks = [];          // 二维: bricks[行][列] = 0 / 1
    let brickColors = [];     // 预定义每行颜色

    // 游戏状态
    let score = 0;
    let lives = 3;
    let gameOver = false;
    let gameWin = false;
    let paused = false;
    let ballLaunched = false;  // 是否已发射

    // 键盘
    let leftPressed = false;
    let rightPressed = false;
    const PADDLE_SPEED = 8.5;

    // 初始化砖块颜色 (逐行变亮)
    function initBrickColors() {
      brickColors = [
        '#FF6B6B', // 红
        '#FFA36B', // 橙
        '#FFD46B', // 黄
        '#8BD46B', // 绿
        '#6BB5FF', // 蓝
        '#B86BFF'  // 紫
      ];
    }

    // 重置砖块 (全部为1,表示存活)
    function resetBricks() {
      bricks = [];
      for (let r = 0; r < BRICK_ROWS; r++) {
        bricks[r] = [];
        for (let c = 0; c < BRICK_COLS; c++) {
          bricks[r][c] = 1;
        }
      }
    }

    // 重置球和挡板位置 (不重置分数/生命)
    function resetBallAndPaddle() {
      paddleX = (CW - PADDLE_WIDTH) / 2;
      ballX = CW / 2;
      ballY = PADDLE_Y - BALL_RADIUS - 2;
      ballDX = 0;
      ballDY = 0;
      ballLaunched = false;
      paused = false;      // 重置暂停状态,但游戏状态仍保留
    }

    // 完整重置游戏
    function resetGame() {
      score = 0;
      lives = 3;
      gameOver = false;
      gameWin = false;
      paused = false;
      initBrickColors();
      resetBricks();
      resetBallAndPaddle();
      updateScoreAndLives();
    }

    // 更新UI
    function updateScoreAndLives() {
      scoreSpan.textContent = score;
      livesSpan.textContent = lives;
    }

    // 检查是否胜利 (所有砖块消失)
    function checkWin() {
      for (let r = 0; r < BRICK_ROWS; r++) {
        for (let c = 0; c < BRICK_COLS; c++) {
          if (bricks[r][c] === 1) return false;
        }
      }
      return true;
    }

    // 发射球 (给予随机角度)
    function launchBall() {
      if (ballLaunched || gameOver || gameWin) return;
      ballLaunched = true;
      // 随机水平方向,垂直向上
      const angle = (Math.random() * 0.7 - 0.35) + (Math.PI * 1.5); // 向上为主
      // 更可靠的: 水平随机-0.6~0.6 rad,垂直向上
      const randomAngle = (Math.random() * 1.2 - 0.6); // -0.6 ~ 0.6
      ballDX = Math.sin(randomAngle) * BASE_SPEED;
      ballDY = -Math.cos(randomAngle) * BASE_SPEED;
      // 确保不是完全水平(极小概率)
      if (Math.abs(ballDX) < 0.8) ballDX = (ballDX > 0 ? 2.0 : -2.0);
    }

    // 碰撞砖块处理
    function handleBrickCollision() {
      for (let r = 0; r < BRICK_ROWS; r++) {
        for (let c = 0; c < BRICK_COLS; c++) {
          if (bricks[r][c] === 0) continue;

          const brickX = BRICKS_OFFSET_X + c * (BRICK_WIDTH + BRICK_GAP);
          const brickY = BRICKS_OFFSET_Y + r * (BRICK_HEIGHT + BRICK_GAP);

          // 球与砖块矩形碰撞检测 (AABB)
          if (ballX + BALL_RADIUS > brickX &&
              ballX - BALL_RADIUS < brickX + BRICK_WIDTH &&
              ballY + BALL_RADIUS > brickY &&
              ballY - BALL_RADIUS < brickY + BRICK_HEIGHT) {

            // 消除砖块
            bricks[r][c] = 0;
            score += 10;
            updateScoreAndLives();

            // 计算碰撞方向 (反弹)
            const overlapLeft = (ballX + BALL_RADIUS) - brickX;
            const overlapRight = (brickX + BRICK_WIDTH) - (ballX - BALL_RADIUS);
            const overlapTop = (ballY + BALL_RADIUS) - brickY;
            const overlapBottom = (brickY + BRICK_HEIGHT) - (ballY - BALL_RADIUS);

            const minOverlapX = Math.min(overlapLeft, overlapRight);
            const minOverlapY = Math.min(overlapTop, overlapBottom);

            // 根据最小重叠方向反弹
            if (minOverlapX < minOverlapY) {
              // 水平反弹
              ballDX = -ballDX;
              // 微调位置防止卡砖
              if (overlapLeft < overlapRight) ballX = brickX - BALL_RADIUS - 0.1;
              else ballX = brickX + BRICK_WIDTH + BALL_RADIUS + 0.1;
            } else {
              // 垂直反弹
              ballDY = -ballDY;
              if (overlapTop < overlapBottom) ballY = brickY - BALL_RADIUS - 0.1;
              else ballY = brickY + BRICK_HEIGHT + BALL_RADIUS + 0.1;
            }

            // 一次只处理一个砖块碰撞,避免多重反射
            return;
          }
        }
      }
    }

    // 更新游戏逻辑 (每帧)
    function update() {
      if (gameOver || gameWin || paused) return;

      // 移动挡板
      if (leftPressed && paddleX > 0) {
        paddleX -= PADDLE_SPEED;
      }
      if (rightPressed && paddleX < CW - PADDLE_WIDTH) {
        paddleX += PADDLE_SPEED;
      }
      // 边界钳制
      paddleX = Math.max(0, Math.min(CW - PADDLE_WIDTH, paddleX));

      // 如果球未发射,跟随挡板
      if (!ballLaunched) {
        ballX = paddleX + PADDLE_WIDTH / 2;
        ballY = PADDLE_Y - BALL_RADIUS - 2;
        return;
      }

      // 移动球
      ballX += ballDX;
      ballY += ballDY;

      // 左右墙壁反弹
      if (ballX - BALL_RADIUS < 0) {
        ballX = BALL_RADIUS;
        ballDX = -ballDX;
      } else if (ballX + BALL_RADIUS > CW) {
        ballX = CW - BALL_RADIUS;
        ballDX = -ballDX;
      }

      // 顶部反弹
      if (ballY - BALL_RADIUS < 0) {
        ballY = BALL_RADIUS;
        ballDY = -ballDY;
      }

      // 挡板碰撞 (只有球向下移动且尚未丢失)
      if (ballDY > 0 && ballY + BALL_RADIUS >= PADDLE_Y && ballY - BALL_RADIUS <= PADDLE_Y + PADDLE_HEIGHT) {
        // 检查水平重叠
        if (ballX + BALL_RADIUS > paddleX && ballX - BALL_RADIUS < paddleX + PADDLE_WIDTH) {
          // 根据击中位置改变水平方向
          const hitPos = (ballX - paddleX) / PADDLE_WIDTH;  // 0~1
          const angle = (hitPos - 0.5) * 1.8;  // -0.9~0.9 弧度
          const speed = Math.sqrt(ballDX * ballDX + ballDY * ballDY);
          // 保持速度,重新设置方向 (基于角度)
          const newSpeed = Math.max(speed, BASE_SPEED * 0.9); // 防止速度过慢
          ballDX = Math.sin(angle) * newSpeed;
          ballDY = -Math.cos(angle) * newSpeed;  // 向上

          // 把球置于挡板上方,避免卡住
          ballY = PADDLE_Y - BALL_RADIUS - 0.5;
        }
      }

      // 砖块碰撞
      handleBrickCollision();

      // 检查胜利 (所有砖块消除)
      if (checkWin()) {
        gameWin = true;
        return;
      }

      // 球掉落底部
      if (ballY - BALL_RADIUS > CH) {
        lives--;
        updateScoreAndLives();
        if (lives <= 0) {
          gameOver = true;
          ballLaunched = false; // 停止显示移动,但让球在挡板上? 实际gameOver会停止更新。
        } else {
          // 重置球和挡板,等待发射
          resetBallAndPaddle();
        }
      }
    }

    // 绘制圆角矩形辅助
    function roundRect(x, y, w, h, radius) {
      ctx.beginPath();
      ctx.moveTo(x + radius, y);
      ctx.lineTo(x + w - radius, y);
      ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
      ctx.lineTo(x + w, y + h - radius);
      ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
      ctx.lineTo(x + radius, y + h);
      ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
      ctx.lineTo(x, y + radius);
      ctx.quadraticCurveTo(x, y, x + radius, y);
      ctx.closePath();
    }

    // 渲染所有元素
    function draw() {
      // 清空画布
      ctx.clearRect(0, 0, CW, CH);

      // 绘制背景网格(装饰)
      ctx.fillStyle = '#0c141c';
      ctx.fillRect(0, 0, CW, CH);
      ctx.strokeStyle = '#1b2f3a';
      ctx.lineWidth = 1;
      for (let i = 0; i < CW; i += 40) {
        ctx.beginPath();
        ctx.moveTo(i, 0);
        ctx.lineTo(i, CH);
        ctx.strokeStyle = '#182a33';
        ctx.stroke();
      }
      for (let i = 0; i < CH; i += 40) {
        ctx.beginPath();
        ctx.moveTo(0, i);
        ctx.lineTo(CW, i);
        ctx.strokeStyle = '#182a33';
        ctx.stroke();
      }

      // 绘制砖块 (带立体效果)
      for (let r = 0; r < BRICK_ROWS; r++) {
        for (let c = 0; c < BRICK_COLS; c++) {
          if (bricks[r][c] === 0) continue;
          const brickX = BRICKS_OFFSET_X + c * (BRICK_WIDTH + BRICK_GAP);
          const brickY = BRICKS_OFFSET_Y + r * (BRICK_HEIGHT + BRICK_GAP);
          const color = brickColors[r % brickColors.length];

          // 主色块
          ctx.fillStyle = color;
          roundRect(brickX, brickY, BRICK_WIDTH, BRICK_HEIGHT, 6);
          ctx.fill();

          // 内高光
          ctx.fillStyle = 'rgba(255,255,255,0.25)';
          roundRect(brickX + 2, brickY + 2, BRICK_WIDTH - 4, 4, 3);
          ctx.fill();

          // 底部阴影
          ctx.fillStyle = 'rgba(0,0,0,0.35)';
          roundRect(brickX + 2, brickY + BRICK_HEIGHT - 6, BRICK_WIDTH - 4, 4, 3);
          ctx.fill();
        }
      }

      // 绘制挡板 (金属质感)
      const paddleGradient = ctx.createLinearGradient(paddleX, PADDLE_Y, paddleX, PADDLE_Y + PADDLE_HEIGHT);
      paddleGradient.addColorStop(0, '#e5f0fa');
      paddleGradient.addColorStop(0.5, '#9bb7cc');
      paddleGradient.addColorStop(1, '#5d7f99');
      ctx.shadowColor = '#3fc4ff';
      ctx.shadowBlur = 12;
      ctx.fillStyle = paddleGradient;
      roundRect(paddleX, PADDLE_Y, PADDLE_WIDTH, PADDLE_HEIGHT, 10);
      ctx.fill();
      // 装饰
      ctx.shadowBlur = 0;
      ctx.shadowColor = 'transparent';
      ctx.fillStyle = '#d9ecff';
      roundRect(paddleX + 4, PADDLE_Y + 2, PADDLE_WIDTH - 8, 4, 4);
      ctx.fill();

      // 绘制球 (发光效果)
      ctx.shadowColor = '#6ec8ff';
      ctx.shadowBlur = 18;
      ctx.beginPath();
      ctx.arc(ballX, ballY, BALL_RADIUS, 0, Math.PI * 2);
      const ballGradient = ctx.createRadialGradient(ballX - 3, ballY - 3, 2, ballX, ballY, BALL_RADIUS + 2);
      ballGradient.addColorStop(0, '#ffffff');
      ballGradient.addColorStop(1, '#4db8ff');
      ctx.fillStyle = ballGradient;
      ctx.fill();
      // 内圈高光
      ctx.shadowBlur = 0;
      ctx.shadowColor = 'transparent';
      ctx.beginPath();
      ctx.arc(ballX - 2, ballY - 2, 3, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(255,255,255,0.8)';
      ctx.fill();

      // 绘制状态文字 (暂停 / 游戏结束 / 胜利)
      ctx.font = 'bold 36px "Courier New", Courier, monospace';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.shadowColor = 'black';
      ctx.shadowBlur = 12;

      if (gameOver) {
        ctx.fillStyle = '#ff8a8a';
        ctx.fillText('GAME OVER', CW / 2, CH / 2 - 20);
        ctx.font = 'bold 20px "Courier New", monospace';
        ctx.fillStyle = '#b0d0e0';
        ctx.fillText('点击"重开"继续', CW / 2, CH / 2 + 35);
      } else if (gameWin) {
        ctx.fillStyle = '#c6ffb3';
        ctx.fillText('胜利!', CW / 2, CH / 2 - 20);
        ctx.font = 'bold 20px "Courier New", monospace';
        ctx.fillStyle = '#b0d0e0';
        ctx.fillText('点击"重开"再来一局', CW / 2, CH / 2 + 35);
      } else if (paused && ballLaunched) {
        ctx.fillStyle = '#f0e68c';
        ctx.fillText('暂停', CW / 2, CH / 2);
      } else if (!ballLaunched && !gameOver && !gameWin) {
        // 提示发射
        ctx.font = 'bold 22px "Courier New", monospace';
        ctx.fillStyle = '#aaccdd';
        ctx.shadowBlur = 12;
        ctx.fillText('按 空格 发射球', CW / 2, CH / 2 + 70);
      }

      // 重置阴影
      ctx.shadowBlur = 0;
      ctx.shadowColor = 'transparent';
    }

    // 游戏循环
    function gameLoop() {
      update();
      draw();
      requestAnimationFrame(gameLoop);
    }

    // 键盘事件
    function handleKeyDown(e) {
      const key = e.key;
      if (key === 'ArrowLeft' || key === 'Left' || key === 'a' || key === 'A') {
        leftPressed = true;
        e.preventDefault();
      } else if (key === 'ArrowRight' || key === 'Right' || key === 'd' || key === 'D') {
        rightPressed = true;
        e.preventDefault();
      } else if (key === ' ') {
        e.preventDefault();  // 阻止页面滚动

        if (gameOver || gameWin) {
          // 游戏结束或胜利时,空格无效(提醒用按钮)
          return;
        }

        // 空格: 如果未发射则发射,如果已发射则切换暂停
        if (!ballLaunched) {
          launchBall();
        } else {
          paused = !paused;
        }
      }
    }

    function handleKeyUp(e) {
      const key = e.key;
      if (key === 'ArrowLeft' || key === 'Left' || key === 'a' || key === 'A') {
        leftPressed = false;
        e.preventDefault();
      } else if (key === 'ArrowRight' || key === 'Right' || key === 'd' || key === 'D') {
        rightPressed = false;
        e.preventDefault();
      }
    }

    // 防止页面滚动 (针对方向键和空格)
    window.addEventListener('keydown', function(e) {
      if ([' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
        e.preventDefault();
      }
    }, false);

    // 鼠标/触摸移动挡板 (增加便利)
    function handleMouseMove(e) {
      const rect = canvas.getBoundingClientRect();
      const scaleX = CW / rect.width;
      const mouseX = (e.clientX - rect.left) * scaleX;
      // 挡板中心跟随鼠标
      let newPaddleX = mouseX - PADDLE_WIDTH / 2;
      newPaddleX = Math.max(0, Math.min(CW - PADDLE_WIDTH, newPaddleX));
      paddleX = newPaddleX;

      // 如果球未发射,同步球位置
      if (!ballLaunched) {
        ballX = paddleX + PADDLE_WIDTH / 2;
        ballY = PADDLE_Y - BALL_RADIUS - 2;
      }
    }

    // 触摸事件
    function handleTouchMove(e) {
      e.preventDefault();
      const rect = canvas.getBoundingClientRect();
      const touch = e.touches[0];
      if (!touch) return;
      const scaleX = CW / rect.width;
      const touchX = (touch.clientX - rect.left) * scaleX;
      let newPaddleX = touchX - PADDLE_WIDTH / 2;
      newPaddleX = Math.max(0, Math.min(CW - PADDLE_WIDTH, newPaddleX));
      paddleX = newPaddleX;

      if (!ballLaunched) {
        ballX = paddleX + PADDLE_WIDTH / 2;
        ballY = PADDLE_Y - BALL_RADIUS - 2;
      }
    }

    // 初始化事件监听
    function initEvents() {
      document.addEventListener('keydown', handleKeyDown);
      document.addEventListener('keyup', handleKeyUp);
      canvas.addEventListener('mousemove', handleMouseMove);
      canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
      canvas.addEventListener('touchstart', (e) => e.preventDefault(), { passive: false });
      restartBtn.addEventListener('click', () => {
        resetGame();
      });
    }

    // 启动游戏
    function init() {
      initBrickColors();
      resetGame(); // 包含重置砖块、球、分数、生命
      initEvents();
      gameLoop();
    }

    init();
  })();
</script>
</body>
</html>

七、效果截图

相关推荐
IT_陈寒27 分钟前
Vite的HMR把我整不会了:一个生产环境的热更新坑
前端·人工智能·后端
Mh38 分钟前
如何使用双缓冲优化视频切换
前端
risenav44 分钟前
低头蓄力,抬头起跳:《萤虫跳跳》的 Rokid AIUI 开发实战
前端
ZhengEnCi1 小时前
如何看待 Fable 5.2 用纯代码生成 1.4MB 视觉作品:光追、水面、倒影、火光的程序化生成原理
前端
再吃一根胡萝卜2 小时前
DSH 插件的 Host 与 Client:为什么「只加载了 Host」,以及要不要自己写 Client 加载器
前端
parade岁月2 小时前
AI 写的前端代码为什么总像 5 年前的?Chrome 官方出手了
前端
tryCbest3 小时前
Vue系列——第 15 篇:内置组件全家桶——Transition、Teleport、Suspense、KeepAlive
前端·vue.js
易师傅3 小时前
技术面试已死?AI 时代,真正拉开程序员差距的是什么?
前端·后端·程序员
jay神3 小时前
基于深度学习的交通流量预测系统(Web 版)
前端·人工智能·深度学习·yolo·计算机毕业设计