你好,我是吴老师。这份是给你自己读的——不用等我讲,看不懂就整段复制给 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 字节内存,又小又好比对;旋转、判断"这格有没有方块"都变成简单的位运算,又快又不容易错。把形状折叠成一个数字,是这课第一个聪明想法。
四、两个代码名,够了
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,走路就是移动/旋转/下落。
六、方块凭什么"自己"往下掉?——下落时钟
贪吃蛇是"你按一下,它走一格";俄罗斯方块是你不碰它,它也每隔一小会儿自己掉一格。 这个"每隔一小会儿"不能用 delay(700)——那会让板子卡死 700 毫秒,手指点了没反应,画面也僵住。用 millis()!
lastDrop 记着"上一次掉的时间"。
loop() 每转一圈都问:现在 millis() 比 lastDrop 多了多少?
够了 700ms? → 方块掉一格,lastDrop 更新成现在。
还不够? → 什么都不做,去跑触摸、画屏幕。
这叫非阻塞计时:不卡死,而是每圈看看"到时间了没",到了就掉一格,没到就干别的。 这样方块自己掉、手指能点、画面能重画,三件事互不耽误。
而且不是固定 700ms——dropInterval() 会把"每 100 分减 50 毫秒"算进去:分越高间隔越短(700ms 一路压到最低 120ms),方块掉得越快、越难。难度从哪来?就从这"掉落间隔"一个数字来。
七、撞到底了——固定、消行,再来下一块
方块掉到下一格"探路"发现不行了,就固定进堆叠。这是 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() 这一个函数的数字决定。 想简单就调大基础间隔,想激烈就调小——这就是"把规则写在数据上":改一个数字,整个游戏节奏都变了。
九、过关检查
- 一块"好几格拼成"的方块,怎么用一个数字(位掩码)表示?举例说 I 字。
collides在干什么?为什么"移动、旋转、下落"之前都先调它?- 方块凭什么"自己"每隔一小会儿掉一格?
millis()和delay()有什么不同? - 消掉一行,程序是"擦掉"还是"搬格子"?为什么从底下往上、消完还要回退重查?
- 想让游戏更难,
dropInterval()大概改哪个数字、往哪改? - 从贪吃蛇"一格一格"到俄罗斯方块"一整块 + 旋转 + 消行",多了哪两样新东西?
留下三样东西:你一局能消掉几行、你第一次点"旋转"时方块转了几度、"俄罗斯方块在板子里是什么"的一句话。
十、还能玩什么
| 作品方向 | 描述给 AI 的效果 | 再想想 |
|---|---|---|
| 消除预告 | 屏幕上方先预告"下一个形状" | NEXT 框已在做,想预告两个怎么存? |
| 等级彩字 | 分数破档就换屏幕字/颜色 | 开个 level 变量,还是从 score 推? |
| 暂停功能 | 玩到一半点一下暂停,再点继续 | 暂停要冻结方块、时钟、分数,加个状态吗? |
| 音效版 | 消行"叮"、落底"咚" | 借第 13 课 I2S 声卡,每个动作一种音 |
| 双人对战 | 屏幕上下各一个场地,谁先堆满谁输 | 屏幕分两半,触摸坐标和网格各管各的 |
下节课预告:学会"形状=位掩码、下落=时钟、动=先碰撞、消行=搬格子"后,第 32 课推箱子你会发现——"移动 + 碰撞 + 一张地图"这套思路几乎一模一样,只是把"下落"换成"玩家推着箱子走"。
