一、游戏核心机制
目标:用挡板反弹球,击碎画面上所有砖块即获胜;球掉落底部则损失一条生命,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>
