LEVEL 01 / 会动会打的游戏
把半成品,改成能玩的游戏
老师已经搭好了游戏的基础。你来补完 4 个函数,让子弹飞起来、击中敌人,再让敌人追着你跑。每通过一关,就用 Git 提交一次。
从这里开始 →👋 0 · 开始这里
欢迎。今天你要把这个半成品补完——补完之后,它就是一个真正能玩的游戏:能移动、能射击、能消灭敌人,敌人也会追你。
🎯 今天你要做什么
填 4 个函数(代码里标了 ★TODO),把半成品变成完整游戏;每填完一个就提交一次(commit)。同时会练到 for、if、数组、碰撞检测,以及 git 的两个要点:clone(获取代码) 和 commit(保存进度)。
🗺️ 通关路线图
| 关卡 | 补完后游戏会 |
|---|---|
| 关卡 1 | 子弹会飞、飞出屏幕会回收 |
| 关卡 2 | 按方向键能射出子弹 |
| 关卡 3 | 子弹能消灭敌人、加分 |
| 关卡 4 | 敌人会追你、撞到你会掉血 |
★TODO,不要四个一起写。🧰 开工前 60 秒自检
在 MSYS2 UCRT64 终端里,两条都显示版本号即可:
g++ --version # C++ 编译器 git --version # 版本控制(提交)要用它
再确认 git 知道“你是谁”(提交需要署名):
git config --global user.name "你的名字或昵称" git config --global user.email "一个邮箱地址"
📥 获取这一课的代码
第一次用?先照 课程主页的「第一次配置」 把 fork 和 upstream 配好(只做一次)。配好后,每开一门新课就这两条:
git fetch upstream # 取老师最新的课 git switch -c my-lesson-1 --no-track upstream/lesson-1 # 从第1课起点开一条自己的分支
先到 1 工具速查 熟悉 make / make test,再回到 关卡 1。出现红色(未通过)不用着急——让代码从红变绿,正是今天要学的内容。
🧰 1 · 工具速查
这是你的操作说明。命令都在 MSYS2 UCRT64 终端、工程根目录 first-frame/ 下运行。做关卡时可随时回来查阅。
四条 make 命令
| 命令 | 作用 |
|---|---|
make | 编译,生成 build/game.exe |
make run | 编译并打开游戏窗口(手动试玩) |
make test | 运行自动评测,不打开窗口,只报告哪些通过、哪些未通过 |
make clean | 删除编译产物,需要从头开始时使用 |
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-raylib看懂 make test 的红和绿
它是个自动评分器(autograder),按 4 个 TODO 分组打分:
② FireBullets —— 按方向键发射 [通过] 没按键→0 颗子弹 [未通过] 按↑只发 1 颗(少 break 会喷一大串) (期望 1,实际 128) (test_todos.cpp:123) —— 本组通过 8 / 10 ——
- 🟢
[通过]= 正确;🔴[未通过]= 有误。 - 括号里的 (期望 X,实际 Y) 会直接指出差异所在。
- 行尾
(test_todos.cpp:123)是检查所在的行号。
提交三步(commit)
git status # ① 查看我改动了哪些文件 git add src/main.cpp # ② 放进「待提交」区 git commit # ③ 正式提交(弹出模板,照提示填写标题)
git config commit.template .gitmessage,git commit 才会弹出中文模板。1子弹会飞 🚀
补完 UpdateBullets,让每颗子弹按速度飞行,飞出屏幕就自动回收。
🎯 这一关学会
- 用
for遍历数组(子弹池) - 用
if (!active) continue;跳过没在用的对象 - 改
rect.x / rect.y让东西动起来
📖 概念:对象池
子弹不临时创建也不删除,而是开一个固定数组 bullets[128],每颗身上有开关 active:true 正在飞,false 空位待命。“回收”就是把 active 关掉。
📋 你要做到
对每颗 active 的子弹:① 按速度移动(x += vx,y += vy);② 飞出屏幕(0~SCREEN_W / 0~SCREEN_H 之外)就 active = false。没在用的直接跳过。
🤔 先自己想一想
- 怎么遍历所有子弹?范围多少?
- 没在用的用什么语句跳过?
- “移动”改哪个值?(想想范例
UpdatePlayer) - 怎么判断“飞出屏幕”?
🪜 卡住了再看:分步提示先自己试,真卡住再点开▸
for从0到MAX_BULLETS - 1。- 开头
if (!bullets[i].active) continue;—— 空位跳过。 - 让它动:
rect.x += vx; rect.y += vy;(同范例UpdatePlayer)。 - 出界:
x<0 || x>SCREEN_W || y<0 || y>SCREEN_H任一成立就active = false;。
🧪 怎么验证
make test # 看「① UpdateBullets」那一组
会测:按 vx/vy 移动、空子弹不动、屏内不回收、四条边界各飞出一次都要回收。
🚀 想挑战(选做)
思考题:飞出屏幕后为什么只写 active = false,而不真的“删除”这颗子弹?
通关条件:
make test 里 ① 组全绿 → 提交:git commit2四方向射击 🎯
补完 FireBullets,按 ↑↓←→ 时从子弹池里找一颗空的射出去。
🎯 这一关学会
IsKeyPressed和IsKeyDown的区别- 在对象池里找一个空位并初始化(同
SpawnEnemies) break为什么关键
📖 概念:找空位 + 屏幕坐标
找空位:发射不是造新子弹,而是去 bullets[] 找 active==false 的填好打开(照 SpawnEnemies)。屏幕坐标:原点在左上角,y 向下变大 —— 所以“向上射” vy = -8。
📋 你要做到
按 → vx=8、← vx=-8、↑ vy=-8、↓ vy=8。有按键才找一颗 !active 子弹:放到玩家身上、设速度、active=true,随后立即停止(不要一次填满整个子弹池)。没有按键就不应产生子弹。
🤔 先自己想一想
- 用
IsKeyPressed还是IsKeyDown?(按一下 vs 按住狂喷) - 子弹从哪来?(去池里找
!active,同SpawnEnemies) - 找到、填好之后最容易忘的一步是什么?
🪜 卡住了再看:分步提示先自己试,真卡住再点开▸
- 用
IsKeyPressed(按一下发一颗)。 - 照
SpawnEnemies骨架:找到!active→ 填好 →active=true→break。 - 方向对速度:右 8、左 -8、上 -8、下 8。
- 起点让子弹从玩家身上冒出来,大小给个
8×8。
break → 一次按键喷上百颗。make test 会用“按↑只发 1 颗”抓你:看到 期望 1,实际 128 就是它。🧪 怎么验证
make test # 「② FireBullets」组:测方向、只发一颗 make run # 手动:按 ↑↓←→,看四方向都射出小方块
自动测试能检查方向和“只发一颗”,但真实手感仍需 make run 亲手体验。
🚀 想挑战(选做)
把 IsKeyPressed 换成 IsKeyDown 感受“连发”,再换回来。
通关条件:② 组全绿 +
make run 四方向都能射 → 提交:git commit3击中得分 💥
补完 HandleHits,每颗子弹和每个敌人两两检查,撞上就双方消失、加 10 分。
🎯 这一关学会
- 嵌套循环:两层
for,子弹配敌人 - 用现成的
CheckCollisionRecs,不用自己算坐标 - 循环里下标别用混(
i归子弹,j归敌人)
📖 概念
嵌套循环:外层每颗子弹,内层每个敌人,逐对检查。AABB 碰撞:CheckCollisionRecs(a, b) 传两个矩形,返回它们有没有重叠,你只管调用。
📋 你要做到
外层遍历子弹、内层遍历敌人;两边都先跳过 !active;用 CheckCollisionRecs 判断相撞;撞上了 子弹 active=false、敌人 active=false、score+=10,然后跳出内层。
🤔 先自己想一想
- 结构长什么样?(外层什么、内层什么)
- 怎么判断两个方块撞上了?
- 撞上后要改哪三样?
- 撞上后要不要
break?为什么?
🪜 卡住了再看:分步提示先自己试,真卡住再点开▸
- 外层
for子弹i,开头if(!bullets[i].active)continue;。 - 内层
for敌人j,开头if(!enemies[j].active)continue;。 if(CheckCollisionRecs(bullets[i].rect, enemies[j].rect))成立:两个active=false、score+=10、break;。
enemies[j] 不是 enemies[i](下标用混会打错敌人,甚至越界)。② 内外层都要先 continue 跳过空位,否则会撞到“已消灭但坐标还在”的幽灵敌人,导致平白加分。🧪 怎么验证
make test # 「③ HandleHits」:命中、错位下标、幽灵敌人、不相撞 make run # 射敌人:红方块消失、Score 每次 +10
🚀 想挑战(选做)
思考题:如果不写 break,一颗子弹会不会一次消灭好几个敌人?对分数有什么影响?
通关条件:③ 组全绿 + 射中会消失、+10 分 → 提交:git commit
4敌人追人 👾
补完 UpdateEnemies,敌人一点点靠近玩家;撞到就扣血、敌人消失;血空了 Game Over。压轴关,做完游戏成型。
🎯 这一关学会
- 不用勾股/向量,只靠比大小就能追人
- 逐轴处理(x 一次、y 一次)
- 用状态标志
gameOver控制结束
📖 概念:逐轴逼近
“靠近玩家”不用算距离开平方,只要分别比 x 和 y:玩家在右(x 更大)→ 敌人 x += speed,在左 → x -= speed;玩家在下 → y += speed,在上 → y -= speed。x、y 各判一次,敌人就斜着逼过来。
📋 你要做到
对每个 active 敌人:① 逐轴靠近玩家(各 ±speed);② CheckCollisionRecs(敌人, player) 判撞;③ 撞到 player.hp -= 10、敌人 active=false;④ hp <= 0 就 gameOver = true。
🤔 先自己想一想
- “x 靠近玩家的 x”,用什么判断、往哪挪?y 呢?
- x 和 y 该同一帧都动,还是只动一个?
- 撞到后改哪些?什么时候游戏才结束?
🪜 卡住了再看:分步提示先自己试,真卡住再点开▸
for遍历敌人,if(!enemies[i].active)continue;。- 用四个独立的
if逐轴逼近(x 大 +=speed / x 小 -=speed;y 同理)。 if(CheckCollisionRecs(enemies[i].rect, player.rect)):hp-=10、敌人active=false。- 扣完血
if(player.hp<=0) gameOver=true;。
if,别连成 else if —— 连起来一帧只动一个轴,敌人只会横着或竖着走,不会斜着追。make test 有“对角同时靠近”专抓这个。方向也别比反。🧪 怎么验证
make test # 「④」:x/y 靠近、对角同时动、掉血、gameOver make run # 敌人追你、撞上掉血、血空 GAME OVER、Enter 重开
🚀 想挑战(选做)
让敌人越来越快(每隔一会给 speed 加一点);或给玩家画个血条。
通关条件:④ 组全绿 + 敌人会追、撞上掉血、血空 Game Over → 提交:git commit
🛟 卡住了看这里
出现红色(未通过)不用着急。程序员大部分时间并不是一次就写对,而是「写一点 → 测试 → 发现问题 → 修改 → 再测试」。这就是 Debug,和写代码同样重要。
🔧 测试变红:按这个顺序查
- 哪一条失败?读那句中文和
(期望 X,实际 Y)。 - 是哪一关?失败行属于 ①②③④ 哪组,就只改那一关。
for范围对不对?(0到MAX_… - 1)active判断对不对?有没有先if(!active)continue;?rect.x / rect.y改对了吗?- 忘了
continue/break没?(关卡 2、3 尤其容易忘break) - 改完
make test再看一遍。
📋 常见症状对照表
| 症状 | 多半是 |
|---|---|
make 报一堆红字 | 漏了分号或括号没配对 —— 看第一条错误的行号 |
| 射击没反应 | 用成 IsKeyDown?起点跑屏幕外?忘了 active=true? |
| 一按键喷一大串子弹 | FireBullets 找到空位后忘了 break |
| 敌人不动 / 往反方向跑 | 比大小方向反了,或 speed 是 0 |
| 平白无故加分 | 撞到“已死但坐标还在”的幽灵敌人 —— 内层要先 continue |
git commit 说 nothing to commit | 没 git add,或文件没真改动 —— 先 git status |
make run 亲手体验。🎉 通关验收
四关都做完了?从干净状态重头跑一遍:
make clean && make && make test && make run
✅ 逐条对照(九条全中就通关)
✅ 玩家可以移动 ✅ 玩家可以射击 ✅ 子弹可以飞行 ✅ 子弹飞出屏幕后被回收 ✅ 子弹可以消灭敌人 ✅ 消灭敌人获得 10 分 ✅ 敌人会追玩家 ✅ 敌人碰到玩家会扣血 ✅ HP 归零后 Game Over
判定:make test 全绿 + make run 手感对。两个都满足,通关 🏆。
🧠 今天你真正练到的
for 遍历 · if 判断 · continue 跳过 · break 提前结束 · 数组 · active 状态(对象池)· 碰撞检测 · 函数拆分 · Debug。还有 git 两件大事:clone(获取代码)和 commit(保存进度)。
git commit;若想把成果备份到你自己的 GitHub,运行 git push -u origin my-lesson-1(推送到你的 fork,不会影响老师的库)。下一课预告:怪物图鉴 —— 给游戏换上贴图,认识打法不同的敌人。也可以回到课程地图看看后面的冒险。恭喜通关!你把一个半成品,一行行改成了能玩的游戏 👏