Hexo 博客实战:俄罗斯方块小游戏开发指南

前言

继上一篇文章介绍贪吃蛇小游戏的实现方案后,本文继续讲解俄罗斯方块游戏的开发。相比贪吃蛇,俄罗斯方块的实现更加复杂,涉及方块旋转、多行消除、速度递增等机制。

通过这个案例,可以学习到如何用原生 JavaScript 实现经典的方块下落消除游戏。

项目结构

游戏放置在 source/ai-games/tetris/ 目录下,文件结构如下:

1
2
3
source/ai-games/tetris/
├── index.md # 游戏页面(HTML + 样式)
└── tetris-game.js # 游戏核心逻辑(约394行)

与贪吃蛇相同的独立目录结构,便于管理和扩展更多游戏。

游戏页面实现

Canvas 画布

1
<canvas id="game-canvas" width="240" height="400"></canvas>

俄罗斯方块采用 12×20 网格,每个格子 20px,宽度较窄以适应竖屏布局。

样式设计

1
2
3
4
5
6
7
canvas {
border: 2px solid #2196F3;
border-radius: 8px;
background: #111;
box-shadow: 0 0 20px rgba(33, 150, 243, 0.3);
touch-action: none;
}

使用蓝色主题(#2196F3)与贪吃蛇的绿色区分开。

核心逻辑实现

方块形状定义

游戏包含 7 种经典方块形状,用矩阵和颜色表示:

1
2
3
4
5
6
7
8
9
10
const SHAPES = {
I: { matrix: [[1,1,1,1]], color: '#00f5ff' }, // 青色长条
O: { matrix: [[1,1],[1,1]], color: '#ffeb3b' }, // 黄色方块
T: { matrix: [[0,1,0],[1,1,1]], color: '#bf5af2' }, // 紫色T形
S: { matrix: [[0,1,1],[1,1,0]], color: '#30d158' }, // 绿色S形
Z: { matrix: [[1,1,0],[0,1,1]], color: '#ff375f' }, // 红色Z形
J: { matrix: [[1,0,0],[1,1,1]], color: '#0a84ff' }, // 蓝色J形
L: { matrix: [[0,0,1],[1,1,1]], color: '#ff9f0a' } // 橙色L形
};
const SHAPE_KEYS = ['I', 'O', 'T', 'S', 'Z', 'J', 'L'];

每种方块用 0/1 矩阵表示形状,1 表示方块存在的位置。

游戏状态管理

1
2
3
4
5
6
let board = [];           // 12×20 游戏板,存储已落方块颜色
let currentPiece = null; // 当前下落的方块
let currentX = 0; // 当前方块X坐标
let currentY = 0; // 当前方块Y坐标
let score = 0; // 得分
let dropInterval = 800; // 下落间隔(毫秒)

board 是二维数组,存储已固定在底部的方块颜色,空位为 0。

随机生成方块

1
2
3
4
5
6
7
8
function randomPiece() {
const key = SHAPE_KEYS[Math.floor(Math.random() * SHAPE_KEYS.length)];
const shape = SHAPES[key];
return {
matrix: shape.matrix.map(row => [...row]), // 深拷贝矩阵
color: shape.color
};
}

使用 map(row => [...row]) 实现矩阵的深拷贝,避免修改原始形状定义。

碰撞检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function checkCollision(matrix, x, y) {
for (let r = 0; r < matrix.length; r++) {
for (let c = 0; c < matrix[r].length; c++) {
if (matrix[r][c]) {
const newX = x + c;
const newY = y + r;
// 边界检测
if (newX < 0 || newX >= COLS || newY >= ROWS) return true;
// 与已落方块碰撞检测
if (newY >= 0 && board[newY][newX]) return true;
}
}
}
return false;
}

遍历方块的每个单元格,检查是否越界或与已落方块重叠。

方块旋转算法

1
2
3
4
5
6
7
8
9
10
11
12
function rotate(matrix) {
const rows = matrix.length;
const cols = matrix[0].length;
const rotated = [];
for (let c = 0; c < cols; c++) {
rotated[c] = [];
for (let r = rows - 1; r >= 0; r--) {
rotated[c].push(matrix[r][c]);
}
}
return rotated;
}

旋转原理:将矩阵的列变为行,实现 90 度顺时针旋转。例如:

1
2
[[0,1,0],     [[1,1,1],
[1,1,1]] → [0,1,0]]

消除行逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function clearLines() {
let linesCleared = 0;
for (let r = ROWS - 1; r >= 0; r--) {
let full = true;
for (let c = 0; c < COLS; c++) {
if (!board[r][c]) {
full = false;
break;
}
}
if (full) {
board.splice(r, 1); // 移除该行
board.unshift(new Array(COLS).fill(EMPTY)); // 顶部插入空行
linesCleared++;
r++; // 重新检查当前位置(因为上方行下移了)
}
}
if (linesCleared > 0) {
score += linesCleared * 100 * linesCleared;
dropInterval = Math.max(100, 800 - Math.floor(score / 500) * 80);
}
}

消除算法:

  • 从底部向上扫描每一行
  • 如果某行全满,移除该行
  • 顶部插入新空行
  • 上方行自动下移
  • 计分:单行100分,双行400分,三行900分(平方递增)

固定方块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function lockPiece() {
for (let r = 0; r < currentPiece.matrix.length; r++) {
for (let c = 0; c < currentPiece.matrix[r].length; c++) {
if (currentPiece.matrix[r][c]) {
const y = currentY + r;
const x = currentX + c;
if (y >= 0 && y < ROWS && x >= 0 && x < COLS) {
board[y][x] = currentPiece.color;
}
}
}
}
clearLines();
newPiece();
}

将当前方块的每个单元格颜色写入游戏板,然后检查消除,最后生成新方块。

控制方式

键盘控制

1
2
3
4
5
6
7
8
9
10
11
12
function handleKeydown(e) {
switch (e.key) {
case 'ArrowUp': moveRotate(); break; // 旋转
case 'ArrowLeft': move(-1, 0); draw(); break; // 左移
case 'ArrowRight': move(1, 0); draw(); break; // 右移
case 'ArrowDown':
while (move(0, 1)) {} // 快速下落
lockPiece();
draw();
break;
}
}

按↓键时使用循环实现快速下落,直到无法继续为止。

触摸滑动控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
canvas.addEventListener('touchend', function(e) {
const deltaX = touchEndX - touchStartX;
const deltaY = touchEndY - touchStartY;

if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 水平滑动:左右移动
if (deltaX > 0) move(1, 0);
else move(-1, 0);
} else {
// 垂直滑动:上=旋转,下=快速下落
if (deltaY > 0) {
while (move(0, 1)) {}
lockPiece();
} else {
moveRotate();
}
}
draw();
});

