AI脑洞板
完整教案
自学课件
第 31 课 · 共 31/51 课

俄罗斯方块 形状、下落时钟与消行

课时
待定
器材
ESP32-S3 触摸屏板 + USB 线
前置知识
无
配套代码
见项目 demo 目录

你好,我是吴老师。这份是给你自己读的——不用等我讲,看不懂就整段复制给 AI,加一句"请一次只解释一个概念,讲完问我一个小问题"。

读完之后,你应该能:

  • 说出"一块方块用一个位掩码数字来表示"是什么意思;
  • 明白"移动、旋转、下落之前为什么要先问collides(碰撞检测)";
  • 说清方块凭什么"自己"每隔一小会儿往下掉一格——millis() 和 delay() 有什么不同;
  • 讲明白消掉一行时,程序到底是"擦掉"还是"搬格子",为什么从底下往上扫。

用时:约 60 分钟。 准备:烧录 demo01_tetris 的板子 + 一根 USB 线。

一、方块从天上掉下来

第 30 课我们让一条贪吃蛇在网格里跑——蛇是一格一点。这一课升级成俄罗斯方块:方块是好几格拼成的一个形状,它自己往下掉,还听你手指的话:左滑右滑移动、点一下旋转、下滑直接落底,落满一行还能消掉。

板子没有"方块"这个实体,它怎么记住形状、怎么判断该掉该停、怎么知道哪行满了?答案藏在三样东西里:位掩码(形状写成数字)、下落时钟(用 millis() 自己掉)、碰撞检测(动之前先问一声)。这一课就把它们玩明白。

二、我观察到的现象

上电先见 TETRIS 标题和 tap to start,轻触开始。

✏️ 可以直接在表格里填写
现象勾选
方块从顶部一格一格自己往下掉
左右滑动,方块左右移一格
点一下,方块转 90°
向下滑动,方块"啪"直坠落底
方块落底或撞到已有方块,就固定住
有一整行填满,那行消失,上面整体下移
堆到顶,屏幕打红字 GAME OVER,轻触重开

三、一块方块,是一个"数字"——位掩码

贪吃蛇一格记一个数就行;俄罗斯方块的 I 字是横的 4 格,T 字是上面 3 格 + 下面中间 1 格……这些形状一个"点"装不下。

诀窍:每一种方块都装得进一个 4×4 的小格子框。我们把 4×4 看成 16 个小位子,每个位子"有方块没方块"用一位(bit)的 1 或 0 表示。比如 I 字只有第 1 行 4 格:

row0: 1 1 1 1
row1: 0 0 0 0
row2: 0 0 0 0
row3: 0 0 0 0
把它压成一个 16 位的数字 → 用十六进制写就是 0x00F0

一个形状 = 一个 16 位的数字。 七种方块就是七个数字:

I: 0x00F0   O: 0x0660   T: 0x0270   S: 0x06C0
Z: 0x0C60   J: 0x0710   L: 0x0740

为什么用数字不是记 16 个坐标? 因为一个 16 位数字只要 2 字节内存,又小又好比对;旋转、判断"这格有没有方块"都变成简单的位运算,又快又不容易错。把形状折叠成一个数字,是这课第一个聪明想法。

一块方块 = 一个位掩码数字:4×4 框 16 个位子,I 字 = 0x00F0,七种方块七个数字

四、两个代码名,够了

1. TETRO[type]——这个形状就是这个数字

TETRO 是存 7 个数字的数组,下标 0..6 对应 I/O/T/S/Z/J/L。
TETRO[0] = 0x00F0  → I 字
TETRO[5] = 0x0710  → J 字

想旋转?rotatedShape(type, rot) 会从 TETRO[type] 出发,
把整个 4×4 位图顺时针转 rot 次,得到新角度的形状数字。

判断"这格有没有方块"用 hasCell(形状, dx, dy):看那个数字里第 dy 行第 dx 列 那个 bit 是不是 1。

2. collides(x, y, type, rot)——这样放,行不行?

collides = 碰撞检测。你问它:把 type 形状以 rot 角度放在 (x,y),合不合法?
它逐格算:会不会越出左右下边界?会不会撞到已经固定的 grid 里的方块?
只要有一格越界或撞到,就返回 true(不行);全部没事,返回 false(可以)。

这是本课最重要的规矩:移动、旋转、下落之前,先问一次 collides。 想往左挪——先问左下位置行不行;想转——先问转后的形状行不行;想往下掉一格——先问下面一格行不行。先"算一算会不会撞"再"动",是几乎所有游戏的头一条规矩。

最该记住的:形状 = 一个位掩码数字;任何移动/旋转/下落前都先问一句 collides。 整个俄罗斯方块,就是"一个形状数字 + 问一句行不行 + 落定就固定 + 满行就搬格子消掉"。

先「探路」再「走路」:移动 / 旋转 / 下落之前都先问一句 collides(越界?撞到已固定的格子?)

五、为什么"墙"和"堆好的方块"都能被躲开?

你问:方块又不是活的,怎么知道左边有墙、下面有别的东西?答案在 collides——在动之前先想一遍。

打个比方:你闭眼往左迈一步,会不会撞墙?聪明人不是先迈再撞,而是先用手探一探。 collides 就是那只"探路的手":它不真的移动方块,只是设想方块到了新位置,逐格看有没有越界或占到别人。只要有一格不行,就整个不行。

