你好,我是吴老师。这份是给你自己读的——不用等我讲,看不懂就整段复制给 AI,加一句"请一次只解释一个概念,讲完问我一个小问题"。
读完之后,你应该能:
- 说出"对象池"是什么(用几个固定槽位 + 一个开关来"借还");
- 说清"判断子弹打中敌机"用哪两个数比较、为什么要"近似";
- 明白"先画到 sprite 再 pushSprite"为什么能让游戏不闪,且这块画布要放 PSRAM。
用时:约 60 分钟。 准备:烧录
demo01_plane_battle的板子 + 一根 USB 线。
一、让飞机满屏飞
前四课的游戏都"待在一个格子里"。今天让一架飞机在整块屏幕上自由地飞、自动开火、撞击敌机——它不再用网格,改成"每个东西记住自己的位置和速度"。
现象:飞机跟着你的手指飞,自动射击,头顶分数往上跳;被撞到就 GAME OVER。
二、我观察到的现象
| 现象 | 勾选 |
|---|---|
| 手指按住,飞机就飞过去 | |
| 每隔一会儿自动打一颗子弹 | |
| 敌机从顶上不断掉下来 | |
| 子弹打中敌机,敌机没了、分数 +1 | |
| 敌机撞到飞机,GAME OVER |
三、子弹和敌机:先"登记"再"出动"——对象池
同时可能有十几颗子弹、几架敌机,总不能让每颗都"新建"吧?太慢还占内存。
用一个巧法——对象池:先预备好固定数量的"空位置"(12 颗子弹、10 架敌机的"槽位"),每个槽位有个 on 开关。
struct Bullet { int x, y; bool on; };
Bullet bullets[12]; // 预备 12 颗子弹的"停车场"
- 发射:找第一个
on==false空槽,填位置,on=true; - 飞出屏:
on=false,槽位还给下一颗。
像停车场:不是每来一辆车就盖一个新停车场,而是先画好一批车位,车进"在用"车位、开走就"释放"。位置固定、数量有上限,又快又省内存。
最该记住的:子弹、敌机是先备好槽位、用 on 开关借还——这就是对象池。
四、两个代码名,够了
1. 碰撞检测——"两样东西有没有碰到"
看两样东西的 x 差值和 y 差值,是不是都小于某个"半径和"。
dx = |子弹.x - 敌机.x|;
dy = |子弹.y - 敌机.y|;
if (dx < 半径和 && dy < 半径和) 就认为"打中了"。
真正的子弹、敌机形状不同,但判"碰没碰上"不需要那么准——挨得够近就算命中,这叫"近似碰撞",又快又够用。
2. fb.pushSprite(0, 0)——"整幅画一次性刷上去"
先把这一帧(飞机、子弹、敌机、分数)画到一块"草稿画布" sprite 上,
再 call 一次 pushSprite(0,0),把整块草稿一次性贴到屏幕。
为什么不一笔一笔画到屏?那样屏幕会一边画一边显示,快速运动就出现闪烁/撕裂。先画完整帧再整体刷,屏幕看到的永远是一张完整的画。这块草稿约 150KB,必须放 PSRAM(用
PSRAM=opi编译)。
最该记住的:先画草稿、再整块刷屏(sprite + pushSprite)是关键,需要大的 PSRAM 画布。
五、为什么用"自由坐标",而不是网格?
| 做法 | 怎么动 | 适合 |
|---|---|---|
| 网格(一格一格) | x、y 是整数格号 | 规则清晰的棋类/蛇 |
| 自由坐标(像素) | x、y 是像素,每帧加速度 | 平滑连续的射击/飞行 |
网格适合"对齐",坐标适合"流畅"。 飞机要满屏平滑飞,就用坐标;每帧 y 加一个速度,就连贯下落。
六、改一个条件:变难或变简单
| 我改了什么 | 我预测 | 实际 |
|---|---|---|
敌机速度 y+=2 → y+=4 | 改 updateGame() 那行,其余不动 | |
发射间隔 FIRE_MS 改小 | 子弹更密,更好打中 |
难不难,往往只是一个数字。 速度/间隔/半径都是常量,改一个,整个节奏就变——这是游戏好调的秘密。
七、一条完整的"打一颗子弹并命中"链路
飞机在 (px,py) → 每隔 FIRE_MS 发一颗子弹(找空槽, on=true)
每 40ms(约25帧/秒):子弹 y 减6(往上),敌机 y 加2(往下)
撞不撞:每颗子弹×每架敌机,比 x/y 差值与半径和
够近 → 两个都 on=false 消失, score++
敌机撞飞机 → GAME OVER over=true
每帧:把飞机/子弹/敌机/分数画到 sprite 草稿
再 pushSprite(0,0) 一次刷上屏 → 不闪
GAME OVER 后点屏重开:分数清零, 所有槽位 on=false
真正要记住的只有一句:飞机大战 = 对象池(借还槽位)+ 近似碰撞(比差值)+ sprite 整帧刷屏(不闪)。
八、打不中 / 卡顿 / 判错,就这样排查
| 现象 | 观察还是推测 | 查什么 |
|---|---|---|
| 飞机不跟手 | 观察 | 触摸坐标接对没 |
| 打了半天敌机不清 | 观察 | 敌机出屏释放 on=false 没 |
| 子弹打中只加一次分 | 观察 | 命中后把两个都设 false 没 |
| 一闪一闪 | 观察 | 用 pushSprite 整帧刷没 |
| 越刷越卡 | 观察 | 槽位释放没 |
九、过关检查
- "对象池"是什么?为什么用固定槽位?
- 判断"子弹打中敌机"用哪两个数?为什么不用很精确?
- 为什么先画 sprite 再 pushSprite 就不闪?画布多大、放哪?
- 飞机为什么用坐标、贪吃蛇用网格?
- 想敌机更快更密,改哪几个数?
- ScreenMirror 镜像到网页有什么用?
留下三样东西:你打到最高分的那局、你改的参数、"游戏为什么流畅又不闪"的一句话。
十、还能玩什么
| 作品方向 | 描述给 AI 的效果 | 再想想 |
|---|---|---|
| 敌机编队 | 一排敌机一起下压 | 怎么让一排一起动? |
| 道具掉落 | 加速/护盾/三连发 | 道具给玩家加什么? |
| 三条命 | 扣命而不是直接结束 | "生命值"也是状态,改哪? |
| 双人积分赛 | 两块板对打比分数 | 需要什么通信? |
| 试玩迭代 | 请人试玩、收集反馈、改一版 | 反馈哪些是现象、哪些是推测? |
下节课预告:第六章游戏开发到此收尾。下一章让板子管学习——第 34 课做一张能翻的课程表。
