从零到一:用纯前端实现俄罗斯方块游戏并发布到华为云开发者作品展览馆

举报
yd_213866132 发表于 2026/08/17 16:39:16 2026/08/17
【摘要】 从零到一:用纯前端实现俄罗斯方块游戏并发布到华为云开发者作品展览馆 前言俄罗斯方块(Tetris)是游戏史上的经典之作,它的规则简单但实现起来涉及不少有趣的技术点。本文记录了在华为云 AI DevSpace 开发环境中,从需求分析到代码实现、再到发布至开发者作品展览馆的完整过程。整个项目采用纯前端单文件架构(HTML + CSS + JavaScript),无需任何构建工具,浏览器直接打开...

从零到一:用纯前端实现俄罗斯方块游戏并发布到华为云开发者作品展览馆

前言

俄罗斯方块(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 技能发布至华为云高校运营平台。完整流程如下:

  1. 预检:验证画廊平台连通性、git 和 zip 可用性
  2. IAM Domain 解析:通过 hcloud CLI 自动获取华为云账号的 Domain ID 和 Domain Name
  3. GitCode 仓库创建:创建远程仓库,初始化本地 Git,提交并推送代码
  4. 渲染门禁:安装 CJK + emoji 字体,校验 fontconfig 中文映射,安装 Chromium
  5. 编码门禁:验证 HTML 文件编码为 UTF-8
  6. 截图与封面:用 Playwright 截取运行中的应用画面,合成 PPT 风格封面
  7. 字形终检:像素级比对封面中的中文字形,确保没有渲染为方块(豆腐块)
  8. 详情文章与架构图:生成包含系统架构图的技术介绍文章,打包为 zip
  9. 门禁复核:统一校验所有门禁标记和产物文件
  10. 发布 API 调用:提交作品至画廊平台
  11. 领取成长积分:调用 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 即可找到),作品已在华为云开发者作品展览馆上线,欢迎查看交流。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。