Canvas 打砖块:挡板跟鼠标走、小球物理反弹、砖块击碎、三条命。重点学 requestAnimationFrame 游戏循环和碰撞检测。

1. 游戏循环与挡板

用 requestAnimationFrame 每帧执行"更新→绘制"。挡板用 mousemove 跟鼠标:

const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = 640, H = 480;
let px = (W - 100) / 2;
let ball = { x: W / 2, y: H - 40, vx: 4, vy: -5, r: 8 };

cv.addEventListener('mousemove', e => {
  const rect = cv.getBoundingClientRect();
  px = e.clientX - rect.left - 50;
  if (px < 0) px = 0;
  if (px > W - 100) px = W - 100;
});

function loop() {
  moveBall();
  hitTest();
  draw();
  requestAnimationFrame(loop);
}

2. 反弹与碰撞

撞左右墙翻 vx,撞顶翻 vy;挡板按命中位置调角度;砖块相交就翻 vy 并移除:

function moveBall() {
  ball.x += ball.vx;
  ball.y += ball.vy;
  if (ball.x < ball.r || ball.x > W - ball.r) ball.vx = -ball.vx;
  if (ball.y < ball.r) ball.vy = -ball.vy;
  if (ball.vy > 0 && ball.y + ball.r >= H - 14 &&
      ball.x >= px && ball.x <= px + 100) {
    const off = (ball.x - (px + 50)) / 50;
    const sp = Math.hypot(ball.vx, ball.vy);
    ball.vx = sp * off * 0.8;
    ball.vy = -Math.sqrt(Math.max(sp * sp - ball.vx * ball.vx, 0.01));
  }
}

function hitTest() {
  for (let i = 0; i < bricks.length; i++) {
    const b = bricks[i];
    if (ball.x + ball.r > b.x && ball.x - ball.r < b.x + b.w &&
        ball.y + ball.r > b.y && ball.y - ball.r < b.y + b.h) {
      ball.vy = -ball.vy;
      bricks.splice(i, 1);
      score += 10;
      i--;
    }
  }
}

3. 完整代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>打砖块</title>
<style>
  body{font-family:sans-serif;text-align:center;background:#222;color:#eee;margin:0}
  canvas{background:#0e0e1c;cursor:none}
</style>
</head>
<body>
<h1>打砖块 <small>鼠标移动挡板</small></h1>
<canvas id="cv" width="640" height="480"></canvas>
<div id="info"></div>
<script>
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = 640, H = 480;
let px = (W - 100) / 2;
let ball = { x: W / 2, y: H - 40, vx: 4, vy: -5, r: 8 };
let bricks = [];
let score = 0, lives = 3, over = false;

cv.addEventListener('mousemove', e => {
  const rect = cv.getBoundingClientRect();
  px = e.clientX - rect.left - 50;
  if (px < 0) px = 0;
  if (px > W - 100) px = W - 100;
});

function buildLevel() {
  bricks = [];
  for (let r = 0; r < 4; r++)
    for (let c = 0; c < 9; c++)
      bricks.push({ x: 30 + c * 66, y: 40 + r * 24, w: 60, h: 18 });
}

function moveBall() {
  ball.x += ball.vx;
  ball.y += ball.vy;
  if (ball.x < ball.r || ball.x > W - ball.r) ball.vx = -ball.vx;
  if (ball.y < ball.r) ball.vy = -ball.vy;
  if (ball.vy > 0 && ball.y + ball.r >= H - 14 &&
      ball.x >= px && ball.x <= px + 100) {
    const off = (ball.x - (px + 50)) / 50;
    const sp = Math.hypot(ball.vx, ball.vy);
    ball.vx = sp * off * 0.8;
    ball.vy = -Math.sqrt(Math.max(sp * sp - ball.vx * ball.vx, 0.01));
  }
}

function hitTest() {
  for (let i = 0; i < bricks.length; i++) {
    const b = bricks[i];
    if (ball.x + ball.r > b.x && ball.x - ball.r < b.x + b.w &&
        ball.y + ball.r > b.y && ball.y - ball.r < b.y + b.h) {
      ball.vy = -ball.vy;
      bricks.splice(i, 1);
      score += 10;
      i--;
    }
  }
}

function draw() {
  ctx.fillStyle = '#0e0e1c';
  ctx.fillRect(0, 0, W, H);
  ctx.fillStyle = '#eee';
  ctx.fillRect(px, H - 14, 100, 14);
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
  ctx.fillStyle = '#ff7a3c';
  ctx.fill();
  ctx.fillStyle = '#e04848';
  for (const b of bricks) ctx.fillRect(b.x, b.y, b.w, b.h);
  document.getElementById('info').textContent =
    '分数: ' + score + '  生命: ' + lives;
}

function loop() {
  if (over) return;
  moveBall();
  hitTest();
  if (ball.y > H) {
    lives--;
    if (lives <= 0) {
      over = true;
      document.getElementById('info').textContent =
        '游戏结束!最终分数: ' + score + ' (刷新重玩)';
      return;
    }
    ball = { x: W / 2, y: H - 40, vx: 4, vy: -5, r: 8 };
  }
  if (!bricks.length) { buildLevel(); ball = { x: W / 2, y: H - 40, vx: 4, vy: -5, r: 8 }; }
  draw();
  requestAnimationFrame(loop);
}

buildLevel();
loop();
</script>
</body>
</html>

4. 运行与常见问题

保存为 breakout.html 用浏览器打开。常见问题:①球水平弹——限制最小 vy;②砖块穿球——检测到碰撞就移除并 i--;③挡板跳——用 getBoundingClientRect 换算坐标。

💡 改进方向:①粒子特效;②道具;③关卡推进;④音效;⑤最高分存 localStorage。