第3课 · 大地图 → 镜头 → 碰撞 → 声音
我的竞技场
上一课用种类和配置表,让怪物有了不同的速度、血量和贴图。这一课继续改这个游戏:先像改配置一样扩大地图、摆一块新墙,再让镜头跟随玩家,最后补上挡墙和声音。仍然写一点、跑一次,看看画面怎样变化。十项可以分几次完成。
本节学习目标
- 用二维数组的行列定位,把地图扩到比窗口大,并换算格子与世界坐标(01、02)。
- 用 Camera2D 让镜头跟随玩家,分清“世界里绘制”和“固定在屏幕的文字 HUD”(03–05)。
- 用整个身体的矩形做碰撞:挡住墙、留在世界内,并能沿墙滑动(06、07)。
- 让射击声、命中声在“真正发生”的事件上触发,而不是每帧乱响(08、09)。
- 自己设计一个能暴露错误的边界测试,并讲清判断依据(10)。
开始前:找到任务
先取回本课起始代码,再创建练习分支
本课起始代码:lesson-3 分支。在你自己的复刻仓库目录中,先保存旧课的修改,再取回老师提供的起始代码:
git fetch upstream git branch -r --list upstream/lesson-3
看到 upstream/lesson-3 才继续;如果没有看到,先向老师确认分支是否取回,不要用main或旧课代码代替本课起点。
git switch -c my-lesson-3 --no-track upstream/lesson-3 make todo make run make test
已有 my-lesson-3 则用 git switch my-lesson-3 返回,别重复创建。保存一项后,检查 git diff,只提交自己改的学生文件;首次备份用 git push -u origin my-lesson-3。不要合并旧课整份 main.cpp 覆盖新起点;需要时仅迁移自己已完成的配置表。复刻需启用 Actions,详见 进度与提交。
三步找到当前任务
十项任务共有十二个唯一定位标记:01 与 07 各有 A/B 两处;其余各一处。
- 打开
src/main.cpp,看文件开头的任务目录;第10项在src/my_cases.h。 - 在这份课包的MSYS2 UCRT64终端运行
make todo,查看每个标记所在的文件和行号。代码修改后行号可能变化,用标记查找更方便。 - 从下面任务复制唯一
TODO(L3-xx...)标记,回到指定文件按Ctrl+F。先读标记前后的代码,弄清输入和用途,再修改这项任务对应的部分。提示只帮助你迈出下一步,具体实现由你完成。
make todo make run make test
在课包根目录(有Makefile的那一层)运行。起始代码可以编译;测试会指出哪些任务还没有完成。先关注当前任务对应的结果,保留tests中的原测试,用它们检查自己的实现。WASD移动,方向键射击,G看格线/身体框,ENTER重开,TAB切练习/战斗,Escape退出。
图片和声音资源已准备好,程序也已经安排了声音的加载、更新和释放,以及镜头初始化、玩家移动后的镜头更新调用。扩地图、镜头函数内容和绘制分层仍由你实现。这次只需要决定声音在什么事件发生时播放,不需要修改声音的加载、更新和释放。加载只在准备阶段进行,不要每帧重新加载。
写一点,运行一次
- 先看现象:运行游戏,看看本项要改变什么;再对照现有代码和下面的说明。
- 试一小步:先改一个数值或试一个局部例子,不必一次写完整个函数。
- 先预测:运行前想一想画面或数值会怎样变化。
- 动手改:找到本项标记,完成对应改动。
- 检查:make test看对应组,make run观察实际效果。
- 说清原因:比较预测与实际,用自己的话说明为什么。
下面会直接说明动手需要的概念,不用先通读手册。卡住时逐级打开提示;想确认某个字段或函数的用途,再查对应资料。
可选参考:在MSYS2安装目录的 ucrt64/include/raylib.h 搜索 Camera2D、BeginMode2D、EndMode2D、CheckCollisionRecs。需要确认字段或参数时,再查对应说明。可主动打开 raylib官方cheatsheet查函数签名,也就是函数名、参数和返回类型;学习内容可离线阅读;在线时字体与 GitHub 自动进度可能发起请求。
先认识这份代码
先找main中的循环,再顺着调用看。本课沿用上一课的怪物配置,不需要从头写游戏。
- 文件:
src/main.cpp放游戏数据、初始化、每帧更新和绘制;src/my_cases.h放第10项自选案例;src/assets.h和src/audio.h已处理图片、音乐与音效;Makefile提供编译、运行、定位和测试命令。tests/是检查工具,保持不改。 - 数据:
player记录身体、速度和血量;bullets、enemies是带active开关的数组;wall[行][列]记录哪格有墙;camera决定世界怎样画到窗口。 - 开场:创建窗口 → 加载图片和声音 → InitGame。InitGame先建地图,再设置玩家、清空子弹和怪物、重置分数等状态,最后调用InitCamera。ENTER重开、TAB切模式也会调用InitGame。
- 每帧:先处理TAB、G、ENTER;游戏未结束时,依次更新玩家 → 发射子弹 → 更新子弹 → 刷怪 → 更新怪物 → 处理命中;随后更新镜头 → 更新音乐 → 绘制画面。练习模式不刷怪。退出时释放声音、图片,再关闭窗口。
- 三种工作:更新函数改变位置或血量;HitsWall、InsideWorld只回答能不能走;DrawGame把当前数据画出来,不替移动做判断。
你补的部分:01地图尺寸和新墙、02格子矩形、03/04镜头、05绘制范围、06/07挡墙与边界、08/09事件音效、10自选案例。调用顺序和资源加载、更新、释放已准备好;起点中的空函数和临时写法,仍要在对应标记处完成。
世界坐标与屏幕坐标
世界坐标:描述墙和角色在整张地图中的位置。屏幕坐标:描述画面上的点在窗口中的像素位置。这里的镜头(相机)由Camera2D表示,决定把哪一块世界画到窗口中;它不改变角色和墙在世界中的位置。
地图行列 × TILE → 世界宽高 世界中的玩家中心 → camera.target 窗口中的屏幕中心 → camera.offset zoom = 1,rotation = 0
例如,镜头可以把世界点(1800,900)画在屏幕中心(640,360),角色的世界坐标仍然是原来的值,不需要减掉1280。碰撞检查始终比较世界中的矩形。本课主线让玩家一直居中,不做平滑跟随,也不限制镜头在世界边缘的位置:走到边缘时,窗口会显示一部分地图外的空白。先观察并解释这个现象,选做C再研究怎样让镜头停在边缘。当前用方向键射击;以后做鼠标瞄准时,再研究把屏幕坐标转成世界坐标的GetScreenToWorld2D。
阶段一 · 地图与镜头
01 · 把地图扩到窗口以外
- 目标
- 先运行游戏,按G看格线,再找地图常量和InitMap中已有的wall记录。像上一课改配置表一样,这次先改地图行列数:世界宽=列数×TILE,高=行数×TILE。窗口保持1280×720,TILE保持40像素。这次把地图扩到200列×120行,也就是8000×4800像素,横向约6.25个窗口、纵向约6.67个窗口;至少在原一屏之外放一块墙。
- 怎样检查
- 检查放墙用的行列下标都在数组范围内;玩家出生时整个身体所在的区域要留空,新增墙不能与它重叠;先选一块原窗口外不远的新墙,记下行列和左上角世界坐标,05、07完成后走过去,按G检查墙与格线是否对齐,不必一开始找最远角。确认200列×120行对应8000×4800,并立刻 make test 核对01(这步可即时自查,不必等走到远端墙)。保留已有墙,试着在不同区域分散放几组墙,留出通道和空地;不必一次摆满,也不用增加地图系统或寻路功能。
- 修改位置
src/main.cpp→地图常量;InitMapTODO(L3-01-A)TODO(L3-01-B)- 先动手
- 对照已有wall记录放墙,用行列数和TILE算世界大小。先规划一条去新墙的路线:出生点右侧已有竖墙占row 5–11,墙底是480像素;先往下绕,玩家身体顶边达到y≥480后,便可从这堵墙下方往右走。练习模式按G对照格线,也留意其他墙。
- 想一想
- 改窗口尺寸、缩小砖,能证明世界变大了吗?你选的远端墙坐标为什么合法?
提示1 · 概念
窗口表示你一次能看见多少,数组表示世界有多少格。下标从0开始,最后一个有效下标是行数或列数减一。
提示2 · 思路
先在纸上算世界宽高与远端墙的行列,再改A;确认尺寸后才在B放墙。保留已有少量墙。
提示3 · 局部实验
小练习:8列、每格40像素,世界宽320;列下标7的左边是280,右边是320,列下标8已越界。改成10列时,宽度和最后一列左边各是多少?这个小例子只用来算,不是游戏地图尺寸。
02 · 把一格变成世界矩形
- 目标
- 按G看一格墙,再读CellRect:它要把行号row、列号col变成世界矩形Rectangle。矩形记录左上角x、y和宽、高;列向右增加,所以x=col×TILE,行向下增加,所以y=row×TILE,每格宽、高都是TILE。先算一格,再补函数。Rectangle的四个字段都是float;col*TILE这样的变量表达式算出int,放进花括号初始化时,要显式转换成float,避免“窄化转换”报错。
- 怎样检查
- 选择行列不同的格子,例如(3,7),算出左上角与尺寸;make test核对02,G格线中的墙位置与计算相符。
- 修改位置
src/main.cpp→CellRectTODO(L3-02)- 先算一格
- TILE=40时,(row,col)=(1,2)的左上角是(80,40)。换成(2,1)会怎样?先在纸上算。
- 想一想
- 为什么col影响x?矩形宽高的单位是什么?
提示1 · 概念
数组写行、列;世界横向是x,纵向是y。
提示2 · 思路
先分别算左边、上边、宽、高,不要把数组编号直接当像素。
提示3 · 局部实验
单表达式例子:static_cast<float>(col * TILE),意思是把列坐标的整数结果转成float。7*40是编译时已知、可精确表示的常量,col*TILE却含变量;花括号初始化对后者更严格。照这个思路处理字段类型即可,不必先研究C++标准。
03 · 初始化镜头
- 目标
- 把窗口想成观看大地图的一扇窗。Camera2D的target选一个世界点,offset决定把这个点画在窗口哪里。要让玩家居中,就把玩家身体中心对应到窗口中心;同时保持zoom(缩放倍数)为1、rotation(旋转角度)为0。先分别算出这两个中心,再填字段。
- 怎样检查
- make test核对03;05完成后起始画面玩家位于中心,ENTER/TAB重开也回到正确视图。
- 修改位置
src/main.cpp→InitCameraTODO(L3-03)- 需要时查
- 在raylib.h中只看Camera2D的target和offset注释,确认哪个是世界点、哪个是屏幕位置。
- 想一想
- target与offset分别属于哪个坐标系?为什么目标不是玩家左上角?
提示1 · 概念
在本机raylib.h中查这两个字段:target指定世界中的目标点,offset指定它画在屏幕上的位置。窗口左上角是屏幕坐标的原点。
提示2 · 思路
先独立算两个中心,再决定它们各填哪个字段。这一版先保持直接居中,不增加平滑跟随、交互缩放或世界边缘限制。
提示3 · 局部实验
纸上实验:zoom=1、rotation=0,target=(1000,500)、offset=(640,360),则世界点(1000,500)画在屏幕(640,360)。只让target.x增加40,世界中的方块不动,预测它在屏幕上往哪边移、移多少。画出对应点即可,不用另建程序。
04 · 让镜头每帧跟随
- 目标
- 03只设置开场镜头。玩家走动后,镜头也要跟着更新:每帧在移动完成后,把target更新为玩家身体的世界中心;offset仍是窗口中心。每帧就是程序更新并画出下一幅画面的过程。完成05后,试着走动,看玩家是否仍居中。
- 怎样检查
- make test核对04;05、07完成后往右、往下走出原一屏,玩家仍居中;重开或切换模式后,镜头也应跟随重新出现的玩家,而不是停在上次的位置。
- 修改位置
src/main.cpp→UpdateCameraTODO(L3-04)- 先看调用
- 在现有代码中找到玩家移动与UpdateCamera的调用,确认更新顺序。
- 想一想
- 只在启动时设置target,会看到什么?移动前更新和移动后更新可能有什么差别?
提示1 · 概念
角色的世界位置变,映射到的屏幕位置可以不变。
提示2 · 思路
看看程序先移动玩家还是先更新镜头。初始化和移动后每帧更新的调用已经写好,这项任务由你补上函数内部的计算。
提示3 · 局部实验
局部练习:矩形左上角(90,50)、尺寸(20,40),中心是(100,70)。向右移5后,世界中心怎样变?offset不变、target跟着中心变时,矩形中心在屏幕上会不会动?
05 · 世界跟着镜头,文字留在屏幕
- 目标
- 把地板、墙、玩家、怪物、子弹、格线和测试案例的矩形框放在相机绘制范围内;把HUD(显示血量、得分等信息的界面文字)、案例文字和结束画面放在范围外,用屏幕坐标绘制。
- 怎样检查
- make test核对05;走远后世界一起移动,HP/Score/案例文字不漂走;按G显示的矩形框仍与对应贴图重合;结束画面固定在窗口。
- 修改位置
src/main.cpp→DrawGameTODO(L3-05)- 需要时查
- 查看BeginMode2D和EndMode2D的参数与用途,确认相机变换从哪里开始、在哪里结束。
- 想一想
- 为何相机不能只包住玩家?案例矩形和案例文字该分别放哪里?
提示1 · 概念
BeginMode2D之后的绘制使用镜头变换,直到EndMode2D;碰撞矩形无需改成屏幕坐标。
提示2 · 思路
先给已有绘制调用分类,再决定范围边界。格线长度用世界尺寸。不能把EndDrawing当成EndMode2D。
提示3 · 局部实验
纸上排序实验:世界方块、屏幕标签各一个,用两个括号标出谁在2D模式内。自行查签名后写最小实验,不要先重排整个游戏。
阶段二 · 碰撞与边界
06 · 让整个身体检测墙
- 目标
- 先按G看身体框:左上角没进墙,不等于整个身体都在墙外。对宽、高为正的矩形,CheckCollisionRecs(a,b)检查重叠;只有边接触时返回false。本课的HitsWall还要求先检查尺寸:宽或高≤0的输入没有面积,直接返回false,不交给底层函数决定。这是本课函数的规则,不能假定底层函数也替你处理了这些输入。其余输入再检查每块墙,只返回判断,不移动角色或播放声音。
- 怎样检查
- 02先完成;make test核对06;案例(280,120,40,40)完全在墙里,(250,120,40,40)身体部分重叠,都应为true。再预测同一位置宽为0或高为0时的HitsWall结果,两种都应为false。
- 修改位置
src/main.cpp→HitsWallTODO(L3-06)- 先试两例
- 用提示3中的两个小矩形,分别验证部分重叠和刚好贴边;有疑问再查CheckCollisionRecs。
- 想一想
- 左上角在墙外,身体能否已进墙?return false放在哪才不会漏掉后面的墙?
提示1 · 概念
比较两个世界矩形,而非两个点。
提示2 · 思路
先排除宽或高非正的输入,再访问有效行列内的墙,用CellRect得到墙格矩形。发现重叠即可结束;所有墙检查完才判断没有墙。
提示3 · 局部实验
最小实验:在独立小程序比较Rectangle a={0,0,10,10}与b={9,0,10,10},先预测再调用CheckCollisionRecs。随后只把b.x改成10,观察两个矩形只有边接触、没有重叠面积时,函数返回什么。
07 · 留在世界里,并能沿墙移动
- 目标
- 向墙斜着走时,横向可能被挡住,纵向却还可以走。A检查整个身体的四条边是否在世界内,刚好贴世界边缘也算在内;B先试X,再试Y,每次只有未越界、未撞墙才接受新位置。试Y时从已接受的X位置开始,而不是重新回到本帧起点。
- 怎样检查
- 02、06先完成;make test核对07;四面撞墙不穿过,朝墙同时沿墙仍能走;超过旧1280/720边界可继续,世界边缘不能越出。
- 修改位置
src/main.cpp→InsideWorld;UpdatePlayerTODO(L3-07-A)TODO(L3-07-B)- 先画一步
- 画一个横向撞墙、纵向可走的候选位置;候选矩形就是身体矩形的副本,用来试走。
- 想一想
- 没有墙就能出世界吗?为什么第二次尝试要从已接受的X位置开始?
提示1 · 概念
世界边界关心矩形四条边,不是屏幕边界;镜头也不改变碰撞规则。
提示2 · 思路
逐步推演:身体从(10,10)开始,本帧想向右、向下各走5。先试(15,10),如果撞墙就仍在(10,10);再试(10,15),若合法就接受。反过来,X成功时试Y应从(15,10)出发。把候选位置检查完再移动,不必先移动再回退。
提示3 · 局部实验
局部副本实验:Rectangle candidate=player.rect; 只改candidate.x,观察player.rect是否随之改变。再解释这为何能用于试走。
阶段三 · 声音与自己的证据
08 · 子弹成功生成才响
- 目标
- 先射击,看新子弹何时出现,再读FireBullets中已有的“找空槽→激活子弹→break”。只在成功激活一颗新子弹时播放射击声,保留break;按键发生但没有空槽时,不应响。
- 怎样检查
- make test核对08;正常一枪响一次,子弹池(预先准备的一组可重复使用的子弹位置)全部被占用时,没有新子弹,也不应播放新的射击声;实际游戏试听。
- 修改位置
src/main.cpp→FireBulletsTODO(L3-08)- 先看代码
- 找到子弹active从false变成true的位置,查看PlayShotSound的声明;不用研究音频加载。
- 想一想
- 按下射击键就一定能生成子弹吗?每帧响与每次生成响有什么不同?
提示1 · 概念
声音属于发生的事件。
提示2 · 思路
沿着现有循环读一遍:什么时候找到空槽,什么时候激活子弹,什么时候break?再比较所有槽都被占用时会经过哪些语句。
提示3 · 局部实验
纸上实验:只有两个槽且都active,按向右的方向键。逐条走过现有循环,记下生成数与声音数的预期,不需真的打满128颗。
09 · 实际扣血就响,包括没死
- 目标
- 上一课已经区分了“扣血”和“死亡加分”。先打一次多血怪,再读HandleHits中已有的扣血与死亡判断:每次有效命中并实际扣血都要播放命中声,没死也响;死亡加分逻辑保持不变。
- 怎样检查
- make test核对09;未命中不响;多血怪非致命命中也响,每次有效命中一次;游戏试听并观察得分时机(练习模式不刷怪,切到战斗模式按TAB,等多血怪出现再试听)。
- 修改位置
src/main.cpp→HandleHitsTODO(L3-09)- 先看代码
- 找到实际扣血的语句,比较它与hp<=0分支的位置;查看PlayHitSound的声明。
- 想一想
- 把声音放进hp<=0分支会漏掉什么?一个子弹命中后为何要失活并break?
提示1 · 概念
受伤与死亡不是同一件事。
提示2 · 思路
找到子弹与怪物相交后实际扣血的位置,看看子弹何时被设为不再活动,让声音对应这次有效命中。
提示3 · 局部实验
纸上实验:怪物初始3血,分别记录第1、2、3次有效命中后的血量、声音次数、是否加分,先预测再观察。
10 · 自己出一道会找出错误的题
- 目标
- 自己设计一个碰墙的边界案例。先写名字、输入矩形和预期expected,再启用enabled;运行后比较结果,说明这个案例能发现什么错误。
- 怎样检查
- 游戏案例文字显示预期与实际;G显示世界案例框。你能算四条边并解释结果。第10项用你的演示和解释与老师一起检查,不在自动评分的九组中。
- 修改位置
src/my_cases.h→myCase(src/my_cases.h)TODO(L3-10)- 先做预测
- CollisionCase中name是名字,rect是输入,expected是预期,enabled决定是否运行此案例。
- 想一想
- 这道题能抓住哪类错误?若实际与预期不同,哪个有独立依据?
提示1 · 概念
先根据矩形四条边的位置和重叠规则算出预期,再看程序输出,这样才能发现实现中的错误。
提示2 · 思路
可选刚好贴边、墙角部分重叠、跨多格的大矩形。坐标由你决定;先纸上画图。
提示3 · 局部实验
局部记录示例:name写测试意图,rect写你的输入,expected写预测;enabled最后打开。选什么矩形、预期是什么,由你根据地图和几何规则决定。
怎样读测试结果
TODO_STATUS id passed total列出01–09的通过数与总数。TODO_BLOCKED id prerequisite说明要先完成哪项任务;0/0是未执行,不是通过。检查九组是否都执行了测试并全部通过。需要注意的先后顺序是:02→06→07,03→04。第10没有自动评分,自己解释给老师听。
若合法的其他世界尺寸被判错,把预测、数值、输出带给老师,不要为了当前输出随意改预期。自动测试检查的是列出的功能;画面是否舒服、音量是否合适,还要亲自运行观察。现在的碰撞检查也没有处理高速移动时一步跨过墙的情况。
课后:先补主线,再从A/B选一
先完成还没完成的十项主线,再从A/B中选一题完成。C仅挑战选做。把未完成的任务号、函数和下一步写进笔记,先观察和试做;遇到新问题,再有针对性地查资料、做小实验,比较预测与实际。A/B和C沿用已有任务中的知识,不新增任务编号,也不计入九组自动进度;下节课通过演示和解释来检查。
带来程序、运行前的纸上预测、资料出处和一次排错记录。先读目标,自己决定最小实验,用资料和实验帮助自己完成实现,而不是照抄整份函数答案。
A · 设计通道
- 目标
- 保留原有墙和远端墙,并让玩家出生时整个身体所在区域留空,新增一条有直角拐弯的通道,分别设计40像素和80像素门洞。
- 先预测
- 在纸上画墙、门洞和身体。40像素玩家与游戏中的大体型怪各能否通过?相同宽度是否在任何位置都能通过?先查怪物实际身体尺寸,再写判断依据。
- 演示与检查
- 运行演示玩家通过或被阻挡的两种门洞与拐弯;至少准备两个独立碰撞案例,包含「刚好贴边」和「部分重叠」。每例先写矩形、预期与理由,再核对实际,不能按当前输出倒填预期。大体型怪的通行条件可用对应尺寸的矩形案例验证;不用实现怪物自动找路或BFS(广度优先搜索)。
- 自己查
InitMap、怪物身体尺寸、CheckCollisionRecs的贴边/重叠规则;可用第10项案例入口做实验,不新增自动组。- 要解释
- 门洞宽度、身体尺寸、位置对齐怎样共同影响通行?你的两个案例能揭露哪类错误?
B · 调试HUD
- 目标
- 在固定屏幕文字中显示玩家世界坐标、所在行列、相机target。明确坐标取玩家身体的哪个点,例如左上角;不要把它和相机追踪的中心混为一谈。
- 先预测
- 玩家移动一格时,坐标、行列和target各会怎样变?走过旧窗口边界后,文字应该在哪个坐标系里保持固定?
- 演示与检查
- 打开格线,走过旧窗口右边或下边,逐项对照地图和数字:文字固定在屏幕,行列对应你选定的玩家点,target符合主线玩家中心跟随。说明预测与实际的差别。
- 自己查
- raylib
DrawText、TextFormat;世界/屏幕坐标、像素/行列的概念。到官方头文件或cheatsheet查参数,再做最小文字实验。 - 要解释
- 为什么显示的位置用屏幕坐标,显示的数值却来自世界?玩家坐标和target为何可能不同?
C · 相机到世界边缘时停止留白(挑战选做)
完成主线04后,target始终跟随玩家身体的世界中心。走到世界边缘时,你会看到地图外的空白,因为镜头仍以玩家为中心。这个挑战尝试让镜头到边缘时停住,避免显示地图外;此时玩家不一定居中,它与04要求的始终居中是两种不同的跟随方式。
先保存一份已通过测试、可以运行的主线版本。在独立 my-lesson-3-extra 分支或单独的代码副本做挑战。若还不会分支,先用明确标为「主线」和「扩展」的不同目录,确保能分别运行,别覆盖主线。
- 自己查与预测
- 研究相机target可以移动到哪里,以及用于把数值限制在给定范围内的
Clamp。先画屏幕与世界边缘,预测target应受到怎样的限制,再到四角观察,不照抄完整实现。 - 演示与检查
- 分别展示保留的主线和扩展。扩展到四角观察世界外留白是否消失、HUD是否固定、玩家能否到达世界四边,并解释target为何不再处处等于玩家中心。
- 测试怎样看
- 原来的九组测试检查的是始终居中的主线,C的边缘限制由你演示、老师检查,不计入九组进度。保留原tests,分别运行保存的主线和挑战版本,说明各自结果;挑战尚未完成,也可以带着问题来讨论。
下节课:演示你的实现,讲清你的判断
- 展示选择与预测。指出主线还需补哪项、选了A还是B、是否挑战C,展示运行前写下的尺寸、坐标或四角预测。
- 运行演示。先演示补完的主线。A展示原墙与出生安全、40/80像素门洞和直角拐弯,核对玩家/大体型矩形以及贴边/部分重叠两例;B展示固定文字,走过旧窗口对照格线、世界坐标、行列与target;C先展示保存的主线版本和挑战版本各在哪个目录或分支,再走到四角与世界边缘。
- 解释资料与实验。指出哪份资料、哪一句支持你的判断,展示一个最小实验,比较预测与实际。不要只说「代码能运行」。
- 拿出失败案例与改进。用「预期 / 实际 / 尝试 / 最小代码 / 推断」讲一次失败和改进;没有自然出现的失败,也可解释实验排除了哪个错误想法,不用故意改坏正确代码。
老师会分别记录主线待补项、所选扩展和挑战C的「通过 / 待补」与理由。扩展验收独立于九组自动成绩;记下哪些地方还需要实验或说明,作为下一步计划。