说实话,CS50这门课我前后跟了不下三轮,每一年第0讲和配套的Scratch练习都会重新看一遍。很多人一看Scratch就默认它是“给小朋友玩的积木工具”,直接跳过,但我个人强烈建议:如果想把计算机思维彻底打通,Scratch这一关必须好好过。2024版CS50在Scratch单元依然保持了相当大的比重,而这篇第六篇学习笔记,正好是整段内容里最容易拉开差距的分水岭——变量、条件判断、自定义积木、列表这些概念开始密集登场,课程从“照着示例搭积木”切换到“自己设计程序结构”。这篇笔记既适合正在跟CS50、想扎扎实实做作品的人,也适合少儿编程教学场景里备课到进阶阶段的老师,我会把这一课的核心概念拆开讲透,再带大家做一个可以直接复用的中秋主题小项目,最后把实验过程中踩过的坑整理成速查表。
1. 第六课到底在讲什么:从“搭积木”跃迁到“编程思维”
1.1 CS50为什么迟迟不放你走:Scratch背后的教学逻辑
CS50第一课用Scratch开场,不是因为它简单到可以糊弄两小时,而是因为哈佛教学组想让学生第一眼就看到“程序设计到底是什么”。如果一开始就上C语言的指针和内存,对零基础的人来说全是语法噪音,根本顾不上思考结构。Scratch把语法错误几乎清零,剩下的全是纯逻辑问题:先做什么、在什么条件下做什么、重复多少次、什么时候停止。
到第六课,这种优势开始转换成真正的门槛。前几课你可能还在拖“移动10步”“说话2秒”,到这一课开始要求你同时管理多个角色、多个变量、多条并行脚本。我自己跟学几轮之后才意识到,第六课真正想训练的不是“记住某个积木在哪”,而是抽象能力:能不能把一个复杂任务拆成模块,再把模块安排成可运行的剧本。
什么叫抽象?打个比方,给一个完全没下过厨的人一张复杂菜谱,他大概率手忙脚乱。但如果有经验的厨师会把菜谱拆成“备料、腌制、煎制、装盘”四步,每步再拆出具体操作,这就是模块化。Scratch第六课的练习目标,就是用事件、自定义积木、消息广播这些元素,帮你在图形界面里完成同样的拆解动作。CS50之所以“不放你走”,是因为这一课对后续C语言、Python的课程影响深远,后面所有函数思想、状态管理、事件驱动概念,都可以在Scratch里提前“预演”一遍。
1.2 第六课的新维度:模块化、事件驱动与“设计模式”启蒙
第六课相比前面几节课,最大的变化是出现了“程序结构”意识。前五课更多是单向脚本,一个精灵从开始到结束按顺序跑;第六课开始频繁出现多个角色同时响应事件、边界条件触发、并发逻辑。几个核心变化特别值得记在心里。
首先是事件驱动。Scratch里最常见的“当绿旗被点击”只是起点,真正让程序活起来的是“当收到消息”“当角色被点击”“当按下空格键”这类分散在各处的触发器。第六课的练习作品里,你几乎不可能用一条顺序脚本完成,必须让不同角色各自挂上事件。我上课给学生的比喻是:舞台上不是只有一个导演,而是每个演员都有自己的剧本,导演喊“action”之后大家按各自的剧本并行工作。
其次是广播消息。广播相当于角色之间的“暗号”,一个角色发出“开始下月饼”,另一个角色收到消息后才启动生成逻辑。很多初学者会反问:为什么不直接让月饼脚本跟着绿旗一起启动?答案是解耦。按下开始键时,篮子、月饼生成器、计分器都要分别启动,如果全部绑在一个事件里,后续想单独暂停某个环节就很麻烦。广播把“什么时候运行”和“运行什么”分开了,这其实是“事件发布与订阅”思想的最朴素版本,后面到JavaScript或者Python GUI编程还会遇到一模一样的模式。
最后是状态管理。复杂一点的Scratch作品都会有“游戏开始”“游戏中”“游戏结束”这些状态,第六课的半命题作业会要求你在同一个作品里管理这些状态切换。最简单的办法是设一个全局变量,比如“游戏状态=0/1/2”,条件分支根据状态决定是否响应键盘、是否继续生成敌人、是否停止计分。别小看这个操作,这是你在图形化编程里第一次主动使用“状态机”思想,后面写任何复杂项目都用得上。
2. 绕不开的三大基础元件:变量、条件判断与循环的组合
2.1 变量:全局与局部、命名与初始化的实际影响
Scratch里的变量分为“适用于所有角色”的全局变量和“仅适用于当前角色”的局部变量。这个区分看着简单,实际编程时坑不少。我在做练习时遇到的最经典问题就是:两个角色都用了同名变量“速度”,以为是分开的,结果因为其中一个变量是全局,另一个角色的脚本无意中把它改了,整个游戏节奏突然失控。后来我给自己定了两条硬规矩:全局变量统一加前缀(比如“全局_得分”),并且只允许一个角色去写它;角色内部的临时变量一律勾选“仅适用于当前角色”。
命名也要讲究。变量叫“a”还是叫“得分”对程序运行没有区别,但对调试体验有天壤之别。我见过很多学生的作品里变量叫“x1”“x2”,三十分钟后再看脚本,完全忘了x1是速度还是分数。CS50课堂上老师反复强调代码的可读性,在Scratch里也一样:变量名至少让人一眼看出用途,比如“月饼速度”“生命值”“连续接住次数”。
初始化是另一个被低估的大坑。舞台启动那一刻,所有变量如果不是在“当绿旗被点击”的初始化脚本里主动赋值,就会延续上一次运行留下的旧值。我踩过一次特别典型的:游戏结束时把生命值改成0,第二次重新开始忘了初始化,生命值依然显示0,新玩家一点开局就输。正确做法是,把初始化操作集中放在舞台或者出场角色的一张脚本里,一开跑就全部重置,不要分散到各个角色的各自事件中。
2.2 条件判断:边界、顺序和逻辑运算
条件判断在Scratch里就是“如果/那么”和“如果/那么/否则”,别看积木简单,真正让它有力量的是里面的比较运算和逻辑运算。第六课的项目里,判断月饼是否被接住、是否掉出屏幕、游戏是否结束,全部靠条件堆出来。最容易翻车的是边界条件,也就是“等于某个值”或者“在两个数之间”的判断。
举个例子,我做的接月饼游戏里,判断月饼落进篮子篮口,不能用“如果 碰到篮子”这么粗糙,因为月饼那么多,碰到就加分会导致一个月饼连续加分好几轮。正确的做法是:检测碰到的同时,给月饼设置一个“已被接住”的标记,然后立刻把它变成不可见并删除克隆体,保证同一块月饼只触发一次得分。很多人做游戏卡在分数乱跳上,核心原因就是“事件触发后缺少一次性保护”。
条件判断的顺序也值得专门讲。多个条件里,优先级最高的要先判断。我习惯的顺序是:先检测是否跌出边界(游戏结束的边界状态),再检测是否接到(得分状态),最后检测是否还在空中(继续下落状态)。如果顺序反了,可能月饼已经掉出屏幕,还被“碰到篮子”判断拦住了。实际调试时,建议把边界条件的数值稍微放宽。比如屏幕底部判断不是“y坐标 < -180”就立即结束,而是先让它隐藏、再等0.5秒再结算,这样视觉上不会突兀。
逻辑运算(并且、或者、不成立)是第六课真正需要掌握的另一个点。比如游戏快到结束时想增加难度:当“生命值 = 1”并且“月饼速度 > 3”,这时速度再增加一个档位。这种复合条件如果用嵌套的“如果”写,脚本会很长;用“并且”合并成一个条件,逻辑清楚又好维护。
2.3 循环嵌套与游戏主循环:别把CPU跑炸了
Scratch提供了三种循环:重复执行、重复执行N次、重复执行直到。第六课最常用的其实是“重复执行直到”,因为它能表达“在条件满足前一直做某事”的语义。但很多人一看需要持续检测条件,就直接上“重复执行”,结果整个程序每秒跑几百次判断,风扇狂转。这里有个经验:如果循环体里没有明显的等待积木,系统会以极快的速度空转。我之前做过一个测试,让一个角色在重复执行里不断判断一个变量,不加上任何“等待”,Scratch的运行速度肉眼可见变卡。
解决方案不是不用循环,而是给循环一个节奏。常用做法:循环体末尾加“等待0.05秒”,这样每秒最多执行20次,对人眼、对CPU都友好。但注意,如果是要追求动画流畅的移动脚本,每帧等待也不能太长,0.01秒~0.03秒之间比较合适,再长了就能看到明显卡顿。
游戏主循环是第六课进阶作品的典型结构:一个永远执行的循环体里,依次完成“检测输入、更新状态、绘制界面”三件事。用Scratch实现时不需要那么严格,但基本的套路可以拆成三个独立脚本并行:一个脚本监听键盘改变移动方向,一个脚本更新所有的月饼坐标和得分,一个脚本检查游戏结束条件。并行脚本并行跑,反而比把所有事情塞进一个大循环更清晰,这也是事件驱动编程的优势。
3. 自定义积木与列表:Scratch里的函数与数据结构
3.1 自定义积木=函数:参数、局部变量与“返回值”的实现
第六课最大的一个认知升级,是理解“自定义积木”就是函数。Scratch的“制作新的积木”,对应到Python就是def,对应到C就是函数定义。你可以给它起名、传入参数,然后整个程序里反复调用,不用复制粘贴一大堆脚本。
自定义积木最实用的一点在于它自带局部变量空位。点击积木定义区域的“选项”,可以勾选“运行时不刷新屏幕”,还能添加数字参数和文本参数。比如我定义了一个“月饼下落(速度)”积木,它接受一个“速度”参数,那么每次通过这个参数传入不同的下落速度,就能生成难度不同的月饼。参数在这里的角色,就是函数调用时传入的值,值不同,行为就不同。
关于“返回值”,Scratch的自定义积木默认不直接返回值,这一点和真正的函数不太一样。怎么解决?我的惯用技巧是借助全局变量:自定义积木负责计算,把结果存入一个变量,调用处马上读取这个变量。举个例子,我想判断两块月饼是否会碰撞,写一个自定义积木“计算距离”,它算出距离后写入“现距离”这个变量,主脚本在调用完积木后立刻检查“现距离”。这个方法不优雅,但很可靠。Scratch 3.0后续版本里有“用于所有浮动的块”,这个需要安装扩展模块才能实现类似返回值的效果,如果不想折腾,全局变量方案完全够用。
封装的好处体现在维护上。以前改下落速度,得去每个克隆体的每个脚本里找,现在只要改自定义积木定义处的参数逻辑,所有调用点全部生效。我上第六课的时候一定要求学生:凡是重复出现过3次以上的脚本片段,统统封装成自定义积木。这一条做到位,后面维护大项目会轻松非常多。
3.2 列表的实操场景:排行榜、抽屉与随机抽取
列表在Scratch中的角色相当于高级语言里的数组。第六课开始引入列表,是因为项目复杂度上来了,单靠变量已经装不下多个数据。想想看:一个游戏要记录玩家前五轮的总分,你用五个变量还算能忍;如果要做本地排行榜,记录前十名玩家和分数,再用变量就完全失控了。列表的价值,就是在一个有序容器里存一片数据。
列表的基础操作看起来简单,无非是添加、删除、替换、读取第几项,但真正的实操难点在于“在什么时候修改列表”。我做过一个“月度累计得分”功能,本来想每接住一个月饼就往列表尾部加一条记录,结果因为循环跑得太快,一次点击记录了十几条。后来改为用一个“是否已计分”的标记配合事件触发,确保一次接住只写一条。
列表还有几个经典应用很值得在第六课练一遍。
第一个是随机抽取。把一堆奖励写在列表里,用“列表[随机数1~项数]”取值,实现随机奖励池,这比用一堆单独变量去做随机分支要优雅得多。
第二个是滑动窗口。记录玩家最近十次接月饼的得分,每次新得分加入列表尾部,同时删除第一项,这样就能算出“近十轮平均分”。这个模式在真实数据分析里也很常见,相当于一个环形缓冲区。
第三个是排行榜的插入位置。用循环遍历列表,找到第一个比当前分数小的位置,把新分数插进去,然后把多余的项删除,保持列表只保留十项。这一段逻辑在Scratch里要嵌套好几个判断才能写完,但写完一次以后,你会对“排序”这个概念有非常直观的感觉。我带着学生走完这套流程后,再回到C语言的排序章节,几乎所有人反应都快了很多。
4. 第六课实操项目:中秋节接月饼小游戏的完整落地
4.1 需求拆解与变量设计
光讲概念不落地是学不会编程的。CS50课程的作业也强调:Scratch部分必须自己做一个能运行的作品。恰好赶上中秋节,我就把第六课的半命题作业做成了“中秋接月饼”小游戏。玩法很简单:天上不断落下月饼,玩家用篮子左右移动去接,接到加10分,漏掉扣1点生命,生命归零游戏结束。
先讲需求拆解。我拿到项目第一件事,不是动手拖积木,而是先列功能清单:
- 背景与角色:月亮背景、兔子角色(固定)、月饼角色(克隆生成)、篮子角色(玩家控制)
- 变量设计:全局得分、全局生命值、当前月饼速度、连续接住次数、游戏状态
- 事件流程:绿旗点击进入开始界面,按下空格游戏开始,生命归零显示结束界面
- 碰撞规则:篮子颜色碰到月饼色块判定为接到,接到后得分并删除该克隆体
- 难度递增:每接住一个月饼,速度增加0.05;速度越快生成间隔越短
变量是整个设计的中心。我特意把“当前月饼速度”设成全局变量,因为生成器要根据当前速度刷新克隆体,“下落”脚本也要读取这个速度,如果设成角色私有,两边数据就同步不了。反过来,“连续接住次数”这种只跟得分逻辑相关的量,我就做成篮子角色的私有变量,减少全局命名空间污染。
4.2 月饼生成、碰撞检测与计分逻辑
月饼生成用的是克隆,这一步是第六课实操含金量最高的部分。生成器角色的脚本框架如下:
当绿灯亮起(绿旗被点击) 重复执行直到 <游戏状态 = 结束> 等待 (0.5 - 得分/1000) 秒 创建自身克隆体 拾取随机x坐标
这样一个简单的循环,配合克隆体启动脚本,就能实现连续不断的月饼雨。但有个性能常识:Scratch的克隆体上限是300个,如果一个克隆体掉出屏幕后只是隐藏却不删除自己,它会一直占用资源。我的克隆体脚本里有一条:
如果 <y坐标 < -190> 隐藏 删除此克隆体
删除自己这个操作,能保证掉出屏幕的月饼立刻释放资源,这是很多新手第一次做克隆作品时完全想不到的关键点。
碰撞检测可以有两个路线。第一个路线是让月饼的克隆体每隔一小段检测“碰到篮子”,第二个路线是让篮子在移动时检测“碰到月饼”。我实测下来,前者的检测频率更容易控制,因为月饼只有一个角色,它会一直检测自己是不是被接住,如果被接住就先加分再删除自己。分数不能直接加10分,因为同一块月饼在碰到篮子那帧之后,如果循环还在跑,它可能会被判定第二次。我用一个“已接住”的私有变量,判断一旦为真,随即跳过之后的碰撞检测脚本。
计分逻辑的细节值得展开。基础分10分,连续接住会有连击奖励:今天的床上连续接住第1个是10分,第2个是12分,第3个是14分,依次递增,每次漏掉清零连击,这个规则用文本描述是三四行,转换成Scratch就是:
如果 <碰到篮子> 将 连续接住次数 增加 1 将 得分 增加 (10 + 连续接住次数 * 2) 将 月饼速度 增加 0.05 删除此克隆体
连击逻辑会推动玩家追求操作稳定性,也让游戏画面更有情绪反馈。速度递增的幅度不要太大,我一开始设成每次增加0.2,没想到接到第三个就快得没法玩了,调试之后改成0.05,难度曲线才变得正常。这个参数调整,没有任何文档会告诉你,全靠现场试出来的手感。
4.3 我踩过的坑与优化记录
这个作品我前后改了三版,三版里各有代表性的坑。
第一版的问题是月饼下落脚本没有用局部变量,所有克隆体共用同一个速度变量。结果是:第一个克隆体创建后拉着速度变量从2变成2.05,第二个克隆体启动时读到的已经是2.05,等第一个克隆体还没掉落一半,速度又被后续克隆体改大了。画面里所有月饼几乎以差不多的速度下落,完全体现不出“新生成的更快”这个规律。解决方法是把每个克隆体的下落速度作为一个私有变量,在克隆体启动时从全局变量读取一次,之后各自独立。
第二版的问题出在游戏结束判断。我把“生命值=0”的判断放在月饼生成器的循环里,没想到生成器循环里有一个“等待”积木,导致游戏真正结束后还会多看几次循环,偶尔出现“结束画面闪了一下又回到游戏”的怪象。后来我在每个关键流程入口加了“游戏状态”判断:生成器只会在“游戏中”状态下继续生成,结束脚本一旦置状态,所有并行循环立刻退出,效果稳定许多。
第三版的是素材处理问题。中秋主题本来就该明亮温暖,我一开始选了一张星空背景素材,篮子用了深红色,结果红色篮子和深色背景混在一起,玩家总是看不清楚,误判碰撞位置。换成高饱和橙色篮子加白色背景后,游戏体验一下子上来了。重要经验:游戏元素的可视性优先级高于视觉华丽度,小孩子玩游戏第一反应是看到目标,而不是欣赏美术。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
我在带着不同学生做CS50第六课作品和少儿编程中级项目时,反复遇到下面这些相同的问题,这里直接整理成一张速查表,方便大家按图索骥。
| 现象 | 真正的原因 | 排查思路 |
|---|---|---|
| 克隆体出生后不动 | 克隆体启动脚本没有被触发 | 检查是否在“当作为克隆体启动时”里写了移动逻辑,而不是普通绿旗里 |
| 同一块月饼多次加分 | 碰撞检测没有一次性保护 | 加“已接住”判断,接住后立刻删除克隆体 |
| 游戏结束画面闪回 | 并行循环未检查游戏状态 | 在每个循环入口加“游戏状态”判断,结束时就跳出循环 |
| 得分变量数值巨大 | 循环重复执行同一个加分操作 | 加分操作应该绑定事件而非循环扫描,或者用等待+标记限制频率 |
| 角色被挡住看不见 | 图层层级设置问题 | 用“移到最前面/移到最后面”调整角色层序 |
| 第二次运行时数据没重置 | 初始化没有放在统一入口 | 把变量初始化集中到绿旗脚本,覆盖延迟或被跳过的情况 |
| 程序跑一段时间明显卡顿 | 克隆体只隐藏没删除 | 所有出界/用过的克隆体都要执行“删除此克隆体” |
| 按键移动方向反了 | 坐标系理解错误 | Scratch里y坐标向上为正,向右移动对应x坐标增加,捋清后再写 |
| “说/思考”气泡永远不消失 | 没有设置持续时间 | 在“说”积木的第二个下拉参数里填入秒数,不填会一直显示 |
这张表我建议直接保存。我自己在辅导过程中发现,初学者80%的报错都能归到这九类里面,处理完这些,剩下的才是真正的算法问题。
5.2 独家调试思路:让游戏告诉你问题在哪
Scratch没有单步断点,也没有print控制台输出,但我摸索出了一套很实用的排查路子。
第一个技巧,善用“说”积木当调试输出。想知道某段脚本有没有执行,就在里面加一句“说 调试信息2秒”,如果画面里角色说出这句,说明这段脚本确实跑了。比看代码猜测高效得多。这个招数用到真正编程语言里,就是日志打印,趁早在Scratch养成习惯很有好处。
第二个技巧,打开舞台“监视器”。在舞台左侧方块里有个“监视器”选项,可以显示所有变量的实时数值。我调试月饼速度递增时,就盯着监视器看每秒数字变化,很快发现问题确实出在克隆体共享变量上。建议编程时把监视器固定在桌面角落,养成实时观察数据的好习惯。
第三个技巧,让单个角色单独测试。程序崩的时候不是所有角色都得跑,完全可以把不相关角色的事件块拖到一边,只保留正在排查的那个角色,配合空格事件逐步触发。这种“可控重现”是缩小问题范围最快的路径,很多复杂问题其实都是多个角色脚本相互干扰导致的。
第四个技巧,每改一个逻辑就另存为一个版本文件。我原以为多存文件是浪费时间,直到某次大改把整个下落的物理手感改坏了,然后想回退到前一天版本,才发现自己从没存档。现在我的项目文件夹里都是“接月饼_v1”“接月饼_v2”这种命名,永远可以随时回到上一个稳定状态,这相当于给项目管理建立了版本控制意识。
最后再说一个很多人忽略的点:Scratch的在线编辑器会自动保存,但如果用的是“Scratch桌面版”,离线编辑时一定要手动Ctrl+S。我见过辛苦两小时的作品因为断电直接丢了大半,那种心痛经历一次就够。备份意识,在编程的任何一个阶段都不过时。
我自己在反复试验中最大的体会是,第六课真正卡住大家的不是某个具体积木不会用,而是心态上还没有从“照着做”切换成“自己规划”。一旦你把“变量是数据、自定义积木是函数、列表是数组、广播是事件通知”这层对应关系想明白,Scratch就不再是玩具,而是你通往C语言和Python的一座稳定桥梁。做完接月饼这个项目,后面再去挑战《Scratch植物大战僵尸》或者任何带实时对战逻辑的作品,就会发现骨架子都差不多。如果后续还有时间,我会把同一套项目对比着移植到Python的pygame版本里,那才是真正检验你理解深度的一步。