触摸控制逻辑:

  • 水平滑动 → 左右移动
  • 向上滑动 → 旋转
  • 向下滑动 → 快速下落

速度递增机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function updateSpeed() {
if (timer) {
clearInterval(timer);
timer = setInterval(drop, dropInterval);
window._tetrisTimer = timer;
}
}

// 监听分数变化
setInterval(() => {
if (score !== lastScore) {
updateSpeed();
lastScore = score;
}
}, 100);

速度计算公式:dropInterval = Math.max(100, 800 - score / 500 * 80)

分数区间 下落间隔
0-499 800ms
500-999 720ms
1000-1499 640ms
3500+ 100ms(最快)

绘制系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 绘制网格线
// ... 绘制横竖线

// 绘制已落方块
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c]) {
ctx.fillStyle = board[r][c];
ctx.fillRect(c * CELL_SIZE + 1, r * CELL_SIZE + 1,
CELL_SIZE - 2, CELL_SIZE - 2);
}
}
}

// 绘制当前方块
if (currentPiece && !gameOver) {
// ... 绘制逻辑
}

// 游戏结束遮罩
if (gameOver) {
// ... 显示最终得分
}
}

总结

俄罗斯方块相比贪吃蛇增加了更多复杂性,核心要点总结:

功能 实现方式
方块形状 7 种形状 + 颜色定义
旋转 矩阵转置算法
碰撞检测 边界 + 已落方块检测
消除 行满检测 + 数组操作
速度 分数关联动态调整
控制 键盘 + 触摸滑动

游戏已集成到博客的小游戏栏目中,可以直接访问体验。


相关链接: