从山地足球到卡通打靶:Scratch 3.0物理与克隆体实战
上周带几个小朋友做项目,有个家长凑过来看了一眼屏幕问我:“这球怎么还会在坡上弹?”我说这不是画出来的动画,是算出来的。我们做的是 Scratch 3.0 里的两个小 Demo,一个叫“山地足球”,一个叫卡通射击打靶。这两个东西单看都不复杂,但放在一起能做到一件事:把“物理”和“随机”这两类最难讲清楚的概念,变成孩子能自己拧的旋钮。山地足球里有重力、有速度、有斜坡法线、有滚动摩擦,这些在课本上是一堆公式,在 Scratch 里就是几个变量和一行“把 y速度 增加 -0.6”。打靶射击里则全是克隆体、命中判定、生命周期管理——这是 Scratch 里最容易做崩、也最能练出工程习惯的部分。这篇文章写给谁看?写给带娃做过 Scr atch、但一做到“球落地弹起来”就卡住的家长;写给想把课程从“走迷宫”升级到“做游戏”的老师;也写给刚学完基础积木、想认真做一个能玩超过三分钟的作品的学生。我不讲“积木在哪一栏”,只讲为什么这么搭、参数怎么定、坑在哪。代码我会贴关键片段,但真正的价值在后面几节的调参经验和排查清单——这些是文档里不会写的。
1. 先把这两个项目在教什么想明白
1.1 为什么把“山地足球”和“打靶射击”配成一对
很多人看到“01”这个编号,会以为这是一篇合集,其实是刻意配对的。山地足球解决的是连续变化的问题:球的位置、速度、角度每一帧都在变,变量之间互相影响,孩子必须理解“先算速度,再用速度改位置”这个顺序,否则球会抽搐。打靶射击解决的是离散事件的问题:什么时候生成一个靶子、什么时候销毁一颗弹丸、命中之后谁来加分,这些是“有没有”而不是“多少”。
这两类问题的思维方式完全不同,但它们在真实项目里必须共存。你在山地足球里进球的那一瞬间,要触发音效、要加分、要重置球的位置——这就是一个离散事件被连续运动触发的场景。所以我把两个 Demo 放进同一节课,不是为了凑内容量,是为了让孩子在一个作品里同时练两种肌肉。实操下来,只做过连续运动的同学,写计分逻辑时经常忘记“进球后要把球放回中场”;只做过打靶的同学,写球的运动时会把“位置”和“速度”搞混。两边一起做,反而互相提醒。
还有一个现实考量:山地足球的成品率低。物理调不好,球会陷进地面、会穿过墙壁、会原地抖动,孩子容易挫败。打靶射击的成品率高,十几分钟就能出效果,能立刻给正反馈。我把打靶放在后半段,就是让它当“情绪缓冲”——前面啃物理啃累了,后半段爽一下,顺便把克隆体这类高级积木学了。
1.2 角色、背景和变量,先把骨架摆出来
动手之前先画一张表,这件事我强烈建议带着孩子一起做。不做规划的后果是:做到一半发现要加一个“球出界”的判断,结果没有变量可用,于是到处补补丁。
| 类别 | 名称 | 类型 | 职责 |
|---|---|---|---|
| 角色 | 球员 | 普通角色 | 左右移动、跳跃、踢球 |
| 角色 | 足球 | 普通角色 | 受重力、受碰撞、滚动 |
| 角色 | 球门左/球门右 | 普通角色 | 进球判定 |
| 角色 | 地形 | 克隆体角色 | 提供可检测的地面 |
| 角色 | 发射器 | 普通角色 | 固定在底部,提供出生点 |
| 角色 | 准星 | 普通角色 | 跟随鼠标,提供方向 |
| 角色 | 弹丸 | 克隆体角色 | 短生命周期高速移动 |
| 角色 | 靶子 | 克隆体角色 | 从上方飘落,被命中即销毁 |
| 变量 | x速度 / y速度 | 仅适用于足球 | 球的速度分量 |
| 变量 | 重力 | 所有角色 | 统一的重力常数 |
| 变量 | 进球数 / 分数 | 所有角色 | 计分 |
| 变量 | 连击 | 所有角色 | 连续命中计数 |
| 变量 | 游戏状态 | 所有角色 | 菜单/足球/射击/结束 |
注意“仅适用于当前角色”和“适用于所有角色”的区别,这是 Scratch 3.0 里最容易被忽略的一个功能。球的速度变量一定要设成“仅适用于足球”,否则两个角色写同名的变量会互相覆盖,症状是:打完靶回到足球,球一出来就飞出去。这个坑我见过太多次。
1.3 用一个变量把两关串起来
“游戏状态”这个变量值我一般就四个:菜单、足球、射击、结束。所有角色在“当绿旗被点击”之后,第一件事不是开始动,而是先判断状态。足球角色的脚本开头长这样:
当绿旗被点击 把 [游戏状态] 设为 [菜单] 隐藏 重复执行 如果 <(游戏状态) = [足球]> 那么 显示 否则 隐藏这样做的好处是,两个玩法各自的初始化脚本可以完整保留,切换的时候不会互相污染。我试过用“广播”来切换、不用变量,也能跑,但广播是“一次性”的,角色如果在这一瞬间正在执行一个长循环(比如球在飞行中),就收不到。变量是“持续”的状态,随时可查,更稳。
还有一个细节:切换状态的时候要发一个“清理广播”。足球这边切走,要把球的速度清零、位置归位;射击这边切走,要让所有靶子克隆体和弹丸克隆体删除自己。清理不做干净,下次进来就会看到上一局残留的靶子在空中飘,孩子会以为程序坏了。
2. 山地足球:把“斜坡”这件事拆成三块
2.1 地形怎么做,才能既好看又好判定
这是整个项目第一个分水岭。我见过三种做法,各有适用场景。
第一种,造型拼接。做三到四个山坡造型,缓坡、陡坡、平台、凹坑,用“移到 x y”摆成一条地面线。判定用“碰到颜色 深绿色”或者“碰到 [地形]”。优点是纯积木拖拽,一年级也能做;缺点是坡度是“硬编码”的,球碰到哪个坡,你不知道该按哪个角度反弹,除非把每个坡做成独立角色,一个一个去判断“碰到 缓坡”“碰到 陡坡”,角色一多脚本就长得没法看。
第二种,画笔生成。用一个自定义积木,从 x=-240 画到 x=240,y 用几个正弦叠加算出来,先画一条深绿色的地面线,再落笔填到底部。这个做法的地面是连续的,看着舒服,但 Scratch 的画笔绘制结果不能直接读坐标,判定还是得靠“碰到颜色”。而“碰到颜色”是逐个像素扫描的,克隆体一多就卡。
第三种,列表查表。画面上照样用画笔或造型,但同时在后台生成一张“地形高度表”——一个长度 241 的列表,第 i 项对应 x = -240 + (i-1)×2 那一列的地面高度。球要判断自己有没有落地,不用去扫描像素,直接查表。这个方法最快,也最准,是我最终采用的方案。
为什么选它?因为查表是 O(1) 的,不管屏幕上有多少个克隆体,查询成本都一样。而碰撞检测的成本跟造型复杂度、克隆体数量成正比。做过一个对比测试:同样 20 个克隆体,用“碰到颜色”那一版在低配笔记本上掉到 22 帧左右,换成查表之后稳定 30 帧。
生成地形表的脚本大概这样:
当绿旗被点击 删除 [地形表 v] 的全部项目 把 [i] 设为 0 重复执行 241 次 把 [x] 设为 (-240 + (i * 2)) 把 [h] 设为 ((30 * [sin v] of (x / 40)) + (18 * [sin v] of ((x / 13) + 20)) - 60) 将 (h) 加入 [地形表 v] 把 [i] 增加 1这里的 30 和 18 是两个波形的振幅,40 和 13 是波长,-60 是整体下移,让地形落在屏幕下半部分。这三个参数你可以随便改,改完地面形状立刻不一样。我一般让孩子先随便拧,看到地面变形了,再告诉他们“这就是参数化”。
2.2 重力、速度和反弹:三个数字定生死
很多教程写“把 y速度 增加 -1”,然后就没有然后了。但 -1 这个数字背后是有依据的。
Scratch 舞台高 360 像素,y 从 -180 到 180。球被我做成直径 24 像素,半径 12。我希望球被踢一下能跳到大约 100 像素高,然后在 1 秒左右落回地面。
按匀加速运动的公式,跳跃高度 h = v₀² / (2g)。假设重力 g = 0.6 像素/帧²,那 v₀² = 2 × 0.6 × 100 = 120,v₀ ≈ 11。所以踢球的垂直初速度设 11 是对的。
再看滞空时间。从起跳到落地的时间 t = 2v₀ / g = 22 / 0.6 ≈ 37 帧。Scratch 默认 30 帧/秒,也就是 1.2 秒左右。这个时长我觉得刚好——太短了看不清轨迹,太长了等得难受。
水平方向我设初速度 10 像素/帧,飞 37 帧,理论水平距离 370 像素,但因为有空气阻力(每帧乘 0.99),实际大概 310 像素。舞台宽 480,刚好能从中间飞到左侧球门附近。这个“刚好够到球门”的感觉是调出来的,第一次我设成 6,球飞一半就掉了,孩子踢十次进不了一次,很快就失去兴趣。
关键在一个顺序问题:先更新速度,再用新速度更新位置。
把 [y速度] 增加 (重力) // 先算:速度被重力改变 把 y 增加 (y速度) // 后动:位置被新速度改变如果反过来,先移动再加速,球在最高点会“愣”一下,视觉上能看出来。这是数值积分里最基础的一阶欧拉法,小学生不用知道名字,但顺序必须记牢。我用一个类比讲:速度是你的车速,位置是你在哪。你先踩刹车(速度变了),车才慢慢停下(位置变了)。如果先算位置再踩刹车,那这一帧你还是全速冲过去的。
在 Scratch 里还有一个隐藏坑:如果你用了“移动 10 步”这种积木,方向是角色朝向。足球这个角色的朝向如果被踢球脚本改过,重力方向就乱套了。所以球的移动必须用把 x 增加和把 y 增加,绝对不能用“移动”。这一点看起来是常识,但每年都有孩子掉进去。
2.3 让球真的“滚”起来,而不只是“滑”
球贴着地面滑行,和球在滚动,观感差得很远。滚动其实就是一个换算:球滚过的弧长等于球心移动的水平距离,转角 = 位移 / 半径,再转成度数。
球的半径 12,周长 = 2π×12 ≈ 75。滚 75 像素转一整圈 360°。所以每移动 1 像素,转 360/75 ≈ 4.8°。脚本就一行:
右转 ((x速度) * (4.8)) 度这里要注意方向。球向右滚(x速度为正在),屏幕上应该顺时针转,Scratch 的“右转”是顺时针,所以直接乘正数就对了。如果球向左滚,x速度为负,“右转”负数就等于“左转”,自动正确。这个写法不需要判断符号,很省事。
但还有个细节:只在球落地滚动的状态下才转,在空中飞的阶段不该转(或者说转得慢一点也行)。我一开始没加判断,球在空中也疯狂旋转,看着像陀螺。后来加了一个“如果碰到地面就转”,观感立刻正常。
还有一个更隐蔽的问题:旋转中心。造型的旋转中心必须设在球心。如果是默认的“造型中心”,有时候因为造型不对称,球转起来会像在绕着一个看不见的轴晃动。改法是在造型编辑器里点“设置旋转中心”,把十字拖到球的正中间。这个操作很多孩子不知道,做出来的球转起来一抖一抖的,找半天找不到原因。
2.4 踢球判定和球员操作的手感
球员的操作我做成三段式:左右方向键改 x,上键跳跃,空格踢球。这里有几个取舍要说。
移动用“当按下按键”还是“重复执行里判断按键”?我坚持后者。用“当按下右移键”这种事件触发的写法,按键的重复频率取决于系统的按键重复率,而且松开键之后角色会“滑”一段。正确写法是在主循环里判断:
重复执行 如果 <按下 [右移 v] 键?> 那么 把 x 增加 6 如果 <按下 [左移 v] 键?> 那么 把 x 增加 -6这样每一帧的位移是确定的,速度稳定。6 这个值也是调出来的:太小走得像蜗牛,太大走过头。舞台宽 480,从中间走到边缘 240 像素,6 像素/帧大约 40 帧,1.3 秒走完半个屏幕,这个速度是有“操作感”的。
跳跃检测用 y 坐标还是用地面高度?简单版直接判断y < -110就认为在地面。但如果地形是起伏的,这个判断在坡上就失效了——你站在高处,y 还是 -60,程序认为你在空中,于是按上键不跳。所以必须用查表得到当前 x 的地面高度:
把 [地面高度] 设为 (第 (四舍五入 (((x位置) + 240) / 2) + 1) 项) 如果 <((y位置) - (地面高度)) < 3> 那么 把 [球员y速度] 设为 11这段“索引换算”是整个项目里唯一需要动点脑筋的数学:列表的索引从 1 开始,x 从 -240 开始,步长 2,所以 x 对应的索引是 (x+240)/2 + 1。你可以直接告诉孩子这个公式,也可以让他们自己推一遍——从 x=-240 开始数是第 1 个,x=-238 是第 2 个,这个推理对孩子来说并不难。
踢球判定。球员碰到球、并且按下空格,就给球一个速度:
如果 <<碰到 [足球 v]?> 与 <按下 [空格 v] 键?>> 那么 把 [x速度] 设为 ((10) * (朝向)) 把 [y速度] 设为 9朝向变量存的是球员面朝左还是右(1 或 -1),这样踢球有方向。这里我做了一个小优化:加一个 0.15 秒的冷却计时器,否则按住空格不松手,球会被连续“踢”,速度叠得离谱。冷却用“计时器”积木最简单:
如果 <(计时器) > (上次踢球时间 + 0.15)> 那么 把 [上次踢球时间] 设为 (计时器) ... 给速度 ...计时器这个积木很好用,它从绿旗开始计时,精度够、不占变量面板。缺点是它不会自动重置,所以切场景的时候要记得“计时器归零”,否则切回来冷却时间对不上。
3. 打靶射击:瞄准、克隆体与命中
3.1 准星跟随和发射方向,避开 atan 的坑
先说明一下玩法:屏幕底部中间有个发射装置,手里拿的是水弹发射器(纯卡通),上方飘着圆环靶和气球,鼠标移动控制准星,按下鼠标发射一颗水弹,打中靶子加分。整个画面是明快的卡通风格,没有真实武器形象,这点在做课程的时候要把握住,孩子做出来的东西是要给家长看的。
准星最简单——移到 [鼠标指针 v],一行搞定。稍微讲究一点可以加个“平滑跟随”,让准星慢慢追上鼠标,手感更柔和:
重复执行 移到 x: ((x位置) + (((鼠标的 x 坐标) - (x位置)) * 0.3)) y: ((y位置) + (((鼠标的 y 坐标) - (y位置)) * 0.3))0.3 是插值系数,越小越“粘”,越大越跟手。0.3 我觉得是比较合适的,既有阻尼感又不会觉得迟钝。
发射方向的计算是重点。有两种写法,我推荐第一种。
写法一,直接用“面向 [准星]”积木:
当作为克隆体启动时 显示 面向 [准星 v] 重复执行 25 次 移动 18 步 ...这个积木会让角色朝向另一个角色,简洁,孩子一拖就会。但有个限制:如果准星有克隆体,它只认原角色(隐藏的那个)的位置。好在我们准星只有一个,没有克隆,所以没问题。
写法二,自己算角度。这里就有个大坑了。Scratch 的[atan v] of (x)积木,返回的是 arctan,值域只有 -90° 到 90°,也就是它分不清第一象限和第三象限。如果你直接写:
面向 (([atan v] of (((准星y) - (y位置)) / ((准星x) - (x位置)))) - 90) 方向那么当准星在发射器左上方时,算出来的角度是错的,弹丸会朝右上方飞。正确做法要根据“准星x 是否小于 发射器x”补一个 180° 的修正。另外 Scratch 的方向约定是 0 朝上、90 朝右,跟数学上的 0 朝右差了 90°,也要减掉。这两件事叠在一起,很多成年人第一次写也会错。
我的建议是:先用“面向”积木跑通,后面如果要做“发射器旋转跟随”的效果,再去推公式。教学上先出结果,再讲原理,孩子的接受度会高很多。
3.2 克隆体的生与死,这里最容易做崩
弹丸和靶子都用克隆体。Scratch 有一个硬上限:一个角色最多同时存在 300 个克隆体。听起来很多,但你按住鼠标不松手试试,几秒钟就能撞到上限,症状是“打不出子弹了”,而且不会有任何报错提示,非常迷惑。
所以必须自己做配额管理。我用一个“当前弹丸数”变量:
当按下 [鼠标 v] 键? 如果 <(当前弹丸数) < (12)> 那么 把 [当前弹丸数] 增加 1 克隆 [自己 v]克隆体销毁之前把计数减回来:
当作为克隆体启动时 显示 重复执行 25 次 移动 18 步 如果 <<碰到 [边缘 v]?> 或 <碰到 [靶子 v]?>> 那么 删除此克隆体 重复执行 1 次 // 走完 25 次还没命中,超时销毁 删除此克隆体这里有个非常容易漏的点:“删除此克隆体”之后,后面的脚本就不执行了,所以你在删除之后写的“把当前弹丸数减少 1”根本不会跑。必须写在删除之前:
如果 <到达销毁条件> 那么 把 [当前弹丸数] 增加 -1 删除此克隆体这个坑我踩过,症状是打了几发之后就再也打不出子弹,变量只增不减,一直是 12。找了一下午。
同样的坑在靶子上也有。靶子的生命周期是这样的:
当作为克隆体启动时 显示 把 x 设为 (在 (-200) 到 (200) 之间取随机数) 把 y 设为 180 把 [下落速度] 设为 (在 (2) 到 (4) 之间取随机数) 重复执行 120 次 把 y 增加 (0 - (下落速度)) 如果 <<y位置) < (-170)> 那么 把 [连击] 设为 0 把 [靶子数] 增加 -1 删除此克隆体 把 [连击] 设为 0 把 [靶子数] 增加 -1 删除此克隆体这里用了“重复执行 120 次”作为超时保险,防止某个靶子因为坐标异常一直卡在屏幕上不消失。这种“双重保险”在克隆体项目里是必须的,因为你永远不知道哪个边界条件会让克隆体变成幽灵。
还有一个更隐蔽的问题:隐藏的克隆体不参与碰撞检测。如果你的靶子克隆体启动后忘了“显示”,它就在那儿飘着但你打不到。同理,子弹如果一直隐藏,也打不到东西。这个规则在 Scratch 里没有明说,但实测就是这样。
3.3 命中判定的三种写法,我为什么选“碰到”
Scratch 里做命中判定大概三条路。
第一种,碰到角色。子弹克隆体里写如果 <碰到 [靶子 v]?> 那么。这个积木检测的是子弹和靶子角色所有克隆体的像素级碰撞。像素级的意思是,造型里的透明区域不算,所以打靶子边上的空隙不会误判,这个体验是对的。而且它不需要额外的循环和列表,写起来最省事。
第二种,距离判定。理论上更可控,但 Scratch 里有个陷阱:(到 [靶子 v] 的距离)这个积木,对克隆体角色而言,取的是到原角色的位置距离,不是到各个克隆体。靶子原角色通常藏在屏幕外,所以这个距离永远是几百,判定永远不成立。要用距离判定,必须自己维护一个“靶子坐标列表”,用列表存每个克隆体的 x、y,然后子弹去遍历算平方和。这个做法当然能做,但对小学阶段的孩子来说太重了。
第三种,广播加坐标比对。靶子自己判断有没有被击中(比如用“碰到 弹丸”反过来写),命中后广播。逻辑上可行,但这样一来每个靶子克隆体每帧都要跑一次碰撞检测,8 个靶子就是 8 次,比子弹那边检测贵得多(子弹只有 12 颗,而且每颗很快就消失)。性能上不划算。
所以我的选择是:子弹那边检测,靶子被动等待。子弹数量少、生命周期短,检测成本低。这个“谁动谁检测”的原则在游戏开发里很通用,值得跟孩子讲一句。
命中之后,靶子怎么知道?用一个广播:
当接收到 [命中 v] 把 [分数] 增加 (1 + (连击)) 把 [连击] 增加 1 把 [靶子数] 增加 -1 删除此克隆体但广播是发给所有角色的,所有靶子都会收到,都会执行“删除此克隆体”——结果是一枪打掉全部靶子。这个 bug 非常经典。解决办法有两个:一是在广播里带上坐标信息(用列表传参),靶子收到后判断自己的坐标是否接近;二是干脆不用广播,让子弹自己去“通知”它碰到的那一个。
我实际用的是第一个思路的简化版:子弹命中后,把“命中点x”“命中点y”写入两个全局变量,然后广播。靶子收到广播后,判断自己离命中点够不够近(比如小于 30 像素),够近才销毁自己:
当接收到 [命中 v] 如果 <(((x位置) - (命中点x)) * ((x位置) - (命中点x)) + ((y位置) - (命中点y)) * ((y位置) - (命中点y))) < (900)> 那么 ... 销毁 ...900 是 30 的平方。用平方比较可以省掉开根号,虽然 Scratch 跑这点运算毫不在意,但这个写法是个好习惯。实测下来,这个方案稳定,不会误杀。
3.4 计分、连击和难度爬坡的设计
分数好做,加一就行。但“连击”这个东西能明显提升可玩性,而且实现起来特别简单:命中加 1,靶子掉出屏幕清零,得分时用1 + 连击而不是 1。孩子第一次看到连击数字跳起来的时候那种兴奋,比单纯加分强烈得多。
难点在“难度爬坡”。一款游戏如果从头到尾一个速度,玩两分钟就腻。我用的方案是按分数分段:
| 分数区间 | 靶子生成间隔 | 下落速度范围 | 同时存在上限 |
|---|---|---|---|
| 0 - 9 | 1.2 秒 | 2 - 3 | 4 |
| 10 - 24 | 0.9 秒 | 2.5 - 4 | 6 |
| 25 - 49 | 0.7 秒 | 3 - 5 | 7 |
| 50 以上 | 0.5 秒 | 4 - 6 | 8 |
这张表是根据试玩调出来的。第一版我把 50 分以上的生成间隔设成 0.3 秒,结果靶子多到屏幕糊住,根本没法瞄准,孩子直接放弃。0.5 秒是“忙但不慌”的临界点。
生成间隔用“等待”积木实现最简单,但要小心:如果你在“重复执行”里写“等待 1.2 秒 → 克隆自己”,那么整个循环的节奏就是 1.2 秒一次,改速度的时候只要改一个数。但如果你用计时器做精确控制,会更平滑。我一般先用“等待”,跑通再说。
4. 从空舞台到能玩,完整的五步走
4.1 第一步:素材、舞台和变量一次配齐
素材来源我一般给孩子三个选项:自己用内置画笔画(最锻炼造型能力)、用内置素材库里的球和圆环(最快)、从网上下载再导入(要注意版权,我一般要求注明来源)。山地足球的地形我用画笔程序生成,不依赖任何外部图片,好处是文件小、跨设备打开不会丢资源。
变量面板一次性建好,别边做边加:
x速度、y速度、重力、进球数 → 仅适用于足球 球员y速度、朝向 → 仅适用于球员 分数、连击、靶子数、弹丸数 → 适用于所有角色 命中点x、命中点y、游戏状态 → 适用于所有角色 地形表 → 列表,适用于所有角色顺序上,先做“足球”再做“射击”。理由前面说过:足球难,先啃;射击爽,收尾。
4.2 第二步:让球先动起来,不加任何地形
这一步的目标只有一个:球能跳、能落、能滚。地面就是一个固定高度 y = -120。
当绿旗被点击 把 [x速度] 设为 0 把 [y速度] 设为 0 把 [重力] 设为 0.6 重复执行 把 [y速度] 增加 (0 - (重力)) 把 y 增加 (y速度) 把 x 增加 (x速度) 如果 <(y位置) < (-120)> 那么 把 y 设为 -120 把 [y速度] 设为 ((0 - (y速度)) * (0.55)) 把 [x速度] 设为 ((x速度) * (0.98)) 如果 <(x位置) < (-230)> 那么 把 x 设为 -230 把 [x速度] 设为 ((0 - (x速度)) * (0.8))0.55 是反弹系数,0.98 是地面摩擦。这两个数可以让孩子自己试:把 0.55 改成 1,球会永远弹下去;改成 0.1,球像铅块一样砸下去不动。这个“拧参数看变化”的过程比讲十遍能量守恒都有用。我通常让他们把系数从 0 到 1 每隔 0.1 试一次,记录球弹跳的次数,画成一张表——这就是最原始的科学实验。
顺手把滚动加上:
如果 <(y位置) < (-118)> 那么 右转 ((x速度) * (4.8)) 度4.3 第三步:换成真实地形,调碰撞
把固定地面换成列表查表。核心就三件事:算当前地面高度、算左右两侧的坡度、按坡度反弹。
定义 取样 (sx) 如果 <(sx) < (-240)> 那么 把 [取样结果] 设为 (第 1 项) 否则 如果 <(sx) > (240)> 那么 把 [取样结果] 设为 (第 241 项) 否则 把 [索引] 设为 ((四舍五入 (((sx) + 240) / 2)) + 1) 把 [取样结果] 设为 (第 (索引) 项)注意 Scratch 的自定义积木不能“返回值”,只能把结果写进一个变量,这是它和普通编程语言最大的不同。孩子第一次接触这个概念会懵,我的解释是:“这个积木不会给你答案,它会帮你把答案写在黑板(变量)上,你再去黑板上看。”
反弹那段,我给你完整的:
把 [地面h] 设为 (取样 (x位置)) 把 [左h] 设为 (取样 ((x位置) - 6)) 把 [右h] 设为 (取样 ((x位置) + 6)) 把 [斜率] 设为 (((右h) - (左h)) / 12) 把 [角度] 设为 ([atan v] of (斜率)) 如果 <((y位置) - 12) < (地面h)> 那么 把 y 设为 ((地面h) + 12) 把 [vn] 设为 (((0 - (x速度)) * ([sin v] of (角度))) + ((y速度) * ([cos v] of (角度)))) 把 [vt] 设为 (((x速度) * ([cos v] of (角度))) + ((y速度) * ([sin v] of (角度)))) 把 [vn] 设为 ((vn) * (-0.55)) 把 [vt] 设为 ((vt) * 0.92)) 把 [x速度] 设为 (((0 - (vn)) * ([sin v] of (角度))) + ((vt) * ([cos v] of (角度)))) 把 [y速度] 设为 (((vn) * ([cos v] of (角度))) + ((vt) * ([sin v] of (角度))))这段是法线分解。原理不复杂:把速度拆成“垂直于坡面”和“沿着坡面”两个分量,垂直的反弹并衰减,沿坡面的保留并加一点摩擦,再合成回去。好处是球在陡坡上会明显地“往下溜”,在缓坡上会平平地弹出,视觉上很像真的。
但是说实话,对大部分孩子来说这段太难。我给的分层方案是:能理解的就上法线反弹;理解不了的用简化版——碰到地面就把 y速度 反向乘 0.6,同时判断如果坡度大于 20° 就把 x速度 也加上一个“往下坡方向”的加速度。简化版的效果打七折,但能跑,而且孩子知道自己在干什么。
调参的时候有个必试清单:把球放在最陡的坡上让它自由滚,看能不能滚到底;把球从最高处丢下去,看会不会陷进地里;让球高速撞墙,看会不会穿过去。这三个测试我叫它“三连测”,每次改完参数都要跑一遍。
4.4 第四步:接上射击模块的公共接口
两个玩法并存,最怕的是互相干扰。我定的规则是:所有跨模块的通信只走全局变量和广播,角色之间不直接读写对方的私有变量。
公共接口就四个:
| 接口 | 类型 | 谁写 | 谁读 |
|---|---|---|---|
| 游戏状态 | 全局变量 | 主控 / 按钮 | 所有角色 |
| 开始射击 | 广播 | 主控 | 发射器、靶子、计分板 |
| 结束射击 | 广播 | 主控 | 所有克隆体(清理用) |
| 返回菜单 | 广播 | 主控 | 所有角色 |
发射器的初始化脚本:
当接收到 [开始射击 v] 把 x 设为 0 把 y 设为 -150 显示 把 [分数] 设为 0 把 [连击] 设为 0 把 [靶子数] 设为 0 把 [弹丸数] 设为 0 计时器归零这里“计时器归零”很关键,因为靶子的生成节奏我用的是计时器控制。如果不归零,进入射击关的一瞬间计时器可能已经是 50 秒了,第一批靶子会瞬间刷出来一堆。
清理脚本同理,收到“结束射击”广播后,所有克隆体删除自己:
当接收到 [结束射击 v] 如果 <(是克隆体?) = [真]> 那么 删除此克隆体Scratch 没有“是克隆体”这个积木,怎么判断?一个常用的技巧是:克隆的时候给克隆体设一个私有变量“我是克隆体 = 1”,原角色是 0。或者更简单——原角色本身隐藏,克隆体启动时显示,那么“显示”这个动作只有克隆体做过。不过 Scratch 里没法查询“我是不是克隆体”。所以还是老老实实用变量标记。
4.5 第五步:联调和性能自检
联调阶段最容易暴露的问题不是逻辑错误,是帧率。Scratch 3.0 的帧率不是固定的,场景复杂会掉帧,而掉帧会让所有“每帧移动 N 步”的物理计算变得不准——原本 1 秒落地的球,掉帧之后要 1.5 秒。
自检的土办法:在舞台角落放一个“帧率计”,每帧把“帧计数”加 1,每秒算一次差。低于 25 就该优化了。我遇到的几个典型掉帧元凶:
- 地形用“碰到颜色”而不是查表,检测成本极高。
- 靶子克隆体上限设成 20,屏幕上同时存在十几个,每个都在跑碰撞。
- 用了“图章”每帧盖章画轨迹,图层越叠越厚。
- 背景音乐同时播放多个克隆体的音效,音频通道打满。
优化顺序我一般是:先砍克隆体数量,再换检测方式,最后才动画面效果。因为画面是给孩子看的,砍了会心疼;克隆体是看不见的,砍了没人发现。
5. 踩坑实录和排查清单
5.1 球陷进地形里出不来
这是最高频的问题,症状是球卡在坡里不动,或者疯狂抖动。
原因一:位置修正不彻底。有的写法是“碰到地面就把 y速度 反向”,但没把球的位置推出来。球的位置还在地面以下,下一帧又“碰到”,又反向,于是原地抽搐。正确做法是先纠正位置(把球顶到地面之上),再改速度。
原因二:一步跨太远。球的速度是 20 像素/帧,地形采样步长是 2 像素,一帧之内球可能直接“穿过”一个尖角,落到了地形内部。这个叫隧穿。解决办法是限制最大速度,或者把一帧的移动拆成几个小步(比如每帧移动 3 次,每次移动 1/3 距离)。我一般把速度上限卡在 18,配合 2 像素的采样精度,基本不会穿。
原因三:斜坡的采样点算错了。如果索引换算时忘了 +1,或者 (x+240)/2 用了向下取整而不是四舍五入,采样点会整体偏移一列,球在坡顶附近就会判断错误。排查方法很简单:在球上显示“地面h”这个变量,肉眼对比球底和地面线的位置,差得明显就是公式错了。
5.2 克隆体越积越多导致卡顿
前面说过 300 的上限和配额管理。但还有一种更阴的情况:克隆体删除了,但计数没减。你以为是配额满了,其实是计数虚高。
排查套路:在舞台上用“说”积木实时显示“靶子数”和“弹丸数”,然后什么都不做,观察这两个数字。正常情况下弹丸数应该在 0 到 12 之间来回跳,靶子数在 0 到 8 之间。如果弹丸数一直往上涨不回落,说明有某条分支删除了克隆体但没减计数。把“减计数”写在“删除克隆体”前面,是唯一的正确姿势。
还有一种情况是克隆体自己又克隆自己,形成指数增长。Scratch 里“当作为克隆体启动时”这个帽子积木下面,如果又写了“克隆自己”,那就是灾难。我见过一个孩子的作品,跑十秒之后整个编辑器卡死,最后发现是子弹克隆体里藏了一句“克隆自己”。
5.3 命中判定失灵的各种花样
我把遇到过的整理成一张表,遇到对不上号的就挨个试。
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 子弹穿过靶子不判定 | 靶子克隆体没显示 | 在克隆体启动时加“显示” |
| 一枪打掉全部靶子 | 用了广播且没有坐标过滤 | 加“命中点”距离判断 |
| 打中边缘不判定 | 造型有透明边距 | 编辑造型,裁掉透明区域 |
| 明明没碰到却加分 | 造型太大 / 旋转中心偏移 | 检查造型尺寸和旋转中心 |
| 子弹打在发射器上就没了 | 发射器也在“碰到”检测范围内 | 发射器加忽略判断或用颜色区分 |
其中“造型有透明边距”这条最坑。Scratch 的碰撞是像素级的,透明像素不算,所以如果靶子造型是“一个大圆环 + 周围一圈透明留白”,那留白部分真的打不中。解决办法:在造型编辑器里用裁剪工具把透明边去掉。这个操作孩子不太会,得教一次。
5.4 常见问题速查表
| 问题 | 快速定位方法 | 修复 |
|---|---|---|
| 球不动 | 显示 x速度、y速度 | 检查重力是否被设成 0 |
| 球飞出屏幕不回来 | 显示 x位置 | 检查边界反弹是否被注释掉 |
| 玩家跳不起来 | 显示“y位置 - 地面高度” | 检查采样索引 |
| 靶子不生成 | 显示“靶子数” | 检查“靶子数 < 上限”的判定 |
| 分数不涨 | 显示“命中点x/y” | 检查广播是否发出、靶子是否收到 |
| 切换关卡后状态错乱 | 显示“游戏状态” | 检查清理脚本是否执行 |
| 音效卡顿 | 关闭所有克隆体音效 | 音效只让原角色播放 |
5.5 给孩子的调试习惯,比技术更重要
最后说点软的。这些年带下来我发现,孩子卡住的时候 90% 不是不会积木,是不知道该看哪里。所以我强制要求三件事。
第一,变量要“可视化”。重要的变量前面打勾,让它在舞台上显示出来。球飞出去了,看一眼 y速度 是正还是负,就知道是重力方向搞反了还是别的。这个动作要养成肌肉记忆。
第二,一次只改一个东西。孩子改 bug 的时候喜欢一次改五处,改完发现好了,也不知道是哪一处修好的;改完发现更糟,也不知道是哪一处弄坏的。我要求他们改一处、跑一次、记一句。这个习惯带到大一点的项目里,价值翻十倍。
第三,先备份再折腾。Scratch 的“保存副本”功能要教会。调物理参数的时候,调出一个满意的版本,立刻存副本,再继续折腾。不然调着调着发现“刚才那个手感更好”,已经回不去了。
我个人在实际操作里的体会是:这两个 Demo 看上去是“做游戏”,其实练的是“拆问题”。山地足球里,把“球怎么动”拆成“速度怎么变”和“位置怎么变”两步;打靶里,把“怎么打中”拆成“谁负责检测”和“谁负责销毁”两个角色。孩子能自己完成这个拆分,比做出一个多漂亮的成品重要得多。至于参数——重力设 0.6 还是 0.8,反弹系数 0.55 还是 0.6,其实没有标准答案。我一般让他们做一个“参数实验记录表”,把每次改的数字和观察到的现象写下来,三五行就行。写着写着,他们就会自己发现“原来那个数字是有原因的”,那一刻比任何讲解都值。