一、教程
本教程详细讲解了基于HTML5的现代俄罗斯方块实现方案。通过模块化设计将系统分为游戏板渲染、方块处理、碰撞检测和用户交互四大核心模块,采用HTML/CSS构建响应式界面,结合JavaScript实现游戏逻辑。重点阐述了七种经典方块的矩阵定义与旋转算法、基于二维数组的行消除机制、动态难度调节策略以及玻璃质感UI的实现技巧。教程包含完整的初始化流程、键盘事件处理方案和游戏结束判断逻辑,并提供了音效集成、本地存储高分等扩展建议,为开发者构建兼具经典玩法与现代视觉风格的俄罗斯方块提供了完整技术方案。(完整代码见文末)
1. 项目结构设计
1.1 HTML结构:
<div class="game-container">
<canvas id="game-board"></canvas> <!-- 主游戏区 -->
<div class="side-panel"> <!-- 右侧信息面板 -->
<得分显示>
<下一个方块预览>
<操作说明>
<开始按钮>
</div>
</div>
<div class="game-over">...</div> <!-- 游戏结束弹窗 -->
1.2 CSS样式:
- 使用Flex布局实现响应式界面
- 霓虹灯风格的边框和阴影
- 透明玻璃效果的面板设计
1.3. JavaScript模块:
- 游戏逻辑控制
- 方块处理类
- 碰撞检测系统
- 得分计算
2. 游戏初始化
// 画布设置
const BLOCK_SIZE = 30; // 每个方块像素大小
const BOARD_WIDTH = 10; // 横向10格
const BOARD_HEIGHT = 20;// 纵向20格
// 游戏板数据结构
let board = Array(BOARD_HEIGHT).fill().map(() => Array(BOARD_WIDTH).fill(0));
// 初始化游戏
function startGame() {
// 重置游戏状态
board = [...];
currentPiece = new Piece();
nextPiece = new Piece();
gameLoop = setInterval(update, gameSpeed);
}
3. 方块处理系统
3.1 方块定义:
const SHAPES = [ // 7种经典形状
[[1,1,1,1]], // I型
[[1,1,1],[0,1,0]], // T型
// ...其他形状
];
const COLORS = [ // 对应的柔和色系
"#8ecfd6", // 蓝绿色
"#f2c091", // 淡杏色
// ...其他颜色
];
3.2 方块类实现:
class Piece {
constructor() {
this.shape = SHAPES[randomIndex];
this.color = COLORS[shapeIndex];
this.x = 4; // 初始居中位置
this.y = 0;
}
rotate() {
// 矩阵转置实现旋转
const newShape = this.shape[0].map((_, i) =>
this.shape.map(row => row[i]).reverse()
);
}
collision(offsetX, offsetY) {
// 边界检测和碰撞检测
}
}
4. 游戏核心逻辑
4.1 游戏循环:
function update() {
// 下落逻辑
if (!collision) {
piece.y++;
} else {
mergeToBoard();
clearLines();
spawnNewPiece();
}
// 速度控制
gameSpeed = Math.max(100, 800 - score * 0.5);
}
4.2 行消除逻辑:
function clearLines() {
for (let i = BOARD_HEIGHT-1; i >=0; i--) {
if (board[i].every(cell => cell)) {
board.splice(i, 1);
board.unshift(Array(BOARD_WIDTH).fill(0));
linesCleared++;
}
}
// 得分计算
score += linesCleared * 100;
}
5. 用户输入处理
document.addEventListener("keydown", (e) => {
switch(e.key) {
case "ArrowLeft": // 左移
if (!collision(-1,0)) piece.x--;
break;
case "ArrowRight": // 右移
if (!collision(1,0)) piece.x++;
break;
case "ArrowUp": // 旋转
piece.rotate();
break;
case " ": // 瞬降
hardDrop();
break;
}
drawBoard();
});
6. 游戏结束处理
function gameOver() {
clearInterval(gameLoop);
document.getElementById("gameOver").style.display = "block";
}
// 检测条件:新方块生成时发生碰撞
if (currentPiece.collision(0,0)) {
gameOver();
}
7. 高级功能实现技巧
7.1 预览方块绘制:
function drawNextPiece() {
// 计算居中显示
const size = nextCanvas.width / 4;
nextPiece.shape.forEach((row, i) => {
nextCtx.fillRect(
j * size + ((4 - row.length)*size)/2,
i * size + ((4 - shape.length)*size)/2,
size-2, size-2
);
});
}
7.2 动态难度调整:
// 得分越高下落速度越快
gameSpeed = 800 - Math.floor(score / 1000) * 100;
7.3 特效实现:
- 使用CSS transition实现按钮动画
- 添加玻璃效果的面板背景
8. 运行与调试
8.1 启动方式:
// 页面加载后自动开始
window.onload = startGame;
8.2 常见问题解决:
- 碰撞检测失效:检查矩阵索引计算
- 旋转越界:添加墙踢(wall kick)逻辑
- 渲染闪烁:使用双缓冲技术
二、完整代码
将代码添加到.html文件中即可直接运行。
(最原始的运行方式:新建文本文件.txt,粘贴代码,保存文件后将文件后缀.txt改为.html)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>现代俄罗斯方块</title>
<link
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap"
rel="stylesheet"
/>
<style>
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #ffffff; /* 修改为白色背景 */
color: #333; /* 文字颜色改为深灰色以适应白色背景 */
font-family: "Orbitron", sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
}
.game-container {
display: flex;
gap: 40px;
background: rgba(240, 240, 240, 0.9); /* 修改为浅灰色背景 */
padding: 30px;
border-radius: 20px;
box-shadow: 0 0 40px rgba(0, 150, 150, 0.3); /* 调整阴影颜色 */
border: 2px solid rgba(255, 255, 255, 0.1);
}
#game-board {
border: 4px solid #00ffff;
border-radius: 10px;
background: rgba(255, 255, 255, 0.9);
}
.side-panel {
display: flex;
flex-direction: column;
gap: 20px;
width: 200px;
}
.info-box {
background: rgba(255, 255, 255, 0.5);
padding: 20px;
border-radius: 10px;
border: 2px solid #00ffff;
text-align: center;
}
.info-box h3 {
font-size: 18px;
margin-bottom: 10px;
color: #00ffff;
}
#next-piece {
width: 120px;
height: 120px;
margin: 0 auto;
border: 2px solid #00ff00;
border-radius: 10px;
background: rgba(255, 255, 255, 0.7);
}
#score {
font-size: 24px;
color: rgb(255, 131, 255);
}
.controls {
margin-top: 20px;
}
.controls p {
font-size: 14px;
color: #ccc;
line-height: 1.6;
}
button {
background: linear-gradient(
135deg,
rgb(142, 255, 255),
rgb(145, 227, 255)
);
border: none;
padding: 12px 24px;
font-family: "Orbitron", sans-serif;
font-size: 16px;
color: #fff;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s ease;
margin-top: 20px;
}
button:hover {
background: linear-gradient(
135deg,
rgb(255, 142, 255),
rgb(255, 122, 193)
);
transform: scale(1.05);
}
.game-over {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9);
padding: 40px;
border-radius: 20px;
text-align: center;
border: 4px solidrgb(255, 95, 95);
display: none;
z-index: 1000;
}
.game-over h2 {
font-size: 32px;
color: rgb(255, 95, 95);
margin-bottom: 20px;
}
.game-over p {
font-size: 24px;
color: #fff;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="game-board" width="300" height="600"></canvas>
<div class="side-panel">
<div class="info-box">
<h3>得分</h3>
<div id="score">0</div>
</div>
<div class="info-box">
<h3>下一个方块</h3>
<canvas id="next-piece" width="120" height="120"></canvas>
</div>
<div class="info-box controls">
<h3>控制</h3>
<p>
← 左移<br />
→ 右移<br />
↑ 旋转<br />
↓ 加速<br />
空格 瞬降
</p>
</div>
<button onclick="startGame()">新游戏</button>
</div>
</div>
<div class="game-over" id="gameOver">
<h2>游戏结束!</h2>
<p>最终得分: <span id="finalScore">0</span></p>
<button onclick="startGame()">再玩一次</button>
</div>
<script>
// 获取游戏主画布和上下文
const canvas = document.getElementById("game-board");
const ctx = canvas.getContext("2d");
// 获取显示下一个方块的画布和上下文
const nextCanvas = document.getElementById("next-piece");
const nextCtx = nextCanvas.getContext("2d");
// 定义方块大小和游戏区域的宽高
const BLOCK_SIZE = 30;
const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
// 定义七种俄罗斯方块的形状
const SHAPES = [
[[1, 1, 1, 1]], // I形方块 - 长条
[
[1, 1, 1],
[0, 1, 0],
], // T形方块
[
[1, 1, 1],
[1, 0, 0],
], // L形方块
[
[1, 1, 1],
[0, 0, 1],
], // J形方块 - L的镜像
[
[1, 1],
[1, 1],
], // O形方块 - 正方形
[
[1, 1, 0],
[0, 1, 1],
], // S形方块
[
[0, 1, 1],
[1, 1, 0],
], // Z形方块 - S的镜像
];
// 定义每种方块对应的颜色
const COLORS = [
"#8ecfd6", // 蓝绿色 - I形方块
"#f2c091", // 淡杏色 - T形方块
"#d4a5c7", // 淡紫色 - L形方块
"#a6b1e1", // 淡蓝紫色 - J形方块
"#e6daa6", // 淡黄色 - O形方块
"#b5d99c", // 淡绿色 - S形方块
"#e6b8b8", // 淡粉色 - Z形方块
];
// 初始化游戏板 - 二维数组,0表示空,非0表示有方块及其颜色
let board = Array(BOARD_HEIGHT)
.fill()
.map(() => Array(BOARD_WIDTH).fill(0));
// 当前正在下落的方块
let currentPiece = null;
// 下一个将要出现的方块
let nextPiece = null;
// 游戏得分
let score = 0;
// 游戏循环的计时器ID
let gameLoop = null;
// 方块下落的速度(毫秒),分数越高速度越快
let gameSpeed = 800;
// 方块类 - 处理方块的移动、旋转和碰撞检测
class Piece {
constructor(shapeIndex) {
// 设置方块形状
this.shape = SHAPES[shapeIndex];
// 设置方块颜色
this.color = COLORS[shapeIndex];
// 计算初始X坐标,使方块出现在顶部中间
this.x =
Math.floor(BOARD_WIDTH / 2) - Math.floor(this.shape[0].length / 2);
// 初始Y坐标为0(顶部)
this.y = 0;
}
// 旋转方块 - 顺时针旋转90度
rotate() {
// 创建旋转后的新形状
const newShape = this.shape[0].map((_, i) =>
this.shape.map((row) => row[i]).reverse()
);
// 检查旋转后是否会发生碰撞,如果不会则应用旋转
if (!this.collision(0, 0, newShape)) {
this.shape = newShape;
}
}
// 碰撞检测 - 检查方块是否与边界或其他方块碰撞
collision(offsetX, offsetY, shape = this.shape) {
return shape.some((row, i) =>
row.some((cell, j) => {
// 计算新位置
const newX = this.x + j + offsetX;
const newY = this.y + i + offsetY;
return (
cell && // 只检查方块实际存在的部分
(newX < 0 || // 左边界碰撞
newX >= BOARD_WIDTH || // 右边界碰撞
newY >= BOARD_HEIGHT || // 底部碰撞
(newY >= 0 && board[newY][newX])) // 与已有方块碰撞
);
})
);
}
}
// 绘制游戏板和当前方块
function drawBoard() {
// 清空画布,填充白色背景
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制已固定在游戏板上的方块
board.forEach((row, i) => {
row.forEach((cell, j) => {
if (cell) {
ctx.fillStyle = cell;
ctx.fillRect(
j * BLOCK_SIZE,
i * BLOCK_SIZE,
BLOCK_SIZE - 1,
BLOCK_SIZE - 1
);
}
});
});
// 绘制当前正在下落的方块
if (currentPiece) {
ctx.fillStyle = currentPiece.color;
currentPiece.shape.forEach((row, i) => {
row.forEach((cell, j) => {
if (cell) {
ctx.fillRect(
(currentPiece.x + j) * BLOCK_SIZE,
(currentPiece.y + i) * BLOCK_SIZE,
BLOCK_SIZE - 1,
BLOCK_SIZE - 1
);
}
});
});
}
}
// 绘制下一个将要出现的方块
function drawNextPiece() {
// 清空下一个方块的画布
nextCtx.fillStyle = "#fff";
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
// 计算方块大小并居中显示
const size = nextCanvas.width / 4;
nextCtx.fillStyle = nextPiece.color;
nextPiece.shape.forEach((row, i) => {
row.forEach((cell, j) => {
if (cell) {
nextCtx.fillRect(
j * size + ((4 - row.length) * size) / 2, // 水平居中
i * size + ((4 - nextPiece.shape.length) * size) / 2, // 垂直居中
size - 2, // 留出1像素边距
size - 2
);
}
});
});
}
// 将当前方块合并到游戏板上
function mergePiece() {
currentPiece.shape.forEach((row, i) => {
row.forEach((cell, j) => {
if (cell) {
// 将方块的颜色保存到游戏板对应位置
board[currentPiece.y + i][currentPiece.x + j] =
currentPiece.color;
}
});
});
}
// 清除已填满的行并计分
function clearLines() {
let linesCleared = 0;
// 从底部向上检查每一行
for (let i = BOARD_HEIGHT - 1; i >= 0; i--) {
// 检查当前行是否已填满
if (board[i].every((cell) => cell)) {
// 移除已填满的行
board.splice(i, 1);
// 在顶部添加新的空行
board.unshift(Array(BOARD_WIDTH).fill(0));
linesCleared++;
// 由于删除了一行,需要重新检查当前位置
i++;
}
}
// 如果有行被清除,更新分数和游戏速度
if (linesCleared) {
// 每清除一行得100分
score += linesCleared * 100;
document.getElementById("score").textContent = score;
// 随着分数增加,游戏速度加快
gameSpeed = Math.max(100, 800 - score * 0.5);
}
}
// 游戏结束处理
function gameOver() {
// 停止游戏循环
clearInterval(gameLoop);
// 显示游戏结束界面
document.getElementById("gameOver").style.display = "block";
// 显示最终得分
document.getElementById("finalScore").textContent = score;
}
// 游戏主循环 - 更新游戏状态
function update() {
// 检查当前方块是否可以下落
if (!currentPiece.collision(0, 1)) {
// 如果可以,则下移一格
currentPiece.y++;
} else {
// 如果不能下落,则固定当前方块
mergePiece();
// 清除已填满的行并计分
clearLines();
// 当前方块变为下一个方块
currentPiece = nextPiece;
// 生成新的下一个方块
nextPiece = new Piece(Math.floor(Math.random() * SHAPES.length));
// 更新下一个方块的显示
drawNextPiece();
// 检查游戏是否结束 - 新方块一出现就发生碰撞
if (currentPiece.collision(0, 0)) {
gameOver();
}
}
// 重绘游戏板
drawBoard();
}
// 开始新游戏
function startGame() {
// 重置游戏板
board = Array(BOARD_HEIGHT)
.fill()
.map(() => Array(BOARD_WIDTH).fill(0));
// 重置分数
score = 0;
// 重置游戏速度
gameSpeed = 800;
// 更新分数显示
document.getElementById("score").textContent = "0";
// 隐藏游戏结束界面
document.getElementById("gameOver").style.display = "none";
// 创建当前方块和下一个方块
currentPiece = new Piece(Math.floor(Math.random() * SHAPES.length));
nextPiece = new Piece(Math.floor(Math.random() * SHAPES.length));
// 显示下一个方块
drawNextPiece();
// 清除之前的游戏循环
if (gameLoop) clearInterval(gameLoop);
// 启动新的游戏循环
gameLoop = setInterval(update, gameSpeed);
}
// 监听键盘事件
document.addEventListener("keydown", (e) => {
if (!currentPiece) return;
switch (e.key) {
case "ArrowLeft": // 左移
if (!currentPiece.collision(-1, 0)) currentPiece.x--;
break;
case "ArrowRight": // 右移
if (!currentPiece.collision(1, 0)) currentPiece.x++;
break;
case "ArrowDown": // 加速下落
if (!currentPiece.collision(0, 1)) currentPiece.y++;
break;
case "ArrowUp": // 旋转
currentPiece.rotate();
break;
case " ": // 空格键 - 瞬间下落到底部
while (!currentPiece.collision(0, 1)) {
currentPiece.y++;
}
update();
break;
}
// 重绘游戏板
drawBoard();
});
// 游戏初始化
startGame();
</script>
</body>
</html>
转载吱一声~