⚠️ 比喻要有边界:"探路的手"是讲动之前先模拟,不是说真有个手在屏上摸。电脑的"先探路"就是纯计算——它在内存里临时算一遍新位置有没有冲突,并没真的把方块搬过去。

一句话记住:方块永远"先问路,再走路"——问路就是 collides,走路就是移动/旋转/下落。

六、方块凭什么"自己"往下掉?——下落时钟

贪吃蛇是"你按一下,它走一格";俄罗斯方块是你不碰它,它也每隔一小会儿自己掉一格。 这个"每隔一小会儿"不能用 delay(700)——那会让板子卡死 700 毫秒,手指点了没反应,画面也僵住。用 millis()!

lastDrop 记着"上一次掉的时间"。
loop() 每转一圈都问:现在 millis() 比 lastDrop 多了多少?
    够了 700ms? → 方块掉一格,lastDrop 更新成现在。
    还不够? → 什么都不做,去跑触摸、画屏幕。

这叫非阻塞计时:不卡死,而是每圈看看"到时间了没",到了就掉一格,没到就干别的。 这样方块自己掉、手指能点、画面能重画,三件事互不耽误。

而且不是固定 700ms——dropInterval() 会把"每 100 分减 50 毫秒"算进去:分越高间隔越短(700ms 一路压到最低 120ms),方块掉得越快、越难。难度从哪来?就从这"掉落间隔"一个数字来。

方块凭什么自己掉:millis() 非阻塞计时 vs delay() 卡死;消行 = 搬格子从底往上、消完回退重查

七、撞到底了——固定、消行,再来下一块

方块掉到下一格"探路"发现不行了,就固定进堆叠。这是 dropToGround():

lockPiece()  把当前形状按 (curX,curY) 一格一格填进 grid
             (grid 是 20×10 大表格,0 空,1..7 是七种颜色)
clearLines() 从最底下那行往上扫:这一行全满吗?
                满 → 这一行"搬掉",上面的行整体往下搬一行
                     搬完再回退重看这一行(可能又满了)
                没满 → 看上一行
score += 消掉行数 × 100
spawnPiece() 生成下一块,从顶上下落

这里有个妙处:消掉一行不是"擦掉"这一行,而是把它上面的所有行,整体往下搬一行。 一笔都不挪,只搬"哪行顶替哪行",消完再看这一行是不是又满了。

为什么从底往上、消完回退重查? 因为下面的行搬下来后,原来"上面那行"可能正好也是满的。从下往上、消了再看一次,才能把连着的好几行一次消干净。

八、改一个条件:让方块掉慢/掉快

方块掉太快,新手跟不上。能不能慢一点?先预测再看代码想想(不用真改):

✏️ 可以直接在表格里填写
我改了什么我预测实际
dropInterval() 里 700 改成 1000掉得更慢——"到时间才掉一格"的门槛更高了
score / 100 改成 score / 50每 50 分就减 50 毫秒,难度升更快
每行加分 100 改成 200分涨得快 → 难度也涨更快

俄罗斯方块的"难度"几乎只由 dropInterval() 这一个函数的数字决定。 想简单就调大基础间隔,想激烈就调小——这就是"把规则写在数据上":改一个数字,整个游戏节奏都变了。

九、过关检查

  1. 一块"好几格拼成"的方块,怎么用一个数字(位掩码)表示?举例说 I 字。
  2. collides 在干什么?为什么"移动、旋转、下落"之前都先调它?
  3. 方块凭什么"自己"每隔一小会儿掉一格?millis() 和 delay() 有什么不同?
  4. 消掉一行,程序是"擦掉"还是"搬格子"?为什么从底下往上、消完还要回退重查?
  5. 想让游戏更难,dropInterval() 大概改哪个数字、往哪改?
  6. 从贪吃蛇"一格一格"到俄罗斯方块"一整块 + 旋转 + 消行",多了哪两样新东西?

留下三样东西:你一局能消掉几行、你第一次点"旋转"时方块转了几度、"俄罗斯方块在板子里是什么"的一句话。

十、还能玩什么

作品方向描述给 AI 的效果再想想
消除预告屏幕上方先预告"下一个形状"NEXT 框已在做,想预告两个怎么存?
等级彩字分数破档就换屏幕字/颜色开个 level 变量,还是从 score 推?
暂停功能玩到一半点一下暂停,再点继续暂停要冻结方块、时钟、分数,加个状态吗?
音效版消行"叮"、落底"咚"借第 13 课 I2S 声卡,每个动作一种音
双人对战屏幕上下各一个场地,谁先堆满谁输屏幕分两半,触摸坐标和网格各管各的

下节课预告:学会"形状=位掩码、下落=时钟、动=先碰撞、消行=搬格子"后,第 32 课推箱子你会发现——"移动 + 碰撞 + 一张地图"这套思路几乎一模一样,只是把"下落"换成"玩家推着箱子走"。

第 31 课 · 俄罗斯方块

第 31 课 · 俄罗斯方块

七种方块从顶部落下,左右滑动移动、点击旋转、下滑加速,消行得分,堆到顶就结束。

31游戏位运算消行