俄罗斯方块游戏技术解析:从前端实现到工程化思考
📚 一、项目架构概述

在前端开发领域,经典小游戏的实现是检验技术综合应用能力的重要方式,而俄罗斯方块作为家喻户晓的经典游戏,其浏览器端实现更是涵盖了 HTML 结构设计、CSS 视觉呈现、JavaScript 逻辑开发等全维度的前端核心能力。本文将以一个完整的 HTML5 + CSS3 + JavaScript 实现的俄罗斯方块游戏为样本,从架构设计到细节实现,从核心算法到性能优化,进行深度技术解析。
该项目采用最简洁的前端技术栈组合,无任何框架依赖,完全基于原生 Web 技术构建,整体由三个核心文件构成,各司其职又高度协同:
- index.html:作为游戏的骨架,定义了所有界面元素的结构布局,包括游戏标题、计分面板、操作说明、Canvas 渲染区域、状态遮罩层等,是整个游戏的界面基础。
- style.css:负责游戏的视觉呈现,融合了现代 CSS 特性,实现了响应式布局、毛玻璃视觉效果、交互动画、多端适配等,为用户提供沉浸式的视觉体验。
- script.js:承载游戏的核心逻辑,包括数据结构设计、碰撞检测、方块旋转、消行计算、游戏循环、状态管理等,是游戏能够正常运行的'大脑'。
整个项目遵循'结构 - 样式 - 逻辑'分离的前端开发最佳实践,代码解耦度高,便于维护和扩展,同时兼顾了性能与用户体验,是前端游戏开发入门的绝佳案例。
📚 二、HTML 结构分析:语义化与模块化的界面搭建
📘 2.1 整体布局设计
HTML 结构的设计核心在于'模块化'与'语义化',既要保证界面元素的清晰分层,也要让结构具备良好的可读性和可维护性。该项目的 HTML 布局采用嵌套式容器结构,整体层级如下:
Container(全局容器) ├── Header(标题区域):展示游戏名称,强化视觉焦点 └── Game-Wrapper(游戏主容器):承载所有游戏核心元素 ├── Left Sidebar(左侧功能面板) │ ├── Info-panel(信息面板):展示得分、等级、消除行数 │ └── Next-panel(预览面板):通过 Canvas 展示下一个方块 ├── Game Area(游戏核心区域) │ ├── game-canvas(主画布):渲染游戏主界面(10 列×20 行) │ ├── game-over(结束遮罩):游戏结束时的提示层 │ └── pause-overlay(暂停遮罩):游戏暂停时的提示层 └── Right Sidebar(右侧操作面板) ├── Controls-panel(操作说明):展示键盘操作指南 └── Button-group(控制按钮):开始、暂停、新游戏按钮
这种布局设计的优势在于:
- 分层清晰:将'信息展示''核心游戏区''操作控制'分离,符合用户的视觉和操作习惯;
- 职责单一:每个容器只承载一类功能,便于后续样式调整和逻辑绑定;
- 扩展灵活:如需新增功能(如音效开关、难度选择),可在对应侧边栏新增模块,不影响核心布局。
📘 2.2 关键元素解析
📖 2.2.1 Canvas 元素的双画布设计
项目中使用了两个<canvas>元素,分别承担不同的渲染职责:
- 主画布(game-canvas):尺寸为 300px×600px(对应 10 列×20 行,每个方块 30px),是游戏的核心渲染区域,负责绘制游戏面板、当前下落的方块、已放置的方块等;
- 预览画布(next-canvas):尺寸为 120px×120px,专门用于渲染下一个即将出现的方块,帮助玩家提前规划策略,提升游戏体验。
Canvas 元素的使用遵循'按需渲染'原则,通过 JavaScript 控制绘制逻辑,相比 DOM 元素渲染,具备更高的性能和更灵活的图形操作能力,适合像素级的游戏画面渲染。
📖 2.2.2 状态遮罩层的设计
游戏的'暂停'和'结束'状态通过遮罩层实现,核心设计思路是:
- 遮罩层使用绝对定位覆盖在游戏画布之上,默认隐藏(
display: none); - 当游戏状态切换为暂停/结束时,通过 CSS 类(
.show)控制显示,无需修改 DOM 结构; - 遮罩层内部包含状态提示文本和操作按钮,与游戏核心逻辑解耦,仅通过事件绑定实现交互。
这种设计的优势在于:
- 避免频繁创建/销毁 DOM 元素,减少浏览器回流重绘;
- 遮罩层使用半透明背景 + 毛玻璃效果,视觉上与游戏界面融合,提升体验;
- 状态切换仅需修改 CSS 类,性能开销极低。
📖 2.2.3 响应式布局的结构基础
HTML 结构为响应式设计提供了良好的基础:
- 所有容器使用相对单位(%、max-width)而非固定像素,适配不同屏幕尺寸;
- 侧边栏、游戏区域等模块独立封装,便于 CSS 媒体查询时调整布局结构;
- 按钮、文本等元素使用弹性布局,保证在不同尺寸下的对齐和显示效果。
📘 2.3 语义化与可访问性考量
虽然是小游戏项目,但 HTML 结构仍兼顾了基础的语义化设计:
- 使用
<h1>~<h3>标签层级展示标题和子标题,符合文档结构规范; - 按钮使用
<button>原生元素,而非<div>模拟,保证键盘可聚焦、屏幕阅读器可识别; - 操作说明使用清晰的文本描述,配合按键标识,提升可理解性。
这些细节看似微小,却是前端开发'用户体验至上'理念的体现,也让代码更符合 Web 标准。
📚 三、CSS 样式技术特点:现代特性与视觉体验的融合
📘 3.1 现代 CSS 特性的全面应用
📖 3.1.1 Flexbox 布局的核心应用
Flexbox 是该项目布局的核心技术,几乎所有模块的对齐、分布都依赖 Flex 实现:
/* 页面整体居中 */
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
/* 游戏主容器的模块分布 */
.game-wrapper {
display: flex;
gap: 20px;
justify-content: center;
align-items: flex-start;
}
/* 按钮组的垂直排列 */
.button-group {
display: flex;
flex-direction: column;
gap: 10px;
}
Flexbox 的优势在于:
- 轻松实现元素的水平/垂直居中,无需复杂的定位和计算;
gap属性简化了模块间间距的设置,避免使用 margin 带来的塌陷问题;- 支持灵活的方向切换(
flex-direction),为响应式布局提供了基础。
📖 3.1.2 渐变与毛玻璃效果:提升视觉质感
项目通过 CSS 渐变和 backdrop-filter 实现了现代感的视觉效果:
/* 页面背景渐变 */
body {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
}
/* 毛玻璃效果面板 */
.info-panel, .next-panel, .controls-panel {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
- 线性渐变:使用 135 度角的蓝紫色渐变,营造深邃的游戏背景,符合经典俄罗斯方块的视觉风格;
- 毛玻璃效果:通过
backdrop-filter: blur(10px)配合半透明背景(rgba(255,255,255,0.1)),让面板呈现出磨砂玻璃的质感,同时不遮挡背景渐变,提升视觉层次; - 阴影效果:
box-shadow为面板、按钮、画布添加投影,模拟真实的物理深度,让界面更具立体感。
📖 3.1.3 过渡动画:增强交互反馈
所有可交互元素(按钮、按键提示)都添加了过渡动画,提升操作反馈:
.btn {
transition: all 0.3s;
}
.btn:hover {
background: #5aa0f2;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn:active {
transform: translateY(0);
}
- 按钮 hover 时轻微上移(
translateY(-2px))并加深阴影,模拟'按下前'的物理反馈; - 按钮 active 时恢复原位,模拟'按下'的触感;
- 所有过渡效果使用
all 0.3s,保证动画的平滑性,避免卡顿。
📖 3.1.4 方块高光效果:模拟 3D 质感
游戏方块的高光效果通过 CSS 绘制实现,无需额外图片资源:
// 在 drawBlock 函数中通过 Canvas 绘制高光
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE / 3, BLOCK_SIZE / 3);
在 Canvas 绘制方块时,在左上角绘制一个小的半透明矩形,模拟光线照射的高光效果,让 2D 方块呈现出 3D 质感,提升视觉体验。
📘 3.2 响应式设计的完整实现
📖 3.2.1 媒体查询的核心逻辑
针对移动端和桌面端的差异,项目通过媒体查询(@media)实现布局的自适应调整:
@media(max-width: 900px){
.game-wrapper {
flex-direction: column;
align-items: center;
}
.sidebar {
width: 100%;
max-width: 300px;
flex-direction: row;
flex-wrap: wrap;
}
.sidebar.left { order: 2; }
.sidebar.right { order: 3; }
.game-area { order: 1; }
#game-canvas {
width: 100%;
max-width: 300px;
height: auto;
}
.header h1 {
font-size: 36px;
}
}
核心调整策略:
- 布局方向切换:桌面端横向排列(flex-direction: row)的游戏容器,在移动端切换为纵向排列(column),避免横向空间不足;
- 模块顺序调整:通过
order属性,将游戏核心区域(game-area)移至最上方,优先展示核心内容; - 尺寸自适应:侧边栏宽度改为 100%(最大 300px),画布宽度自适应,保证在小屏幕上的完整显示;
- 字体适配:标题字体从 48px 缩小为 36px,避免移动端文字溢出。
📖 3.2.2 移动端交互适配
除了布局调整,CSS 还针对移动端的触控特性进行了优化:
- 按钮尺寸增大,保证触控的精准性;
- 画布使用
max-width: 300px,避免在小屏幕上超出可视区域; - 遮罩层的按钮和文本尺寸适配,保证移动端的可读性。
📘 3.3 CSS 代码的可维护性设计
📖 3.3.1 类名的语义化命名
CSS 类名采用'功能 + 类型'的命名方式,如:
.info-panel:信息面板,明确功能;.control-item:操作项,描述元素用途;.game-over:游戏结束遮罩,关联游戏状态。
语义化的类名让代码可读性提升,即使不查看 HTML 结构,也能通过类名理解元素的功能和位置。
📖 3.3.2 样式的模块化封装
每个功能模块的样式独立封装,如.info-panel、.next-panel、.controls-panel等,样式之间互不干扰,便于单独修改某个模块的样式而不影响其他部分。
📖 3.3.3 颜色和尺寸的统一管理
虽然未使用 CSS 变量,但项目中颜色和尺寸的使用保持高度统一:
- 主色调为蓝紫色系,所有面板、按钮的颜色都基于该色系延伸;
- 方块尺寸固定为 30px,所有 Canvas 的尺寸都基于该单位计算;
- 间距统一使用 20px、10px 等固定值,保证界面的视觉一致性。
📚 四、JavaScript 核心逻辑解析:游戏开发的核心原理
📘 4.1 游戏数据结构设计:底层逻辑的基石
📖 4.1.1 方块定义系统:二维矩阵的经典应用
俄罗斯方块的核心是 7 种经典形状,项目通过二维数组(矩阵)定义每种形状的结构:
// 游戏配置:方块尺寸、颜色、形状
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;
const COLORS = [
null,
'#FF0D72', // I 型(1)
'#0DC2FF', // O 型(2)
'#0DFF72', // T 型(3)
'#F538FF', // S 型(4)
'#FF8E0D', // Z 型(5)
'#FFE138', // J 型(6)
'#3877FF' // L 型(7)
];
const SHAPES = [
// I 型:4×4 矩阵,仅中间一行有方块
[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
// O 型:2×2 矩阵,全填充
[[2,2],[2,2]],
// T 型:3×3 矩阵,中心 + 上、左、右
[[0,3,0],[3,3,],[,,]],
[[,,],[,,],[,,]],
[[,,],[,,],[,,]],
[[,,],[,,],[,,]],
[[,,],[,,],[,,]]
];
设计思路解析:
- 数字标识:每个形状的非 0 数字对应 COLORS 数组的索引,既标识形状类型,又关联颜色,一举两得;
- 矩阵尺寸:根据形状大小选择合适的矩阵尺寸(2×2、3×3、4×4),避免空间浪费;
- 中心点设计:每种形状的矩阵都以'旋转中心'为核心设计,便于后续旋转算法的实现;
- 扩展性:如需新增形状,只需在 SHAPES 数组中添加新的二维矩阵,修改 COLORS 数组即可,无需调整核心逻辑。
📖 4.1.2 游戏板数据结构:二维数组的状态管理
游戏板(Board)是存储已放置方块状态的核心数据结构,采用 20 行×10 列的二维数组:
// 初始化游戏板:所有单元格初始化为 0(空)
function initBoard() {
board = Array(ROWS).fill(null).map(() => Array(COLS).fill(0));
}
- 值的含义:0 表示空单元格,1-7 表示对应类型的方块(与 SHAPES 和 COLORS 对应);
- 更新逻辑:当方块下落到底部或碰撞到其他方块时,将方块的矩阵值合并到游戏板中;
- 消行逻辑:遍历游戏板的每一行,检测是否全为非 0 值,若是则删除该行并在顶部添加新行。
这种数据结构的优势在于:
- 结构简单,易于遍历和修改;
- 与 Canvas 的渲染逻辑一一对应,便于绘制;
- 内存占用低,20×10 的数组仅需存储 200 个数值,性能开销可忽略。
📖 4.1.3 游戏状态变量:全局状态的统一管理
项目通过一组全局变量管理游戏的核心状态,构成简单的状态机:
let board = []; // 游戏板数据
let currentPiece = null; // 当前下落的方块
let nextPiece = null; // 下一个方块
let score = 0; // 得分
let level = 1; // 等级
let lines = 0; // 消除行数
let dropCounter = 0; // 下落计时器
let dropInterval = 1000; // 下落间隔(毫秒)
let lastTime = 0; // 上一帧时间
let gameRunning = false; // 游戏是否运行
let gamePaused = false; // 是否暂停
let gameOver = false; // 是否结束
状态变量的设计遵循'最小必要'原则,仅存储游戏运行所需的核心状态,避免冗余,同时所有状态变量都有清晰的命名和明确的用途,便于维护。
📘 4.2 核心算法实现:游戏逻辑的核心
📖 4.2.1 碰撞检测算法:游戏的边界控制
碰撞检测是俄罗斯方块的核心算法之一,决定了方块能否移动/旋转,是游戏规则的基础:
function collide(piece, dx = 0, dy = 0) {
const newX = piece.x + dx;
const newY = piece.y + dy;
// 遍历方块的每个单元格
for (let y = 0; y < piece.shape.length; y++) {
for (let x = 0; x < piece.shape[y].length; x++) {
// 仅检测非空单元格
if (piece.shape[y][x]) {
const boardX = newX + x;
const boardY = newY + y;
// 1. 检测边界:超出左右边界或下边界
if (boardX < 0 || boardX >= COLS || boardY >= ROWS) {
return true;
}
// 2. 检测与已放置方块的碰撞(忽略上方超出的部分)
if (boardY >= 0 && board[boardY][boardX]) {
return true;
}
}
}
}
return false;
}
算法解析:
- 参数设计:
piece为当前方块对象,dx和dy为拟移动的偏移量(默认 0); - 遍历逻辑:遍历方块矩阵的每个非空单元格,计算移动后的位置(
boardX/boardY); - 边界检测:检查是否超出游戏板的左右边界(0 ≤ boardX < COLS)或下边界(boardY ≥ ROWS);
- 碰撞检测:检查移动后的位置是否与游戏板中已放置的方块(非 0 值)重叠;
- 特殊处理:忽略方块在游戏板上方(boardY < 0)的碰撞,允许方块从顶部下落。
复杂度分析:方块矩阵的最大尺寸为 4×4,因此算法的时间复杂度为 O(1)(常数时间),即使最坏情况下也仅需遍历 16 个单元格,性能极高。
📖 4.2.2 旋转算法:矩阵变换与墙踢机制
方块旋转是俄罗斯方块的核心操作,项目实现了顺时针旋转 90 度的算法,并加入了'墙踢'(Wall Kick)机制,符合经典游戏规则:
function rotate(piece) {
// 顺时针旋转 90 度:矩阵转置后反转每一行
const rotated = piece.shape.map((_, i) => piece.shape.map(row => row[i]).reverse());
// 保存原始形状,用于旋转失败时恢复
const originalShape = piece.shape;
piece.shape = rotated;
// 墙踢机制:旋转后若碰撞,尝试微调位置
if (collide(piece)) {
// 尝试向左移动 1 格
piece.x -= 1;
if (collide(piece)) {
// 向左失败,尝试向右移动 2 格(恢复 + 右移 1)
piece.x += 2;
if (collide(piece)) {
// 向右也失败,恢复原始形状
piece.x -= 1;
piece.shape = originalShape;
return false;
}
}
}
return true;
}
旋转原理:
- 顺时针旋转 90 度的矩阵变换公式:
rotated[i][j] = original[shape.length - 1 - j][i]; - 项目中通过
map和reverse方法实现该变换,代码简洁且易理解。
墙踢机制解析:
- 旋转后若发生碰撞,并非直接禁止旋转,而是尝试调整方块的水平位置,模拟真实的游戏体验;
- 先向左移动 1 格,若仍碰撞则向右移动 2 格(相当于原始位置右移 1 格);
- 若两次调整都失败,则恢复原始形状,旋转失败。
这种机制避免了'方块卡在墙边无法旋转'的问题,提升了游戏的可玩性。
📖 4.2.3 消行算法:行检测与数组操作
消行是游戏得分的核心逻辑,算法的核心是检测满行并删除,同时在顶部添加新行:
function clearLines() {
let linesCleared = 0;
// 从底部向上遍历(避免删除行后索引错乱)
for (let y = ROWS - 1; y >= 0; y--) {
// 检测当前行是否全为非空
if (board[y].every(cell => cell !== 0)) {
// 删除满行
board.splice(y, 1);
// 在顶部添加空行
board.unshift(Array(COLS).fill(0));
// 消行数 +1
linesCleared++;
// 回退索引,重新检查当前行(因删除后上方行下落)
y++;
}
}
// 计算得分和等级
if (linesCleared > 0) {
lines += linesCleared;
// 得分规则:1 行=100,2 行=300,3 行=500,4 行=800
const points = [0, 100, 300, 500, 800];
score += points[linesCleared] * level;
// 等级提升:每消除 10 行升 1 级,加快下落速度
level = Math.floor(lines / 10) + 1;
dropInterval = Math.max(100, 1000 - (level - 1) * 100);
// 更新 UI 显示
();
}
}
算法解析:
- 遍历方向:从底部向上遍历(y 从 19 到 0),因为删除底部的行不会影响上方行的索引,若从上向下遍历会导致漏检;
- 满行检测:使用
every方法检测当前行的所有单元格是否为非 0 值,简洁高效; - 行操作:
splice(y, 1)删除满行,unshift在顶部添加空行,模拟方块下落的效果; - 索引回退:删除行后,
y++回退索引,因为上方的行会下落至当前位置,需要重新检测; - 得分计算:根据消除行数给予不同分值,连消行数越多,单分行值越高,符合经典规则;
- 等级调整:每消除 10 行提升 1 级,同时减少下落间隔(最快 0.1 秒/次),提升游戏难度。
复杂度分析:算法需要遍历 20 行,每行遍历 10 列,时间复杂度为 O(ROWS×COLS)=O(200),属于常数时间,性能无压力。
📖 4.2.4 方块生成与合并算法
(1)方块生成
// 创建指定类型的方块
function createPiece(type) {
const shape = SHAPES[type];
return {
type: type + 1, // 对应 COLORS 索引
shape: shape,
x: Math.floor(COLS / 2) - Math.floor(shape[0].length / 2), // 水平居中
y: 0 // 垂直顶部
};
}
// 随机生成方块
function randomPiece() {
return createPiece(Math.floor(Math.random() * SHAPES.length));
}
// 生成新方块(当前方块下落到底部后调用)
function spawnPiece() {
currentPiece = nextPiece || randomPiece();
nextPiece = randomPiece();
drawNext(); // 绘制下一个方块
// 检测游戏结束:新方块生成即碰撞
if (collide(currentPiece)) {
gameOver = true;
gameRunning = false;
document.getElementById('final-score'). = score;
.()..();
}
}
设计思路:
createPiece函数负责创建方块对象,包含类型、形状、位置等属性,位置默认水平居中、垂直顶部;randomPiece函数随机生成 7 种方块之一,保证游戏的随机性;spawnPiece函数负责切换当前方块和下一个方块,并检测游戏是否结束(新方块生成即碰撞,说明顶部已满)。
(2)方块合并
当方块无法继续下落时,将其合并到游戏板中:
function merge() {
for (let y = 0; y < currentPiece.shape.length; y++) {
for (let x = 0; x < currentPiece.shape[y].length; x++) {
if (currentPiece.shape[y][x]) {
const boardY = currentPiece.y + y;
const boardX = currentPiece.x + x;
// 仅合并游戏板内的部分(忽略顶部超出的部分)
if (boardY >= 0) {
board[boardY][boardX] = currentPiece.type;
}
}
}
}
}
合并逻辑与碰撞检测逻辑对称,遍历方块的每个非空单元格,将其值写入游戏板对应的位置,完成方块的'放置'。
📘 4.3 游戏循环与渲染系统:流畅的动画体验
📖 4.3.1 游戏主循环:基于 requestAnimationFrame 的帧动画
游戏循环是所有动态游戏的核心,项目使用requestAnimationFrame实现平滑的帧动画:
function gameLoop(time = 0) {
// 计算时间差(毫秒)
const deltaTime = time - lastTime;
lastTime = time;
// 仅在游戏运行、未暂停、未结束时执行下落逻辑
if (gameRunning && !gamePaused && !gameOver) {
dropCounter += deltaTime;
// 达到下落间隔,移动方块
if (dropCounter > dropInterval) {
if (movePiece(0, 1)) {
// 成功下移,重置计时器
} else {
// 无法下移,合并方块并生成新方块
merge();
clearLines();
spawnPiece();
}
dropCounter = 0;
}
}
// 绘制游戏画面
drawBoard();
// 请求下一帧
requestAnimationFrame(gameLoop);
}
// 启动游戏循环
gameLoop();
核心原理:
requestAnimationFrame由浏览器提供,会在每次重绘前调用回调函数,通常帧率为 60fps(约 16.67ms/帧),相比setInterval更平滑,且能根据浏览器性能自动调整;- 时间差控制:使用
deltaTime计算两帧之间的时间差,累加至dropCounter,当达到dropInterval时触发方块下落,避免固定帧率导致的'不同设备下落速度不一致'问题; - 状态判断:仅在游戏运行、未暂停、未结束时执行下落逻辑,保证状态的正确性;
- 持续渲染:无论是否执行下落逻辑,每次循环都调用
drawBoard绘制画面,保证界面的实时更新。
📖 4.3.2 Canvas 渲染系统:分层绘制与视觉优化
(1)方块绘制函数
function drawBlock(ctx, x, y, color) {
// 绘制方块主体
ctx.fillStyle = color;
ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
// 绘制方块边框
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
// 绘制高光效果(增强立体感)
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE / 3, BLOCK_SIZE / 3);
}
绘制逻辑:
- 先绘制方块主体(填充色),再绘制边框(黑色,2px 宽),最后绘制左上角的高光(半透明白色);
- 所有绘制都基于
BLOCK_SIZE(30px),保证尺寸的一致性; - 高光的尺寸为方块的 1/3,位置固定在左上角,模拟光线从左上方照射的效果。
(2)游戏板绘制
function drawBoard() {
// 清空画布(黑色背景)
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制已放置的方块(游戏板数据)
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (board[y][x]) {
drawBlock(ctx, x, y, COLORS[board[y][x]]);
}
}
}
// 绘制当前下落的方块
if (currentPiece) {
for (let y = 0; y < currentPiece.shape.length; y++) {
for (let x = 0; x < currentPiece.shape[y].length; x++) {
if (currentPiece.shape[y][x]) {
const blockX = currentPiece.x + x;
const blockY = currentPiece.y + y;
// 仅绘制游戏板内的部分(避免绘制顶部外的方块)
if (blockY >= 0) {
drawBlock(ctx, blockX, blockY, COLORS[currentPiece.type]);
}
}
}
}
}
}
绘制顺序:
- 先清空画布(黑色背景),再绘制已放置的方块,最后绘制当前下落的方块,保证层级正确;
- 绘制当前方块时,忽略
blockY < 0的部分(即超出游戏板顶部的部分),避免无效绘制。
(3)下一个方块绘制
function drawNext() {
// 清空预览画布
nextCtx.fillStyle = '#000';
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
if (nextPiece) {
const shape = nextPiece.shape;
// 计算居中偏移量
const offsetX = (nextCanvas.width / BLOCK_SIZE - shape[0].length) / 2;
const offsetY = (nextCanvas.height / BLOCK_SIZE - shape.length) / 2;
// 绘制下一个方块(居中显示)
for (let y = 0; y < shape.length; y++) {
for (let x = 0; x < shape[y].length; x++) {
if (shape[y][x]) {
drawBlock(nextCtx, offsetX + x, offsetY + y, COLORS[nextPiece.type]);
}
}
}
}
}
居中逻辑:
- 通过计算画布尺寸与方块矩阵尺寸的差值,得到偏移量(
offsetX/offsetY),让方块在预览画布中居中显示,提升视觉体验。
📘 4.4 输入处理与状态控制:交互逻辑的实现
📖 4.4.1 键盘事件处理
项目通过监听键盘事件实现方块的操作,支持方向键、空格键等:
document.addEventListener('keydown', (e) => {
if (!gameRunning || gameOver) return;
switch (e.key) {
case 'ArrowLeft':
e.preventDefault(); // 阻止页面滚动
movePiece(-1, 0); // 左移
break;
case 'ArrowRight':
e.preventDefault();
movePiece(1, 0); // 右移
break;
case 'ArrowDown':
e.preventDefault();
movePiece(0, 1); // 下移
break;
case 'ArrowUp':
e.preventDefault();
if (currentPiece) {
rotate(currentPiece); // 旋转
}
break;
case ' ':
e.preventDefault();
togglePause(); // 暂停/继续
break;
}
});
设计要点:
- 状态判断:仅在游戏运行且未结束时处理键盘事件,避免无效操作;
- 防页面滚动:使用
e.preventDefault()阻止方向键和空格键的默认行为(如页面滚动、空格翻页); - 单一职责:每个按键仅对应一个操作,逻辑清晰,易于维护。
📖 4.4.2 按钮事件处理
项目为所有控制按钮绑定了点击事件,实现鼠标/触控操作:
// 开始游戏
document.getElementById('start-btn').addEventListener('click', startGame);
// 暂停游戏
document.getElementById('pause-btn').addEventListener('click', togglePause);
// 新游戏
document.getElementById('new-game-btn').addEventListener('click', newGame);
// 游戏结束后重新开始
document.getElementById('restart-btn').addEventListener('click', newGame);
// 暂停后继续
document.getElementById('resume-btn').addEventListener('click', togglePause);
按钮事件与键盘事件最终调用相同的核心函数(startGame、togglePause、newGame),保证操作逻辑的一致性。
📖 4.4.3 游戏状态控制函数
(1)开始游戏
function startGame() {
if (gameRunning && !gamePaused) return;
if (!gameRunning) {
// 初始化游戏状态
initBoard();
score = 0;
level = 1;
lines = 0;
dropCounter = 0;
dropInterval = 1000;
gameOver = false;
gameRunning = true;
gamePaused = false;
// 隐藏遮罩层
document.getElementById('game-over').classList.remove('show');
document.getElementById('pause-overlay').classList.remove('show');
// 生成第一个方块
spawnPiece();
// 更新 UI
updateUI();
} else if (gamePaused) {
// 暂停状态下点击开始,恢复游戏
togglePause();
}
}
(2)暂停/继续
function togglePause() {
if (!gameRunning || gameOver) return;
gamePaused = !gamePaused;
// 显示/隐藏暂停遮罩
document.getElementById('pause-overlay').classList.toggle('show', gamePaused);
}
(3)新游戏
function newGame() {
gameRunning = false;
gamePaused = false;
gameOver = false;
// 隐藏所有遮罩
document.getElementById('game-over').classList.remove('show');
document.getElementById('pause-overlay').classList.remove('show');
// 启动新游戏
startGame();
}
状态控制逻辑:
- 所有状态切换都通过修改全局状态变量实现,保证状态的唯一性;
- 状态切换时同步更新 UI(如显示/隐藏遮罩、更新得分),保证视图与逻辑的一致性;
- 函数职责单一,
startGame负责启动游戏,togglePause负责暂停/继续,newGame负责重置游戏,便于调试和扩展。
📘 4.5 UI 更新函数:数据与视图的同步
function updateUI() {
document.getElementById('score').textContent = score;
document.getElementById('level').textContent = level;
document.getElementById('lines').textContent = lines;
}
该函数负责将游戏的核心数据(得分、等级、消除行数)同步到 HTML 元素中,保证视图的实时更新。函数仅负责数据展示,不涉及任何业务逻辑,符合'视图与逻辑分离'的原则。
📚 五、性能优化分析:从细节到架构的优化思路
📘 5.1 现有实现的性能优势
该项目作为轻量级小游戏,在性能上已经做了很多优化,主要体现在以下方面:
📖 5.1.1 Canvas 渲染的高效性
- 批量绘制:每次游戏循环仅清空画布一次,然后批量绘制所有方块,避免频繁的 Canvas 状态切换;
- 最小化绘制区域:仅绘制游戏板内的内容,忽略超出顶部的方块,减少无效绘制;
- 像素级控制:Canvas 直接操作像素,相比 DOM 元素渲染,减少了浏览器的布局和绘制开销。
📖 5.1.2 算法的低复杂度
所有核心算法(碰撞检测、旋转、消行)的时间复杂度均为常数级(O(1))或线性级(O(n)),即使在低端设备上也能流畅运行:
- 碰撞检测:最多遍历 16 个单元格;
- 旋转算法:最多执行 3 次碰撞检测;
- 消行算法:固定遍历 20 行×10 列=200 个单元格。
📖 5.1.3 事件处理的优化
- 事件委托:键盘事件仅绑定在
document上,而非单个元素,减少事件监听器数量; - 状态过滤:事件处理函数首先判断游戏状态,避免无效的逻辑执行;
- 默认行为阻止:仅在需要时阻止默认行为,减少不必要的性能开销。
📖 5.1.4 内存管理的合理性
- 游戏板使用二维数组存储,内存占用极低(200 个数值,约 1.6KB);
- 方块对象复用,避免频繁创建/销毁对象;
- 全局变量仅存储必要的游戏状态,无内存泄漏风险。
📘 5.2 潜在的性能优化方向
虽然现有实现性能良好,但仍有进一步优化的空间,适合作为进阶优化的学习方向:
📖 5.2.1 脏矩形渲染:减少绘制区域
现有实现每次循环都清空并重新绘制整个画布,可优化为仅绘制变化的区域(脏矩形):
// 伪代码:脏矩形渲染
let dirtyRegions = []; // 存储需要重绘的区域
// 当方块移动/旋转时,记录脏区域
function markDirty(x, y, width, height) {
dirtyRegions.push({ x, y, width, height });
}
// 渲染时仅重绘脏区域
function drawBoard() {
if (dirtyRegions.length === 0) return;
// 遍历脏区域,仅清空并绘制这些区域
dirtyRegions.forEach(region => {
ctx.clearRect(region.x, region.y, region.width, region.height);
// 绘制该区域内的方块
// ...
});
// 清空脏区域
dirtyRegions = [];
}
脏矩形渲染可减少 Canvas 的绘制面积,尤其在方块移动较小时,能显著提升性能。
📖 5.2.2 对象池模式:减少对象创建
现有实现每次生成新方块时都会创建新对象,可通过对象池复用方块对象:
// 伪代码:方块对象池
const piecePool = [];
// 创建对象池
function initPool() {
for (let i = 0; i < 5; i++) {
piecePool.push({ type: 0, shape: [], x: 0, y: 0 });
}
}
// 从池获取对象
function getPieceFromPool(type) {
let piece = piecePool.pop() || {};
const shape = SHAPES[type];
piece.type = type + 1;
piece.shape = shape;
piece.x = Math.floor(COLS / 2) - Math.floor(shape[0].length / 2);
piece.y = 0;
return piece;
}
// 归还对象到池
function returnPieceToPool(piece) {
piecePool.push(piece);
}
对象池可减少垃圾回收(GC)的频率,避免 GC 导致的游戏卡顿,尤其在长时间游戏时效果明显。
📖 5.2.3 Web Workers:分离计算与渲染
将耗时的算法(如消行、碰撞检测)移至 Web Worker 中执行,避免阻塞主线程的渲染:
// 主线程代码
const worker = new Worker('game-worker.js');
// 发送游戏状态到 Worker
worker.postMessage({ type: 'collide', piece: currentPiece, board: board });
// 接收 Worker 的计算结果
worker.onmessage = (e) => {
if (e.data.type === 'collideResult') {
isCollided = e.data.result;
}
};
// game-worker.js 代码
self.onmessage = (e) => {
if (e.data.type === 'collide') {
const result = collide(e.data.piece, 0, 0);
self.postMessage({ type: 'collideResult', result });
}
};
Web Workers 可利用多核 CPU,将计算逻辑与渲染逻辑分离,保证游戏的流畅性,尤其在复杂游戏中效果显著。
📖 5.2.4 离屏 Canvas:预渲染静态内容
将固定的静态内容(如方块的高光、边框)预渲染到离屏 Canvas 中,避免每次绘制时重复计算:
// 伪代码:离屏 Canvas 预渲染
const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d');
// 预渲染方块的基础样式
function preRenderBlocks() {
for (let i = 1; i < COLORS.length; i++) {
offscreenCtx.fillStyle = COLORS[i];
offscreenCtx.fillRect(0, 0, BLOCK_SIZE, BLOCK_SIZE);
// 绘制边框和高光
// ...
// 保存到缓存
blockCache[i] = offscreenCtx.getImageData(0, 0, BLOCK_SIZE, BLOCK_SIZE);
}
}
// 绘制时直接使用缓存
function drawBlock(ctx, x, y, type) {
ctx.putImageData(blockCache[type], x * BLOCK_SIZE, y * BLOCK_SIZE);
}
离屏 Canvas 可减少重复的绘制操作,提升渲染效率。
📖 5.2.5 节流/防抖:优化输入处理
虽然现有输入处理已足够高效,但可通过节流优化频繁的按键操作(如长按方向键):
// 伪代码:节流函数
function throttle(fn, delay) {
let lastCall = 0;
return (...args) => {
const now = Date.now();
if (now - lastCall >= delay) {
fn(...args);
lastCall = now;
}
};
}
// 节流处理方向键事件
const throttledMove = throttle(movePiece, 50);
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
throttledMove(-1, 0);
}
});
节流可限制频繁的移动操作,减少不必要的碰撞检测和绘制,提升性能。
📚 六、设计模式应用:代码组织的最佳实践
📘 6.1 模块化设计:逻辑的分层与解耦
虽然项目未使用 ES6 模块(import/export),但通过函数和变量的组织,实现了模块化的设计思想:
- 数据层:包含游戏配置(COLS、ROWS、BLOCK_SIZE)、方块定义(SHAPES、COLORS)、游戏状态(board、score、level 等),负责存储游戏的核心数据;
- 控制层:包含游戏循环(gameLoop)、输入处理(keydown 事件、按钮事件)、核心算法(collide、rotate、clearLines 等),负责游戏的逻辑控制;
- 视图层:包含 Canvas 渲染(drawBoard、drawNext、drawBlock)、UI 更新(updateUI),负责游戏的视觉呈现。
模块化设计的优势在于:
- 各层职责单一,便于调试和修改;
- 层与层之间通过明确的接口交互(如控制层修改数据层,视图层读取数据层),降低耦合度;
- 便于扩展新功能,如新增音效模块,只需在控制层添加音效触发逻辑,不影响其他层。
📘 6.2 观察者模式:事件驱动的交互
项目大量使用观察者模式(Observer Pattern)实现交互逻辑:
- 键盘事件:
document.addEventListener('keydown', handler),当用户按下键盘时,触发对应的操作; - 按钮事件:
button.addEventListener('click', handler),当用户点击按钮时,触发游戏状态切换; - 游戏状态变化:当游戏状态(score、level、gameOver)变化时,触发 UI 更新(updateUI)。
观察者模式的优势在于:
- 解耦事件源和事件处理逻辑,事件源无需知道谁会处理事件;
- 支持多个观察者监听同一个事件,如多个按钮可触发同一个
newGame函数; - 便于扩展新的事件处理逻辑,如新增'音效开关'按钮,只需添加新的事件监听,不影响现有逻辑。
📘 6.3 状态模式:游戏状态的统一管理
项目通过状态变量(gameRunning、gamePaused、gameOver)实现了简单的状态模式(State Pattern):
- 状态定义:游戏有三种核心状态:未运行、运行中(未暂停)、运行中(暂停)、结束;
- 状态转换:状态之间的转换通过明确的函数(startGame、togglePause、newGame)实现,避免状态混乱;
- 状态行为:不同状态下,游戏的行为不同(如暂停状态下方块不下落,结束状态下不处理键盘事件)。
状态模式的优势在于:
- 避免大量的 if/else 判断,代码结构更清晰;
- 状态转换逻辑集中管理,便于维护和扩展;
- 状态与行为分离,新增状态时只需添加对应的行为逻辑。
📘 6.4 工厂模式:方块对象的创建
createPiece和randomPiece函数实现了工厂模式(Factory Pattern):
- 工厂函数:
createPiece负责创建指定类型的方块对象,封装了对象的创建逻辑; - 抽象工厂:
randomPiece负责创建随机类型的方块对象,提供了更高级的创建接口。
工厂模式的优势在于:
- 封装对象的创建细节,调用者无需知道对象的具体结构;
- 便于统一管理对象的创建逻辑,如修改方块的初始位置,只需修改
createPiece函数; - 支持创建不同类型的对象,符合'开闭原则'(对扩展开放,对修改关闭)。
📚 七、浏览器兼容性与跨端适配
📘 7.1 兼容性分析
项目使用的核心技术的浏览器兼容性如下:
| 技术特性 | 兼容浏览器版本 | 备注 |
|---|---|---|
| HTML5 Canvas | IE9+、Chrome 4+、Firefox 3.6+、Safari 4+、Edge 12+ | 核心渲染技术,兼容性良好 |
| ES6 语法(const/let、箭头函数、map/reduce) | Chrome 45+、Firefox 42+、Safari 10+、Edge 14+、IE 不支持 | IE 需转译(Babel) |
| CSS3 Flexbox | Chrome 29+、Firefox 28+、Safari 9+、Edge 12+、IE11(部分支持) | IE11 需前缀和兼容写法 |
| CSS3 backdrop-filter | Chrome 76+、Firefox 70+、Safari 9+、Edge 79+、IE 不支持 | 毛玻璃效果,不支持的浏览器显示半透明背景 |
| requestAnimationFrame | Chrome 10+、Firefox 4+、Safari 6+、Edge 12+、IE10+ | 游戏循环核心,IE10+支持 |
📘 7.2 兼容性优化方案
针对低版本浏览器,可采取以下优化方案:
📖 7.2.1 ES6 语法转译
使用 Babel 将 ES6 语法转译为 ES5,兼容 IE11 等低版本浏览器:
# 安装 Babel
npm install @babel/core @babel/cli @babel/preset-env --save-dev
# 配置.babelrc
{
"presets": [
["@babel/preset-env", {
"targets": {
"ie": "11",
"chrome": "45"
}
}]
]
}
# 转译代码
npx babel script.js --out-file script-es5.js
📖 7.2.2 CSS 特性降级
- backdrop-filter 降级:为不支持的浏览器提供纯色背景替代;
.info-panel {
background: rgba(255, 255, 255, 0.1);
/* 降级方案 */
background: #2a5298\9; /* IE9- */
backdrop-filter: blur(10px);
/* 针对不支持 backdrop-filter 的浏览器 */
@supports not (backdrop-filter: blur(10px)) {
background: rgba(255, 255, 255, 0.2);
}
}
- Flexbox 降级:为 IE11 提供兼容写法,如使用
-ms-flex前缀;
.game-wrapper {
display: -ms-flexbox;
display: flex;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-align: start;
align-items: flex-start;
}
📖 7.2.3 Canvas 兼容性
IE9+ 支持 Canvas,但部分 API(如getImageData)需注意跨域问题,项目中无跨域图片,无需额外处理。
📘 7.3 跨端适配细节
📖 7.3.1 移动端触控适配
现有实现仅支持键盘和鼠标操作,可新增触控事件支持,提升移动端体验:
// 伪代码:触控操作
let touchStartX = 0;
let touchStartY = 0;
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
});
canvas.addEventListener('touchend', (e) => {
e.preventDefault();
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
const dx = touchEndX - touchStartX;
const dy = touchEndY - touchStartY;
// 左滑/右滑:移动方块
if (Math.abs(dx) > Math.abs(dy)) {
if (dx < -20) {
movePiece(-1, 0); // 左滑
} else if (dx > 20) {
movePiece(1, 0); // 右滑
}
} else {
// 上滑:旋转
(dy < -) {
(currentPiece);
} (dy > ) {
();
}
}
});
📖 7.3.2 屏幕方向适配
添加屏幕方向检测,适配横屏/竖屏:
/* 竖屏适配 */
@media (orientation: portrait) {
.game-area {
width: 100%;
max-width: 300px;
}
}
/* 横屏适配 */
@media (orientation: landscape) {
.game-wrapper {
flex-direction: row;
}
}
📚 八、扩展性与维护性:从 demo 到产品的升级思路
📘 8.1 功能扩展方向
现有项目是一个基础版的俄罗斯方块,可扩展以下功能,使其更接近产品级应用:
8.1.1 音效系统
添加背景音乐、方块移动/旋转/消行/游戏结束音效,提升沉浸感:
// 伪代码:音效管理
class AudioManager {
constructor() {
this.sounds = {
move: new Audio('sounds/move.mp3'),
rotate: new Audio('sounds/rotate.mp3'),
clear: new Audio('sounds/clear.mp3'),
gameOver: new Audio('sounds/game-over.mp3'),
bgm: new Audio('sounds/bgm.mp3')
};
// 循环播放背景音乐
this.sounds.bgm.loop = true;
}
playSound(name) {
this.sounds[name].currentTime = 0;
this.sounds[name].play().catch(e => console.log('Audio play failed:', e));
}
playBgm() {
this.sounds.bgm.play().( .(, e));
}
() {
...();
}
}
audioManager = ();
() {
(!(currentPiece, dx, dy)) {
currentPiece. += dx;
currentPiece. += dy;
audioManager.();
;
}
;
}
📖 8.1.2 最高分记录
使用 localStorage 存储最高分,跨会话保留游戏数据:
// 保存最高分
function saveHighScore() {
const highScore = localStorage.getItem('tetrisHighScore') || 0;
if (score > highScore) {
localStorage.setItem('tetrisHighScore', score);
document.getElementById('high-score').textContent = score;
}
}
// 加载最高分
function loadHighScore() {
const highScore = localStorage.getItem('tetrisHighScore') || 0;
document.getElementById('high-score').textContent = highScore;
}
// 游戏结束时保存最高分
function spawnPiece() {
// ... 现有逻辑
if (collide(currentPiece)) {
// ... 游戏结束逻辑
saveHighScore();
}
}
📖 8.1.3 难度选择
添加难度选择功能,不同难度对应不同的初始下落速度和得分倍率:
// 难度配置
const DIFFICULTY = {
easy: { dropInterval: 1000, scoreMultiplier: 1 },
medium: { dropInterval: 800, scoreMultiplier: 1.5 },
hard: { dropInterval: 500, scoreMultiplier: 2 }
};
// 选择难度
function selectDifficulty(diff) {
const config = DIFFICULTY[diff];
dropInterval = config.dropInterval;
scoreMultiplier = config.scoreMultiplier;
}
// 得分计算时应用倍率
score += points[linesCleared] * level * scoreMultiplier;
📖 8.1.4 皮肤系统
支持切换方块皮肤/主题,提升个性化体验:
// 皮肤配置
const SKINS = {
classic: [null, '#FF0D72', '#0DC2FF', '#0DFF72', '#F538FF', '#FF8E0D', '#FFE138', '#3877FF'],
neon: [null, '#FF00FF', '#00FFFF', '#FFFF00', '#00FF00', '#FF0000', '#0000FF', '#FF8800'],
pastel: [null, '#F8B195', '#F67280', '#C06C84', '#6C5B7B', '#355C7D', '#88C0D0', '#8FBCBB']
};
// 切换皮肤
function changeSkin(skinName) {
COLORS = SKINS[skinName];
// 重新绘制画面
drawBoard();
drawNext();
}
📘 8.2 代码维护性提升
📖 8.2.1 代码重构:面向对象(OOP)改造
现有代码使用函数式编程,可重构为面向对象风格,提升代码的组织性和可维护性:
// 重构后的游戏类
class TetrisGame {
constructor() {
// 配置常量
this.COLS = 10;
this.ROWS = 20;
this.BLOCK_SIZE = 30;
this.COLORS = [/* 颜色配置 */];
this.SHAPES = [/* 形状配置 */];
// 游戏状态
this.board = [];
this.currentPiece = null;
this.nextPiece = null;
this.score = 0;
this.level = 1;
this.lines = 0;
this.dropCounter = 0;
this.dropInterval = 1000;
this.gameRunning = false;
this.gamePaused = false;
this.gameOver = false;
. = .();
. = ..();
. = .();
. = ..();
.();
.();
.();
}
() {
. = (.).().( (.).());
}
() {
}
() {
}
}
game = ();
面向对象改造的优势在于:
- 将游戏的配置、状态、方法封装在一个类中,避免全局变量污染;
- 方法可通过
this访问状态,无需传递大量参数; - 便于继承和扩展,如新增
TetrisGameWithSound子类,添加音效功能。
📖 8.2.2 注释与文档
添加详细的注释和文档,提升代码的可读性:
/**
* 碰撞检测函数
* @param {Object} piece - 方块对象,包含 type、shape、x、y 属性
* @param {number} dx - 水平偏移量,默认 0
* @param {number} dy - 垂直偏移量,默认 0
* @returns {boolean} - 是否发生碰撞
* @description 检测方块移动 dx/dy 后是否超出边界或与已放置方块碰撞
*/
function collide(piece, dx = 0, dy = 0) {
// ... 实现逻辑
}
📖 8.2.3 错误处理
添加错误处理逻辑,提升代码的健壮性:
// Canvas 初始化错误处理
try {
this.ctx = this.canvas.getContext('2d');
if (!this.ctx) {
throw new Error('Canvas 2D context not supported');
}
} catch (e) {
console.error('Canvas initialization failed:', e);
document.getElementById('game-area').innerHTML = '<p>您的浏览器不支持 Canvas,请升级浏览器!</p>';
}
// 音效播放错误处理
playSound(name) {
try {
this.sounds[name].currentTime = 0;
this.sounds[name].play();
} catch (e) {
console.warn(`Failed to play sound ${name}:`, e);
}
}
📖 8.2.4 单元测试
添加单元测试,保证核心算法的正确性:
// 使用 Jest 进行单元测试
describe('collide function', () => {
test('检测方块超出右边界', () => {
const game = new TetrisGame();
const piece = { type: 1, shape: [[1, 1, 1, 1]], x: 8, y: 0 };
expect(game.collide(piece, 1, 0)).toBe(true);
});
test('检测方块与已放置方块碰撞', () => {
const game = new TetrisGame();
game.board[19][5] = 1;
const piece = { type: 2, shape: [[2, 2], [2, 2]], x: 4, y: 18 };
expect(game.collide(piece, 0, 1)).toBe(true);
});
});
📚 九、总结与思考
📘 9.1 项目的技术价值
这个俄罗斯方块游戏虽然是一个小型前端项目,但涵盖了前端开发的核心技术和游戏开发的基础原理,其技术价值体现在:
- 原生 Web 技术的综合应用:HTML 语义化布局、CSS3 现代特性、JavaScript 核心语法和 DOM/Canvas API 的深度使用,是前端基础能力的全面实践;
- 游戏开发核心原理的落地:游戏循环、碰撞检测、状态管理、渲染系统等游戏开发的核心概念,通过简单的代码实现,易于理解和学习;
- 性能与体验的平衡:在保证功能完整的前提下,通过算法优化、渲染优化等手段,实现了流畅的游戏体验,体现了前端性能优化的核心思路;
- 工程化思维的体现:模块化设计、设计模式应用、兼容性考虑等,展示了从'写代码'到'做工程'的思维转变。
📘 9.2 学习与进阶方向
对于前端开发者而言,这个项目是一个极佳的学习案例,可从以下方向进阶:
- 技术深度:深入研究 Canvas 渲染原理、游戏物理引擎、性能优化的底层逻辑;
- 工程化:学习使用 Webpack/Vite 构建项目,使用 ES6 模块、TypeScript 提升代码质量;
- 跨端开发:基于该项目,尝试使用 React/Vue 重构,或使用 Electron 打包为桌面应用,使用 Cordova 打包为移动端应用;
- 游戏开发进阶:学习 Phaser、PixiJS 等游戏引擎,开发更复杂的 2D 游戏。
📘 9.3 最终思考
前端开发的核心是'解决问题'和'提升体验',这个俄罗斯方块项目虽然简单,但完美体现了这两个核心:通过简洁的代码解决了游戏逻辑的核心问题,通过现代 CSS 和交互设计提升了用户体验。对于前端开发者而言,无论项目大小,保持对技术的钻研和对体验的关注,才是持续进步的关键。
这个项目的完整实现,不仅是一个可运行的游戏,更是一份前端技术的实践指南,从基础的 HTML/CSS/JS 到进阶的算法、性能优化、设计模式,都能从中找到学习和思考的切入点,是前端学习道路上的一个优秀的'练手项目'。
📚 十、代码
📘 项目目录

