从零到一:用纯前端实现俄罗斯方块游戏并发布到华为云开发者作品展览馆
从零到一:用纯前端实现俄罗斯方块游戏并发布到华为云开发者作品展览馆
前言
俄罗斯方块(Tetris)是游戏史上的经典之作,它的规则简单但实现起来涉及不少有趣的技术点。本文记录了在华为云 AI DevSpace 开发环境中,从需求分析到代码实现、再到发布至开发者作品展览馆的完整过程。整个项目采用纯前端单文件架构(HTML + CSS + JavaScript),无需任何构建工具,浏览器直接打开即可运行。
一、开发环境与工具链
本次开发在华为云 AI DevSpace 容器中进行,核心工具链如下:
- 码道(CodeArts):华为云提供的 AI 辅助编码服务,通过 ACP(Agent Communication Protocol)WebSocket 协议与沙箱交互,由 AI 生成代码
- acpx:ACP 客户端,用于创建会话、发送提示词、接收生成结果
- GitCode:华为云代码托管平台,用于仓库创建和代码推送
- 开发者作品展览馆:华为云高校运营平台,用于发布作品并领取成长积分
开发流程概括为:通过 acpx 向码道发送自然语言描述,码道在沙箱中生成代码,随后启动 HTTP 服务预览,最后推送至 GitCode 并发布到展览馆。
二、需求分析与设计
2.1 功能需求
| 类别 | 需求 |
|---|---|
| 方块系统 | 7 种经典方块(I/O/T/S/Z/J/L),各自独立配色 |
| 操作控制 | 方向键移动、旋转、加速下落、空格直落到底、暂停 |
| 游戏机制 | 分数计算、消除行数统计、等级系统、下落速度递增 |
| 辅助功能 | 下一个方块预览、幽灵落点提示、游戏结束检测与重新开始 |
| 界面设计 | 深色主题、居中布局、中文界面、响应式适配 |
2.2 技术选型
- 渲染方案:Canvas API —— 相比 DOM 操作,Canvas 在逐帧重绘场景下性能更优
- 游戏循环:requestAnimationFrame —— 浏览器原生 API,自动匹配刷新率,比 setInterval 更流畅
- 方块表示:矩阵(二维数组)—— 天然支持旋转操作
- 架构:单文件内联 —— 零依赖、零构建,部署门槛最低
2.3 架构设计
项目采用三层架构:
输入层(键盘事件监听)
↓
游戏逻辑层(方块状态、碰撞检测、行消除、计分)
↓
渲染层(Canvas 绘制:网格、方块、幽灵投影、预览)
三、关键代码解析
3.1 方块定义与旋转
7 种方块用矩阵表示,每种方块对应一种颜色:
const SHAPES = {
I: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
O: [[1,1],[1,1]],
T: [[0,1,0],[1,1,1],[0,0,0]],
S: [[0,1,1],[1,1,0],[0,0,0]],
Z: [[1,1,0],[0,1,1],[0,0,0]],
J: [[1,0,0],[1,1,1],[0,0,0]],
L: [[0,0,1],[1,1,1],[0,0,0]]
};
const COLORS = {
I: '#36d6e4', O: '#f5d33b', T: '#b066f2',
S: '#4ed868', Z: '#f25762', J: '#4a86ff', L: '#ff9d3b'
};
旋转采用顺时针矩阵旋转算法。对于一个 N×N 的矩阵,旋转后 r[x][n-1-y] = m[y][x]:
function rotateMatrix(m) {
const n = m.length;
const r = Array.from({ length: n }, () => Array(n).fill(0));
for (let y = 0; y < n; y++)
for (let x = 0; x < n; x++)
r[x][n - 1 - y] = m[y][x];
return r;
}
3.2 碰撞检测
碰撞检测是方块游戏的核心。每次移动或旋转前,都需要检查方块是否与边界或已有方块重叠:
function collide(piece, dx, dy, shape) {
shape = shape || piece.shape;
for (let y = 0; y < shape.length; y++) {
for (let x = 0; x < shape[y].length; x++) {
if (!shape[y][x]) continue;
const nx = piece.x + x + dx;
const ny = piece.y + y + dy;
if (nx < 0 || nx >= COLS || ny >= ROWS) return true;
if (ny >= 0 && board[ny][nx]) return true;
}
}
return false;
}
注意 ny >= 0 的判断:方块在顶部生成时 y 坐标可能为负数(尚未进入棋盘),此时不应判定为碰撞。
3.3 墙踢机制
旋转方块时,如果旋转后的位置与墙壁或已有方块冲突,不能直接拒绝旋转。墙踢(Wall Kick)机制会尝试在水平方向偏移若干格,找到可行的位置:
function rotate() {
if (!cur || paused || gameOver) return;
const rotated = rotateMatrix(cur.shape);
// 简单墙踢:尝试原位、左右各偏移 1~2
const kicks = [0, -1, 1, -2, 2];
for (const k of kicks) {
if (!collide(cur, k, 0, rotated)) {
cur.x += k;
cur.shape = rotated;
return;
}
}
}
这里采用简单的偏移序列 [0, -1, 1, -2, 2],依次尝试原位旋转、左移 1 格、右移 1 格等。虽然不如 SRS(Super Rotation System)完善,但对经典俄罗斯方块已经足够。
3.4 行消除与计分
行消除采用从底部向上扫描的方式。发现满行后,用 splice 移除并 unshift 在顶部补入空行:
function clearLines() {
let cleared = 0;
for (let y = ROWS - 1; y >= 0; y--) {
if (board[y].every(c => c)) {
board.splice(y, 1);
board.unshift(Array(COLS).fill(null));
cleared++;
y++; // 重新检查同一行
}
}
if (cleared > 0) {
const pts = [0, 100, 300, 500, 800][cleared];
score += pts * level;
lines += cleared;
const newLevel = Math.floor(lines / 10) + 1;
if (newLevel !== level) {
level = newLevel;
dropInterval = Math.max(80, 800 - (level - 1) * 70);
}
updateHud();
}
}
计分规则采用经典 Tetris 标准:消除 1 行 100 分、2 行 300 分、3 行 500 分、4 行 800 分,且乘以当前等级倍数。每消除 10 行升一级,下落间隔从 800ms 递减,最低 80ms。
3.5 幽灵投影
幽灵投影(Ghost Piece)在当前方块的正下方显示一个半透明轮廓,提示玩家方块将落在何处。实现方式是计算方块在不发生碰撞的前提下能下落的最大距离:
function drawGhost() {
let gy = 0;
while (!collide(cur, 0, gy + 1)) gy++;
ctx.save();
ctx.globalAlpha = 0.22;
for (let y = 0; y < cur.shape.length; y++)
for (let x = 0; x < cur.shape[y].length; x++)
if (cur.shape[y][x])
drawCell(ctx, cur.x + x, cur.y + y + gy, cur.color, CELL);
ctx.restore();
}
通过 ctx.globalAlpha = 0.22 设置半透明度,ctx.save() 和 ctx.restore() 保证透明度设置不影响后续绘制。
3.6 游戏主循环
使用 requestAnimationFrame 驱动游戏循环,通过时间差(delta time)累加来判断是否该下落:
function loop(time) {
if (!running) return;
if (!lastTime) lastTime = time;
const dt = time - lastTime;
lastTime = time;
if (!paused && !gameOver) {
dropTimer += dt;
if (dropTimer >= dropInterval) {
dropTimer = 0;
if (!collide(cur, 0, 1)) cur.y++;
else lockPiece();
}
drawBoard();
}
rafId = requestAnimationFrame(loop);
}
这种方式的好处是:下落速度由 dropInterval 控制(与帧率无关),而渲染则每帧执行,保证画面流畅。
3.7 单元格绘制与立体感
每个方块单元格不是简单的纯色方块,而是通过高光和暗边模拟立体感:
function drawCell(c, x, y, color, size) {
const px = x * size, py = y * size;
c.fillStyle = color;
c.fillRect(px + 1, py + 1, size - 2, size - 2);
// 高光(顶部和左侧)
c.fillStyle = 'rgba(255,255,255,0.18)';
c.fillRect(px + 1, py + 1, size - 2, 3);
c.fillRect(px + 1, py + 1, 3, size - 2);
// 暗边(底部和右侧)
c.fillStyle = 'rgba(0,0,0,0.28)';
c.fillRect(px + 1, py + size - 4, size - 2, 3);
c.fillRect(px + size - 4, py + 1, 3, size - 2);
}
顶部和左侧加白色半透明高光,底部和右侧加黑色半透明暗边,形成凸起效果。
四、界面设计要点
4.1 深色主题与渐变背景
使用 CSS 变量定义主题色系,背景采用径向渐变:
:root {
--bg-1: #0f1020;
--bg-2: #141b35;
--accent: #5ad1ff;
--accent-2: #7c5cff;
}
body {
background: radial-gradient(1200px 800px at 50% -10%,
#1b2447 0%, var(--bg-1) 55%, #07080f 100%);
}
4.2 玻璃态面板
侧边栏面板使用半透明背景加模糊滤镜,实现玻璃态(Glassmorphism)效果:
.panel {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 10px;
padding: 12px 14px;
}
4.3 渐变文字标题
标题使用 CSS 渐变裁剪实现彩色文字:
h1 {
background: linear-gradient(90deg, var(--accent), var(--accent-2));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
五、发布至开发者作品展览馆
5.1 发布流程
作品开发完成后,通过 publish-work-to-gallery 技能发布至华为云高校运营平台。完整流程如下:
- 预检:验证画廊平台连通性、git 和 zip 可用性
- IAM Domain 解析:通过 hcloud CLI 自动获取华为云账号的 Domain ID 和 Domain Name
- GitCode 仓库创建:创建远程仓库,初始化本地 Git,提交并推送代码
- 渲染门禁:安装 CJK + emoji 字体,校验 fontconfig 中文映射,安装 Chromium
- 编码门禁:验证 HTML 文件编码为 UTF-8
- 截图与封面:用 Playwright 截取运行中的应用画面,合成 PPT 风格封面
- 字形终检:像素级比对封面中的中文字形,确保没有渲染为方块(豆腐块)
- 详情文章与架构图:生成包含系统架构图的技术介绍文章,打包为 zip
- 门禁复核:统一校验所有门禁标记和产物文件
- 发布 API 调用:提交作品至画廊平台
- 领取成长积分:调用 rewards 接口领取发布奖励
5.2 渲染门禁的坑
在 Linux(EulerOS arm64)环境下,渲染门禁是最容易出问题的环节:
- CJK 字体缺失:默认不预装中文字体,截图中中文会显示为方块。需安装 Noto Sans CJK SC
- emoji 字体缺失:需要 Noto Color Emoji 彩色版,yum 仓库的版本过旧,需从 CDN 下载
- fontconfig 中文映射:只装字体不够,fontconfig 可能没有把 sans-serif 映射到 CJK 字体,需手动写配置文件
- Chromium 依赖:EulerOS 上 playwright install-deps 不生效(仅支持 apt-get),需手动 yum 安装系统依赖
- 时序问题:必须先装字体、刷新缓存,再启动浏览器——运行中的进程不识别新字体
5.3 发布结果
作品成功发布至 AI 素养课 ClassRoom 第二期训练营(主题:智慧系统),获得 200 成长积分。作品陈列馆链接可在华为云开发者作品展览馆中查看。
六、开发心得
6.1 单文件架构的优势
对于俄罗斯方块这种规模的项目,单文件架构是最佳选择:
- 零部署门槛:一个 HTML 文件,双击即可运行
- 零依赖管理:不需要 npm、webpack、vite 等任何工具
- 零网络请求:所有代码内联,离线也能用
- 易于分享:发一个文件就等于发了整个项目
6.2 Canvas vs DOM
游戏中选择 Canvas 而非 DOM 元素来渲染方块,原因在于:
- 方块游戏每帧需要重绘整个棋盘(200 格 + 当前方块 + 幽灵投影),DOM 操作的开销远大于 Canvas
- Canvas 的 fillRect 性能极高,适合大量矩形绘制
- 高光、暗边等效果在 Canvas 中只是多几次 fillRect,在 DOM 中则需要多层嵌套或 box-shadow
6.3 矩阵旋转的优雅
用矩阵表示方块是一个经典且优雅的选择。旋转操作只需要一个通用的矩阵旋转函数,不需要为每种方块单独写旋转逻辑。代价是 I 方块需要 4x4 矩阵(而非更紧凑的 1x4),但代码简洁性的收益远大于这点空间浪费。
6.4 AI 辅助开发的体验
本次开发通过华为云码道(CodeArts)AI 服务生成代码。体验感受:
- 自然语言驱动:用中文描述需求,AI 自动生成完整实现,包括 HTML 结构、CSS 样式、JavaScript 逻辑
- 一次生成可用:生成的代码质量较高,552 行单文件直接可运行,无需大幅修改
- 沙箱隔离:代码在 bwrap 沙箱中生成,与宿主机隔离,安全性有保障
- 全流程贯通:从代码生成到 GitCode 推送、作品发布,整个链路在同一个环境中完成
七、总结
本项目展示了在华为云 AI DevSpace 环境中,从 AI 辅助代码生成到作品发布的完整流程。技术层面,俄罗斯方块虽是经典项目,但其中涉及的矩阵旋转、碰撞检测、墙踢机制、幽灵投影、Canvas 渲染优化等技术点,对前端游戏开发具有普遍参考价值。工程层面,单文件架构、渲染门禁、字形终检等实践,体现了从开发到发布的全链路质量把控。
项目源码已托管在 GitCode 平台(搜索 Eddygit/tetris-game 即可找到),作品已在华为云开发者作品展览馆上线,欢迎查看交流。
- 点赞
- 收藏
- 关注作者
评论(0)