俄罗斯方块:完整可玩的经典玩法——7 种方块、旋转撞墙自动修正、影子落点、下一个预览、消行计分、每 10 行升一级加速、最高分保存。全部用内置的画布和积木实现,不需要拓展。
这一篇回答的搜索: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 项业务检查(出块位置、绘制格数、移动到墙、旋转与踢墙、计时下落、落底计分、单行与四行消除、升级加速、暂停、手势映射、游戏结束与最高分、再来一局)。手感(速度、手势灵敏度)以真机体验为准。详见验证记录。
扫码添加客服咨询