📘 项目代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>俄罗斯方块</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>俄罗斯方块</h1>
</div>
<div class="game-wrapper">
<div class="sidebar left">
<div class="info-panel">
<div class="score-box">
得分
0
等级
1
消除行数
0
下一个
游戏结束
最终得分:0
重新开始
游戏暂停
继续游戏
操作说明
← →
左右移动
↓
快速下落
↑
旋转
空格
暂停/继续
开始游戏
暂停
新游戏
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
color: #fff;
}
.container {
max-width: 1200px;
width: 100%;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header h1 {
font-size: 48px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
color: #fff;
}
.game-wrapper {
display: flex;
: ;
: center;
: flex-start;
}
{
: ;
: flex;
: column;
: ;
}
, , {
: (, , , );
: ;
: ;
: ();
: (, , , );
}
, {
: ;
: (, , , );
: ;
: center;
}
{
: ;
}
{
: ;
}
{
: ;
: (, , , );
: ;
}
{
: ;
: bold;
: ;
}
{
: center;
}
{
: (, , , );
: ;
: ;
}
{
: ;
: ;
: center;
}
{
: flex;
: space-between;
: center;
: ;
: ;
}
{
: ;
}
{
: (, , , );
: ;
: ;
: bold;
: ;
: center;
}
{
: relative;
: (, , , );
: ;
: ;
: (, , , );
}
{
: block;
: ;
: ;
: solid (, , , );
}
, {
: absolute;
: ;
: ;
: ;
: ;
: (, , , );
: none;
: center;
: center;
: ;
}
, {
: flex;
}
, {
: center;
: (, , , );
: ;
: ;
: ();
}
, {
: ;
: ;
}
{
: ;
: ;
}
{
: ;
: bold;
: ;
}
{
: flex;
: column;
: ;
}
{
: ;
: ;
: none;
: ;
: ;
: ;
: bold;
: pointer;
: all ;
: ;
}
{
: ;
: (-);
: (, , , );
}
{
: ();
}
{
: (, , , );
: not-allowed;
: ;
}
(max-width: ) {
{
: column;
: center;
}
{
: ;
: ;
: row;
: wrap;
}
{ : ; }
{ : ; }
{ : ; }
{
: ;
: ;
: auto;
}
{
: ;
}
}
// 游戏配置
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;
const COLORS = [
null,
'#FF0D72', // I
'#0DC2FF', // O
'#0DFF72', // T
'#F538FF', // S
'#FF8E0D', // Z
'#FFE138', // J
'#3877FF' // L
];
// 方块形状定义
const SHAPES = [
// I
[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
// O
[[2,2],[2,2]],
// T
[[0,3,0],[3,3,3],[,,]],
[[,,],[,,],[,,]],
[[,,],[,,],[,,]],
[[,,],[,,],[,,]],
[[,,],[,,],[,,]]
];
board = [];
currentPiece = ;
nextPiece = ;
score = ;
level = ;
lines = ;
dropCounter = ;
dropInterval = ;
lastTime = ;
gameRunning = ;
gamePaused = ;
gameOver = ;
canvas = .();
ctx = canvas.();
nextCanvas = .();
nextCtx = nextCanvas.();
() {
board = ().().( ().());
}
() {
shape = [type];
{
: type + ,
: shape,
: .( / ) - .(shape[]. / ),
:
};
}
() {
(.(.() * .));
}
() {
ctx. = color;
ctx.(x * , y * , , );
ctx. = ;
ctx. = ;
ctx.(x * , y * , , );
ctx. = ;
ctx.(x * , y * , / , / );
}
() {
ctx. = ;
ctx.(, , canvas., canvas.);
( y = ; y < ; y++) {
( x = ; x < ; x++) {
(board[y][x]) {
(ctx, x, y, [board[y][x]]);
}
}
}
(currentPiece) {
( y = ; y < currentPiece..; y++) {
( x = ; x < currentPiece.[y].; x++) {
(currentPiece.[y][x]) {
blockX = currentPiece. + x;
blockY = currentPiece. + y;
(blockY >= ) {
(ctx, blockX, blockY, [currentPiece.]);
}
}
}
}
}
}
() {
nextCtx. = ;
nextCtx.(, , nextCanvas., nextCanvas.);
(nextPiece) {
shape = nextPiece.;
offsetX = (nextCanvas. / - shape[].) / ;
offsetY = (nextCanvas. / - shape.) / ;
( y = ; y < shape.; y++) {
( x = ; x < shape[y].; x++) {
(shape[y][x]) {
(nextCtx, offsetX + x, offsetY + y, [nextPiece.]);
}
}
}
}
}
() {
newX = piece. + dx;
newY = piece. + dy;
( y = ; y < piece..; y++) {
( x = ; x < piece.[y].; x++) {
(piece.[y][x]) {
boardX = newX + x;
boardY = newY + y;
(boardX < || boardX >= || boardY >= ) {
;
}
(boardY >= && board[boardY][boardX]) {
;
}
}
}
}
;
}
() {
rotated = piece..( piece..( row[i]).());
originalShape = piece.;
piece. = rotated;
((piece)) {
piece. -= ;
((piece)) {
piece. += ;
((piece)) {
piece. -= ;
piece. = originalShape;
;
}
}
}
;
}
() {
( y = ; y < currentPiece..; y++) {
( x = ; x < currentPiece.[y].; x++) {
(currentPiece.[y][x]) {
boardY = currentPiece. + y;
boardX = currentPiece. + x;
(boardY >= ) {
board[boardY][boardX] = currentPiece.;
}
}
}
}
}
() {
linesCleared = ;
( y = - ; y >= ; y--) {
(board[y].( cell !== )) {
board.(y, );
board.(().());
linesCleared++;
y++;
}
}
(linesCleared > ) {
lines += linesCleared;
points = [, , , , ];
score += points[linesCleared] * level;
level = .(lines / ) + ;
dropInterval = .(, - (level - ) * );
();
}
}
() {
.(). = score;
.(). = level;
.(). = lines;
}
() {
currentPiece = nextPiece || ();
nextPiece = ();
();
((currentPiece)) {
gameOver = ;
gameRunning = ;
.(). = score;
.()..();
}
}
() {
(!currentPiece || gamePaused || gameOver) ;
(!(currentPiece, dx, dy)) {
currentPiece. += dx;
currentPiece. += dy;
;
}
;
}
() {
(!currentPiece || gamePaused || gameOver) ;
((, )) {
}
();
();
();
}
() {
deltaTime = time - lastTime;
lastTime = time;
(gameRunning && !gamePaused && !gameOver) {
dropCounter += deltaTime;
(dropCounter > dropInterval) {
((, )) {
} {
();
();
();
}
dropCounter = ;
}
}
();
(gameLoop);
}
.(, {
(!gameRunning || gameOver) ;
(e.) {
:
e.();
(-, );
;
:
e.();
(, );
;
:
e.();
(, );
;
:
e.();
(currentPiece) {
(currentPiece);
}
;
:
e.();
();
;
}
});
() {
(gameRunning && !gamePaused) ;
(!gameRunning) {
();
score = ;
level = ;
lines = ;
dropCounter = ;
dropInterval = ;
gameOver = ;
gameRunning = ;
gamePaused = ;
.()..();
.()..();
();
();
} (gamePaused) {
();
}
}
() {
(!gameRunning || gameOver) ;
gamePaused = !gamePaused;
.()..(, gamePaused);
}
() {
gameRunning = ;
gamePaused = ;
gameOver = ;
.()..();
.()..();
();
}
.().(, startGame);
.().(, togglePause);
.().(, newGame);
.().(, newGame);
.().(, togglePause);
();
();
();
();


