第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 两处;其余各一处。

  1. 打开 src/main.cpp,看文件开头的任务目录;第10项在 src/my_cases.h。
  2. 在这份课包的MSYS2 UCRT64终端运行 make todo,查看每个标记所在的文件和行号。代码修改后行号可能变化,用标记查找更方便。
  3. 从下面任务复制唯一 TODO(L3-xx...) 标记,回到指定文件按Ctrl+F。先读标记前后的代码,弄清输入和用途,再修改这项任务对应的部分。提示只帮助你迈出下一步,具体实现由你完成。
make todo
make run
make test

在课包根目录(有Makefile的那一层)运行。起始代码可以编译;测试会指出哪些任务还没有完成。先关注当前任务对应的结果,保留tests中的原测试,用它们检查自己的实现。WASD移动,方向键射击,G看格线/身体框,ENTER重开,TAB切练习/战斗,Escape退出。

图片和声音资源已准备好,程序也已经安排了声音的加载、更新和释放,以及镜头初始化、玩家移动后的镜头更新调用。扩地图、镜头函数内容和绘制分层仍由你实现。这次只需要决定声音在什么事件发生时播放,不需要修改声音的加载、更新和释放。加载只在准备阶段进行,不要每帧重新加载。

写一点,运行一次

  1. 先看现象:运行游戏,看看本项要改变什么;再对照现有代码和下面的说明。
  2. 试一小步:先改一个数值或试一个局部例子,不必一次写完整个函数。
  3. 先预测:运行前想一想画面或数值会怎样变化。
  4. 动手改:找到本项标记,完成对应改动。
  5. 检查:make test看对应组,make run观察实际效果。
  6. 说清原因:比较预测与实际,用自己的话说明为什么。

下面会直接说明动手需要的概念,不用先通读手册。卡住时逐级打开提示;想确认某个字段或函数的用途,再查对应资料。

可选参考:在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表示,决定把哪一块世界画到窗口中;它不改变角色和墙在世界中的位置。

世界坐标与屏幕坐标示意一个大的世界矩形里有玩家,窗口只框住世界的一小块;玩家的世界坐标不随窗口移动而改变。世界 World(地图行列 × TILE)(0,0)x→y↓窗口 1280×720玩家世界(1800,900)窗口只框住世界的一小块;移动窗口,玩家的世界坐标不变。
地图行列 × TILE → 世界宽高
世界中的玩家中心 → camera.target
窗口中的屏幕中心 → camera.offset
zoom = 1,rotation = 0
Camera2D 的 target 与 offset镜头把世界中的 target 点,画到屏幕中 offset 指定的位置;本课让玩家一直居中。world 世界target(1800,900)screen 屏幕 1280×720offset(640,360) 窗口中心镜头把 target 画到 offsetzoom=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 → 地图常量;InitMap
TODO(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 → CellRect
TODO(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 → InitCamera
TODO(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 → UpdateCamera
TODO(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 → DrawGame
TODO(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 → HitsWall
TODO(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;UpdatePlayer
TODO(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 → FireBullets
TODO(L3-08)
先看代码
找到子弹active从false变成true的位置,查看PlayShotSound的声明;不用研究音频加载。
想一想
按下射击键就一定能生成子弹吗?每帧响与每次生成响有什么不同?
提示1 · 概念

声音属于发生的事件。

提示2 · 思路

沿着现有循环读一遍:什么时候找到空槽,什么时候激活子弹,什么时候break?再比较所有槽都被占用时会经过哪些语句。

提示3 · 局部实验

纸上实验:只有两个槽且都active,按向右的方向键。逐条走过现有循环,记下生成数与声音数的预期,不需真的打满128颗。

09 · 实际扣血就响,包括没死

目标
上一课已经区分了“扣血”和“死亡加分”。先打一次多血怪,再读HandleHits中已有的扣血与死亡判断:每次有效命中并实际扣血都要播放命中声,没死也响;死亡加分逻辑保持不变。
怎样检查
make test核对09;未命中不响;多血怪非致命命中也响,每次有效命中一次;游戏试听并观察得分时机(练习模式不刷怪,切到战斗模式按TAB,等多血怪出现再试听)。
修改位置
src/main.cpp → HandleHits
TODO(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,分别运行保存的主线和挑战版本,说明各自结果;挑战尚未完成,也可以带着问题来讨论。

下节课:演示你的实现,讲清你的判断

  1. 展示选择与预测。指出主线还需补哪项、选了A还是B、是否挑战C,展示运行前写下的尺寸、坐标或四角预测。
  2. 运行演示。先演示补完的主线。A展示原墙与出生安全、40/80像素门洞和直角拐弯,核对玩家/大体型矩形以及贴边/部分重叠两例;B展示固定文字,走过旧窗口对照格线、世界坐标、行列与target;C先展示保存的主线版本和挑战版本各在哪个目录或分支,再走到四角与世界边缘。
  3. 解释资料与实验。指出哪份资料、哪一句支持你的判断,展示一个最小实验,比较预测与实际。不要只说「代码能运行」。
  4. 拿出失败案例与改进。用「预期 / 实际 / 尝试 / 最小代码 / 推断」讲一次失败和改进;没有自然出现的失败,也可解释实验排除了哪个错误想法,不用故意改坏正确代码。

老师会分别记录主线待补项、所选扩展和挑战C的「通过 / 待补」与理由。扩展验收独立于九组自动成绩;记下哪些地方还需要实验或说明,作为下一步计划。