App Inventor 俄罗斯方块:画布实现的完整小游戏(附 .aia 源码)

俄罗斯方块:完整可玩的经典玩法——7 种方块、旋转撞墙自动修正、影子落点、下一个预览、消行计分、每 10 行升一级加速、最高分保存。全部用内置的画布和积木实现,不需要拓展。

下载 Tetris.aia 返回搜索需求专题

这一篇回答的搜索:App Inventor 俄罗斯方块 aia、appinventor 小游戏教程、App Inventor 2 作品源代码下载、二维列表怎么用。

界面

俄罗斯方块界面布局预览

图为由最终 .aia 生成的布局预览(棋盘上的方块为示例),不是真机截图。

玩法

  • 点「开始」。◀ ▶ 移动,⟳ 旋转,▼ 加速下落一格(+1 分),⤓ 直接落到底(每格 +2 分)。
  • 也可以直接在棋盘上操作:左右滑移动,下滑直接落底,点一下旋转。
  • 消 1/2/3/4 行分别得 100/300/500/800 分 × 当前等级;每消 10 行升一级,下落间隔从 700 毫秒每级缩短 60 毫秒(最快 120 毫秒)。
  • 新方块放不下时游戏结束,破纪录会保存为最高分。

实现思路

棋盘:二维列表

20 行 × 10 列,每格一个数字:0 是空,1~7 是不同颜色的方块。棋盘[行][列] = 值 直接修改某一格。

global 棋盘 = []

function 空行() {
  return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}

procedure 清空棋盘() {
  棋盘 = []
  for 行号 = 1 to 20 {
    listadd(棋盘, 空行())
  }
}

空行() 每次调用都返回一个新列表。如果先建一个全局空行再往棋盘里加 20 次,20 行会是同一个列表,改一格整列都变。

方块与旋转

每种方块用「盒子尺寸 + 4 个格子在盒子里的 [行, 列] 偏移」描述,比如 T 形是尺寸 3、格子 [0,1] [1,0] [1,1] [1,2]。顺时针旋转只需要一个公式:(行, 列) → (列, 尺寸 − 1 − 行),不用为每种方块的 4 个方向各写一份数据。

function 旋转后(格子, 尺寸) {
  return [[格子[1][2], 尺寸 - 1 - 格子[1][1]], [格子[2][2], 尺寸 - 1 - 格子[2][1]], [格子[3][2], 尺寸 - 1 - 格子[3][1]], [格子[4][2], 尺寸 - 1 - 格子[4][1]]]
}

碰撞判断

一个格子能放下:没出左右边界、没出底,并且棋盘上那格是空的。方块刚出现时有一部分还在棋盘上面(行号小于 1),这部分不算碰撞。and、or 都是「短路」的——前面的条件不成立就不会去读 棋盘[行][列],所以不会越界报错。

global 棋盘 = []

function 单格可放(格, 行, 列) {
  return 列 + 格[2] >= 1 and 列 + 格[2] <= 10 and 行 + 格[1] <= 20 and (行 + 格[1] < 1 or 棋盘[行 + 格[1]][列 + 格[2]] == 0)
}

function 可放(格子, 行, 列) {
  return 单格可放(格子[1], 行, 列) and 单格可放(格子[2], 行, 列) and 单格可放(格子[3], 行, 列) and 单格可放(格子[4], 行, 列)
}

移动、旋转、下落都先问一句 可放(新位置),能放才改;旋转时放不下,就依次试着往左右挪 1~2 格(「踢墙」),贴着墙也能转。

消行

从最底行往上查:一行里没有 0 就是满了,删掉它,再在顶部插入一行空的,同一个行号再查一次(上面的行掉下来了)。

global 棋盘 = []
global 分数 = 0

function 空行() {
  return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}

procedure 消行() {
  var 行号 = 20
  var 本次 = 0
  while 行号 >= 1 {
    if contains(棋盘[行号], 0) {
      行号 = 行号 - 1
    } else {
      listremove(棋盘, 行号)
      listinsert(棋盘, 1, 空行())
      本次 = 本次 + 1
    }
  }
  if 本次 > 0 {
    分数 = 分数 + select([100, 300, 500, 800], 本次)
  }
}

绘制:只画有方块的格子

棋盘的网格线是一张背景图(board-grid.png),Clear 清除画布时背景图保留。每一帧只需要画有方块的格子,用 DrawShape 画一个四边形,四周各留 1 像素缝,看起来就是一块块的。

procedure 画格(x, y, 边长, 颜色) {
  画布_棋盘.PaintColor = 颜色
  画布_棋盘.DrawShape([[x + 1, y + 1], [x + 边长 - 1, y + 1], [x + 边长 - 1, y + 边长 - 1], [x + 1, y + 边长 - 1]], true)
}

滑动手势

画布的 Flung 事件给出滑动方向 heading:0 是向右、±180 是向左、−90 是向下、90 是向上。

when 画布_棋盘.Flung(x, y, speed, heading, xvel, yvel, flungSprite) {
  if abs(heading) <= 45 {
    标签_状态.Text = "右滑"
  } else if abs(heading) >= 135 {
    标签_状态.Text = "左滑"
  } else if heading < 0 {
    标签_状态.Text = "下滑"
  } else {
    标签_状态.Text = "上滑"
  }
}

常见问题

现象 原因与处理
改一格,整列都变了 棋盘的 20 行用了同一个列表对象。每行都要用 空行() 新建(见上文)。
方块转到墙里去了 旋转前没有判断。先用旋转公式算出新格子,可放 才替换;放不下再试左右挪一挪。
消多行时漏消 删掉一行后行号不能往上走,要原地再检查一次(上面的行掉下来了)。
游戏一卡一卡 每帧重画了整张网格。网格做成画布背景图,只画有方块的格子;计时器间隔别小于 100 毫秒。
切到后台还在下落 计时器的 TimerAlwaysFires(一直计时)设为假,App 在后台时自动暂停。
想加音效 消行时用音效组件播放一个短音,或用 Vibrate 振动一下。

用到的组件

相关教程:画布游戏合集 · 台球游戏 · 按住物体移动

验证范围

最终 .aia 已通过:正式编辑器载入与保存重开 0 错误、后台导入、Android APK 编译,并执行了真实积木的 17 项业务检查(出块位置、绘制格数、移动到墙、旋转与踢墙、计时下落、落底计分、单行与四行消除、升级加速、暂停、手势映射、游戏结束与最高分、再来一局)。手感(速度、手势灵敏度)以真机体验为准。详见验证记录。

文档反馈