你好,我是吴老师。这份是给你自己读的——不用等我讲,看不懂就整段复制给 AI,加一句"请一次只解释一个概念,讲完问我一个小问题"。
读完之后,你应该能:
- 说清"蛇身 = 一串格子(数组)"是什么意思;
- 明白"方向"在板子里就是一个数(上/右/下/左);
- 理解蛇每 tick 整体挪一格、吃到食物才变长;
- 分清"撞墙只是反弹",真正结束的是撞自己/撞对方/两蛇头对撞;
- 说出板子有"标题 / 倒计时 / 进行中 / 结束"四个状态(状态机)。
用时:约 60 分钟。 准备:烧录
demo01_snake的板子 + 一根 USB 线。
一、让板子玩贪吃蛇
第 29 课我们让板子当棋盘陪你下五子棋。这一课更带劲——让一条蛇在格子里咬来咬去。
规则你玩过:蛇一直往前爬,吃到食物变长;撞墙、撞自己就结束。可板子没有"墙/蛇/食物"这些实体,它只有像素点。真正的问题是:
板子怎么知道蛇现在在哪一格?朝哪个方向爬?这一步该往哪走?吃到东西怎么变长?
答案很省事:蛇身就是数组里的一串格子,方向就是一个数。 这一课把上一课"棋盘=数组"升级成"蛇身=一串格子"。
先看现象:烧录后屏幕上出现两条蛇——一条绿的你控制(滑动转向),一条红的 NPC(板子自己控制),它俩抢一个卡通食物。谁先撞自己/撞对方就输,两蛇头撞同一格算平局。
二、我观察到的现象
| 现象 | 勾选 |
|---|---|
| 屏幕上有条会自己移动的蛇 | |
| 我"滑动"屏幕,蛇就转向 | |
| 蛇头吃到食物,身体变长、分数 +1 | |
| 有条红色的 NPC 蛇,自己会去追食物 | |
| 蛇撞到墙会"反弹"回来(没死) | |
| 蛇撞到自己/撞到对方,游戏结束 | |
| 两蛇头撞在同一格,显示"平局" |
三、蛇,就是"一串格子"
屏幕上共 20 列 × 24 行 = 480 个格子,每格 12 像素。每条蛇就住在这张网格上,是一串连着的格子。代码把它记成:
struct Snake {
Cell body[480]; // 一串格子:body[0]=头,body[1]=第二节…最后是尾
uint16_t len; // 蛇现在有多长
uint8_t dir; // 当前方向(一个数)
uint8_t queuedDir;// 你刚滑动、下一 tick 才生效的方向
bool alive; // 还活着吗
uint16_t score; // 吃了多少分
};
你滑一下屏幕,蛇不会"立刻转弯"——程序只是把目标方向存进
queuedDir,下一 tick 才真正朝那边走,这样画面才不鬼畜。
四、方向,就一个数
蛇朝哪爬,不是箭头,就是一个数:
enum Dir { UP=0, RIGHT=1, DOWN=2, LEFT=3 };
DX[] = { 0, 1, 0, -1 }; // 列怎么变
DY[] = { -1, 0, 1, 0 }; // 行怎么变
- UP=0:往上行减1(-1);RIGHT=1:往右列加1;DOWN=2:往下行加1;LEFT=3:往左列减1。
方向变成"一个数"的好处:想走一步,就拿蛇头的行列 + 一组 DX/DY 就行。 反方向也超好判断——(dir+2)%4(上的反是下,右的反是左)。
五、两个代码名,够了
1. stepSnake(snake, 新头x, 新头y, 吃了没)——整条蛇往前挪一格
stepSnake 做的事:
1) 身体从头到尾,每一节都往后腾一格;
2) 把新蛇头塞到 body[0];
3) 吃到食物(grow=true) → 长度+1、分数+1、重新生成一颗食物;
4) 踩到道具(吸铁石/闪电) → 只有玩家会生效。
蛇是"往前挪一格"而不是"整个重画":头前插一格、尾巴去掉一格。只有吃到食物时尾巴才不"去掉",于是变长。想通这一点,"变长"其实只是"尾巴这次没跟上"。
2. doMove()——这一 tick,两条蛇各走一步,并处理所有碰撞
doMove 每 150ms 触发一次,按顺序做三件事:
1) 解转向:玩家转向真正生效;如果蛇头在墙边,先算出"撞墙反弹"的新方向。
2) 算两个新头:玩家和 NPC 各自按方向走一格,得到两个"新头"位置。
3) 解碰撞:新蛇头去比"墙/自己的身体/对方身体/对方头":
撞自己或撞对方 → alive=false(死了)
两蛇头撞同一格,或互相踩对方现在的位置 → 两条都死,算平局
谁都没死 → 才各自 stepSnake 真走这一步。
这个 demo 里撞到墙只是反弹(回到场内继续走),不会死!真正结束的是撞到自己、撞到对方,或两蛇头对撞。和你以前玩过的"撞墙就死"不一样,一定记住。
最该记住的:蛇 = 一串格子(数组)+ 一个方向数;每 tick 整体挪一格,吃到食物尾巴才不掉所以变长;撞自己/撞对方/两蛇头对撞才结束,撞墙只反弹。
六、为什么蛇身要用数组,而不是一条会动的线?
| 做法 | 怎么知道"撞到自己身体" | 前进一步就算了 |
|---|---|---|
| 记一串像素点 | 判断"新点是否落在旧点上"很麻烦 | 整体平移图形,费劲 |
| 用数组记一串格子 | 拿新蛇头去 body[i] 比是不是同一格 | 头前插 + 尾去掉,一步完成 |
数据怎么存,决定规则好不好写。 把世界"对齐成格子",所有"撞没撞"都变成"两个格子是不是同一个",简单一眼看懂。
七、改一个条件:撞墙就死?
现在的蛇撞墙会反弹。如果改成"经典贪吃蛇"——撞墙直接死,要改什么?
| 我改了什么 | 我预测 | 实际 |
|---|---|---|
| 撞墙反弹 → 撞墙即死 | 把撞墙那一步交给碰撞判定判死、去掉 bounceDir,其余不用动 |
方向是数、格子是行列,所以改一条规则常常只是改一小段判断。"反弹"和"撞死"只是对"新头出界没"的两种处理。
八、状态机:板子怎么知道"该干嘛"?
板子不是一直在动,它在几种状态间切换:
| 状态 | 板子在干什么 |
|---|---|
| 标题 | 显示 SNAKE、PLAYER/NPC、swipe to steer / tap to start,等你点屏 |
| 倒计时 | 点屏后先 3-2-1 数秒 |
| 进行中 | 两条蛇动起来,你滑动转向、吃食物、抢道具 |
| 结束 | 有蛇死了,显示 YOU WIN / NPC WIN / DRAW,等你点屏重开 |
代码是个 switch(state),每个状态只干一两件事,在特定时机跳到下一个状态。
状态机就是"按当前状态决定该干嘛"。以后做任何有开始、有过程、有结束的程序(切屏、菜单、多关卡)都会用到。
九、过关检查
- 板子怎么记住一条蛇?"蛇身=一串格子"是什么意思?
- "方向"在板子里是一个什么?
DX/DY那组数字和方向什么关系? stepSnake具体做了哪几件事?为什么吃到食物才变长?- 这个 demo 里撞到墙会怎样?真正结束的"死法"有哪几种?
- 状态机有哪四个状态?靠什么从"倒计时"切到"进行中"?
- 为什么滑动转向用
queuedDir(下一 tick 才生效)而不是立刻转弯?
留下三样东西:你一局的结果(你几分、NPC 几分、谁赢)、你第一次转向的方向(上/右/下/左)、"贪吃蛇在板子里是什么"的一句话。
十、还能玩什么
| 作品方向 | 描述给 AI 的效果 | 再想想 |
|---|---|---|
| 经典死法 | 撞墙也直接死 | 把 bounceDir 换成"撞墙判死",改哪里? |
| 我是一条蛇 | 去掉 NPC,只有一条蛇自己玩 | chooseNpcDir 还留着吗? |
| 串串香 | 每 15 秒多刷新一颗食物 | 食物生成用定时器,怎么跟移动 tick 分开? |
| 贪吃满分 | 增加"最高分"记录,打完显示历史最佳 | 最佳分存 TF 卡还是掉电就丢? |
| 双人齐心 | 两条蛇都人控制 | 触摸只报一个触点,怎么做双指控制两蛇? |
下节课预告:学会"网格 + 方向 + 状态机"后,第 31 课俄罗斯方块也是这张网格、也有方向和状态机,只是改成方块往下掉——这个三角摸熟,俄罗斯方块就是小菜一碟。
