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删除编译产物,需要从头开始时使用
💡 缺 raylib?第一次若提示缺库,先装一次:
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                 # ③ 正式提交(弹出模板,照提示填写标题)
一次性设置clone 后每台电脑跑一次 git config commit.template .gitmessage,git commit 才会弹出中文模板。

1子弹会飞 🚀

补完 UpdateBullets,让每颗子弹按速度飞行,飞出屏幕就自动回收。

⏱ 预计 20 分钟🧩 前置:看完 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)
  • 怎么判断“飞出屏幕”?
🪜 卡住了再看:分步提示先自己试,真卡住再点开▸
  1. for 从 0 到 MAX_BULLETS - 1。
  2. 开头 if (!bullets[i].active) continue; —— 空位跳过。
  3. 让它动:rect.x += vx; rect.y += vy;(同范例 UpdatePlayer)。
  4. 出界:x<0 || x>SCREEN_W || y<0 || y>SCREEN_H 任一成立就 active = false;。

🧪 怎么验证

make test      # 看「① UpdateBullets」那一组

会测:按 vx/vy 移动、空子弹不动、屏内不回收、四条边界各飞出一次都要回收。

🚀 想挑战(选做)

思考题:飞出屏幕后为什么只写 active = false,而不真的“删除”这颗子弹?

?未连接
通关条件:make test 里 ① 组全绿 → 提交:git commit

2四方向射击 🎯

补完 FireBullets,按 ↑↓←→ 时从子弹池里找一颗空的射出去。

⏱ 预计 22 分钟🧩 前置:关卡 1 已通过📄 改 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)
  • 找到、填好之后最容易忘的一步是什么?
🪜 卡住了再看:分步提示先自己试,真卡住再点开▸
  1. 用 IsKeyPressed(按一下发一颗)。
  2. 照 SpawnEnemies 骨架:找到 !active → 填好 → active=true → break。
  3. 方向对速度:右 8、左 -8、上 -8、下 8。
  4. 起点让子弹从玩家身上冒出来,大小给个 8×8。
⚠️ 最常见的坑找到空位、发射后忘了 break → 一次按键喷上百颗。make test 会用“按↑只发 1 颗”抓你:看到 期望 1,实际 128 就是它。

🧪 怎么验证

make test      # 「② FireBullets」组:测方向、只发一颗
make run       # 手动:按 ↑↓←→,看四方向都射出小方块

自动测试能检查方向和“只发一颗”,但真实手感仍需 make run 亲手体验。

🚀 想挑战(选做)

把 IsKeyPressed 换成 IsKeyDown 感受“连发”,再换回来。

?未连接
通关条件:② 组全绿 + make run 四方向都能射 → 提交:git commit

3击中得分 💥

补完 HandleHits,每颗子弹和每个敌人两两检查,撞上就双方消失、加 10 分。

⏱ 预计 20 分钟🧩 前置:关卡 2 已通过📄 改 HandleHits

🎯 这一关学会

  • 嵌套循环:两层 for,子弹配敌人
  • 用现成的 CheckCollisionRecs,不用自己算坐标
  • 循环里下标别用混(i 归子弹,j 归敌人)

📖 概念

嵌套循环:外层每颗子弹,内层每个敌人,逐对检查。AABB 碰撞:CheckCollisionRecs(a, b) 传两个矩形,返回它们有没有重叠,你只管调用。

📋 你要做到

外层遍历子弹、内层遍历敌人;两边都先跳过 !active;用 CheckCollisionRecs 判断相撞;撞上了 子弹 active=false、敌人 active=false、score+=10,然后跳出内层。

🤔 先自己想一想

  • 结构长什么样?(外层什么、内层什么)
  • 怎么判断两个方块撞上了?
  • 撞上后要改哪三样?
  • 撞上后要不要 break?为什么?
🪜 卡住了再看:分步提示先自己试,真卡住再点开▸
  1. 外层 for 子弹 i,开头 if(!bullets[i].active)continue;。
  2. 内层 for 敌人 j,开头 if(!enemies[j].active)continue;。
  3. 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。压轴关,做完游戏成型。

⏱ 预计 20 分钟🧩 前置:关卡 3 已通过📄 改 UpdateEnemies

🎯 这一关学会

  • 不用勾股/向量,只靠比大小就能追人
  • 逐轴处理(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 该同一帧都动,还是只动一个?
  • 撞到后改哪些?什么时候游戏才结束?
🪜 卡住了再看:分步提示先自己试,真卡住再点开▸
  1. for 遍历敌人,if(!enemies[i].active)continue;。
  2. 用四个独立的 if 逐轴逼近(x 大 +=speed / x 小 -=speed;y 同理)。
  3. if(CheckCollisionRecs(enemies[i].rect, player.rect)):hp-=10、敌人 active=false。
  4. 扣完血 if(player.hp<=0) gameOver=true;。
⚠️ 关键坑x、y 要用四个独立的 if,别连成 else if —— 连起来一帧只动一个轴,敌人只会横着或竖着走,不会斜着追。make test 有“对角同时靠近”专抓这个。方向也别比反。

🧪 怎么验证

make test      # 「④」:x/y 靠近、对角同时动、掉血、gameOver
make run       # 敌人追你、撞上掉血、血空 GAME OVER、Enter 重开

🚀 想挑战(选做)

让敌人越来越快(每隔一会给 speed 加一点);或给玩家画个血条。

?未连接
通关条件:④ 组全绿 + 敌人会追、撞上掉血、血空 Game Over → 提交:git commit

🛟 卡住了看这里

出现红色(未通过)不用着急。程序员大部分时间并不是一次就写对,而是「写一点 → 测试 → 发现问题 → 修改 → 再测试」。这就是 Debug,和写代码同样重要。

🔧 测试变红:按这个顺序查

  1. 哪一条失败?读那句中文和 (期望 X,实际 Y)。
  2. 是哪一关?失败行属于 ①②③④ 哪组,就只改那一关。
  3. for 范围对不对?(0 到 MAX_… - 1)
  4. active 判断对不对?有没有先 if(!active)continue;?
  5. rect.x / rect.y 改对了吗?
  6. 忘了 continue / break 没?(关卡 2、3 尤其容易忘 break)
  7. 改完 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,不会影响老师的库)。下一课预告:怪物图鉴 —— 给游戏换上贴图,认识打法不同的敌人。也可以回到课程地图看看后面的冒险。

恭喜通关!你把一个半成品,一行行改成了能玩的游戏 👏

↑ 课程目录