用Scratch做FNF模组 Part4:镜头移动和放大缩小终于做出来了——边拍边修bug的完整复盘
做节奏模组的人都知道,一首歌能不能让玩家“爽”起来,除了谱面本身,最吃功夫的就是镜头语言。FNF(Friday Night Funkin')里那个随着BGM鼓点突然拉近的画面,角色唱到高音时镜头微微颤抖的感觉,才是真正让演示视频看起来“有那味”的关键。
但这句话放到Scratch里,马上就变成另一个问题:Scratch根本没有“摄像机”。
这次做Pluto‘s Reprisal Part4的时候,我把镜头移动和放大缩小做出来了,过程比想象中曲折得多。最典型的开发节奏就是“边拍边修bug”——录一段演示视频,回放发现镜头跳了、缩放出问题了,暂停、改积木、再录。一个几秒钟的镜头效果,前后折腾了好几个晚上,中间还顺手摸清了页面布局和坐标计算的不少坑。
这篇博文就把这段经历完整复盘一遍:Scratch里的伪镜头原理、镜头移动和缩放的具体实现思路、以及我在实际开发里遇到的那些bug和排查方法。如果你也在用Scratch做节奏游戏,或者想做带镜头效果的横版动作游戏,这篇文章应该能帮你少走不少弯路。
1. 先搞清楚:FNF模组在Scratch里到底难在哪
先说结论:在Scratch里做一个能玩的FNF模组,真正难的从来不是“接音符”的逻辑,而是三件事——谱面和音乐的对齐、镜头表现、以及项目结构。
FNF的玩法本质是“跟着音乐节奏,在箭头到达判定线时按下对应方向键”。用Scratch的按键事件和变量完全可以实现,音符克隆体、下落、判定线碰撞,这些基础机制在社区里已经有大量现成方案。
但Part4要做的是带有镜头表现力的演示版本,需求一下就变了。镜头要根据歌手的位置平滑移动,鼓点落下时要有一个明显的放大缩小冲击感,歌词或者特效还要跟着变。这些东西加在一起,游戏循环的每一帧都要做大量坐标计算,任何一个环节算错,画面就会“啪”地跳回中心,或者缩放到某个奇怪的位置。
我这次踩得最深的坑就是:误以为“镜头移动”和“放大缩小”是两个独立功能,分开做就行。结果把它们合到一起时,坐标全部乱掉。后来才想明白,镜头移动和缩放共用一套坐标变换公式,必须放在一起设计,否则就是给自己埋bug。
另外一个容易忽略的难点是“演示场景”。边拍边修bug意味着你要反复录制视频、回放、截图、对比。Scratch编辑器本身没有帧调试器,所有问题只能靠肉眼和慢速播放去找。这个流程如果没想清楚,项目会越改越乱。
所以本文不只是讲几个积木怎么拖,更重要的是讲清楚一套能在Scratch里落地的伪镜头方案,以及配合这套方案的调试习惯。
2. 核心概念:Scratch里的“伪镜头”是怎么来的
2.1 Scratch为什么没有摄像机
Scratch的舞台是一个480像素宽、360像素高的平面坐标系统,原点(0,0)在舞台正中央。角色要么显示在舞台上,要么隐藏,没有“视口”“相机矩阵”“渲染管线”这些概念。
其他游戏引擎里的摄像机,本质是一个“视口”,你告诉它看向哪里,它就把世界坐标转换成屏幕坐标,再交给渲染器。Scratch没有这层机制,所以只能自己造。
所谓“伪镜头”,就是用一组变量记录“镜头的位置”和“镜头的缩放”,然后手动把所有世界物体的位置换算到屏幕上。这个思路和真正的引擎是一回事,只是所有换算都得自己写。
2.2 三个核心变量
我用的方案是全局变量,选择“适用于所有角色”:
| 变量名 | 作用 | 建议初始值 |
|---|---|---|
| 镜头X | 镜头中心的世界X坐标 | 0 |
| 镜头Y | 镜头中心的世界Y坐标 | 0 |
| 缩放值 | 当前缩放比例,100表示原大小 | 100 |
这三个变量是所有镜头效果的“唯一数据源”。所有角色在每一帧都根据这三个变量重新计算自己的屏幕位置。谁要是绕过这三个变量直接改坐标,后期一定会出问题。
2.3 拍子与时间基准
节奏游戏的镜头效果必须和音乐挂钩,不能用一个随意的计时器。正确做法是先算出BPM(每分钟节拍数),再把“当前时间”换算成“当前在第几拍”,然后所有镜头动作都以拍为时间单位触发。
比如BPM是120,每拍的间隔就是60除以120等于0.5秒。我在Part4里用一个“全局拍数”变量来记录已经过了多少拍,每次音乐播放器开始新的节拍时就广播一次“节拍事件”,镜头效果只在这个事件里触发。
这样做的最大好处是:音乐节奏变了,只需要改BPM这一个变量,所有镜头效果自动跟着变,不用每个积木都改一遍。
3. 环境准备与项目结构
3.1 用哪个Scratch版本
推荐使用Scratch 3.0桌面版。原因很简单:Part4这种带大量克隆、音效和镜头计算的项目,在线编辑器在中后期会明显卡顿,桌面版的稳定性和资源管理都更好。版本请以实际下载为准,本文演示的是通用思路,不依赖某个具体的3.0小版本。
另外要提醒一点:如果项目里含有外部导入的音乐和素材,先确认版权可用。自制BGM或者获得授权的人声曲目是最稳妥的选择。
3.2 网上找的素材怎么直接用
很多朋友会问“网上找的素材如何直接在Scratch中使用”,这里统一说一次。
图片素材可以直接把文件拖进Scratch编辑器,会自动生成一个造型。但要注意两点:一是背景图要导入成“舞台背景”,不要导入成角色;二是带透明背景的图片,最好用PNG格式,JPG会自动补白底,放到舞台上会非常突兀。
音频素材需要提前处理。Scratch对音频时长没有硬性限制,但导入一首3分钟的高码率音乐后,项目文件会很大,打开和保存都会变慢。建议把音乐压成128kbps左右的MP3,能明显减小体积。
3.3 推荐的项目分层
我把角色分成四个层次,用一个“镜头控制器”统一驱动:
| 图层 | 包含内容 | 是否受镜头影响 |
|---|---|---|
| 背景层 | 远景、墙体、装饰 | 是,但偏移量小 |
| 舞台层 | 歌手、对手、麦克风等主要角色 | 是,偏移量中等 |
| 特效层 | 鼓点冲击环、歌词、转场特效 | 是,偏移量随特效而定 |
| UI层 | 判定线、血条、分数 | 否,永远固定在屏幕上 |
UI层不受镜头影响,这是很多新手容易搞错的地方。判定线要是跟着镜头跑,玩家根本没法稳定按音符。
3.4 变量设计:别什么都用“仅适用于当前角色”
镜头相关的变量必须选“适用于所有角色”,否则克隆体之间互相读不到。这里顺便回答一个高频问题:Scratch中变量模块列表是怎么使用的。变量的作用域有三种——“仅适用于当前角色”“适用于所有角色”“仅适用于当前克隆体”。做镜头系统时,镜头X、镜头Y、缩放值、全局拍数,全部用“适用于所有角色”;每个音符克隆体自己的速度、目标位置,用“仅适用于当前克隆体”。把这两组变量分清,项目就不会出现“一个克隆体改了变量,其他克隆体全部乱飞”的经典事故。
4. 镜头移动:不是移动摄像机,是移动整个世界
4.1 核心思路
Scratch没有摄像机,所以镜头移动其实是反着来的:镜头往右移动,世界就往左移动;镜头往上移动,整个世界就往下移。
最省事的做法是做一个“舞台容器”角色,背景层、舞台层、特效层都作为它的子对象。镜头移动时,只改这个容器角色的x和y坐标,方向与镜头变量相反。
下面的积木是文本化表示,方便整理思路,实际在Scratch编辑器里直接拖对应积木即可。
// 舞台容器的移动逻辑 当绿旗被点击 重复执行 将 [镜头X v] 增加 (((目标X) - (镜头X)) * (0.08)) 将 [镜头Y v] 增加 (((目标Y) - (镜头Y)) * (0.08)) 将 x 坐标设为 ((0) - (镜头X)) 将 y 坐标设为 ((0) - (镜头Y))这里最关键的是那个0.08。它表示每帧镜头向目标位置靠近8%,数值越小越平滑,但过小会显得拖沓;数值越大响应越快,但超过0.3就容易出现明显“甩镜头”的顿挫感。实际取值要结合项目的帧率和节奏感调整。
4.2 平滑跟随的效果
为什么不用“移到目标位置”而是用这种“每帧靠近一点”的写法?因为直接瞬移会让画面毫无过渡,玩家眼睛跟不上。真实游戏里的镜头是带惯性的,先加速、后减速、最后停稳。这个0.08的写法就是最简单的惯性模拟,一帧一帧逼近目标,停下来之前自然会有轻微的“过冲再回摆”效果,很有手感。
4.3 鼓点冲击位移
FNF里最常用的镜头技巧之一,是鼓点落下的瞬间让镜头朝某个方向小幅抖动一下,再回到原位。实现上就是给目标位置临时加一个偏移量:
// 收到节拍事件时,临时给目标X加偏移 当接收到 [下个节拍 v] 将 [目标X v] 增加 (8) 等待 (0.05) 秒 将 [目标X v] 增加 (-8)注意偏移值不要加在“镜头X”本身上,要加在“目标X”上。否则平滑跟随逻辑会把这次偏移当成永久位移,镜头就再也回不到原来的位置了。这个区别,我在Part4里至少踩了三次,每次都表现为“镜头突然永久偏移到一边”。
4.4 边界限制
镜头不能无限制移动。角色唱到舞台最左边时,镜头如果还跟着角色走,画面右侧会露出大片空白区。所以要给镜头X和镜头Y加范围限制:
如果 <(镜头X) < (-200)> 那么 将 [镜头X v] 设为 (-200) 否则 如果 <(镜头X) > (200)> 那么 将 [镜头X v] 设为 (200) end end限制值要根据舞台的宽度和角色位置实测确定,不要拍脑袋写死。我的经验是先让角色站到舞台最边缘,看屏幕哪个方向出现空白,再逐步调整限制值。
5. 放大缩小:Scratch里最容易翻车的效果
5.1 Scratch的“缩放”本质
Scratch里每个角色都有“大小”属性,100是原始大小,200就是放大一倍。这个属性做静态放大没问题,但用在镜头缩放上有一个致命弱点:缩放的中心点是角色自身的中心点,不是屏幕中心。
如果你的舞台容器角色中心在原点,那放大效果就是围绕舞台中心进行的,接近真实镜头效果。但只要容器中心被移动过,缩放就会把画面往一边扯,越放大偏移越明显。这是“缩放导致画面错位”的根源。
5.2 居中缩放的标准写法
正确的处理方式,是缩放时先记录当前容器的中心位置,缩放后再根据缩放比例补偿坐标偏移。简单说,就是把“世界坐标→屏幕坐标”的换算统一收口到一个公式里。
// 物体定位:统一坐标换算,世界坐标转为屏幕坐标 将 [屏幕X v] 设为 ((((物体世界X) - (镜头X)) * (缩放值)) / (100)) 将 [屏幕Y v] 设为 ((((物体世界Y) - (镜头Y)) * (缩放值)) / (100)) 将 x 坐标设为 (屏幕X) 将 y 坐标设为 (屏幕Y)这段代码的意思是:先算出物体相对镜头中心的世界偏移,再乘以缩放系数,得到它在屏幕上的实际位置。当缩放值是100时,公式退化成普通偏移,之前做好的镜头移动逻辑完全不用改;当缩放值是150时,所有物体离镜头中心更远、看起来更大,画面自然就有了拉近感。
这也是我在Part4里最终采用的方案。它比直接改“舞台容器”大小稳定得多,因为坐标计算和缩放计算用的是同一套变量,不会出现“挪了镜头但缩放又自动回中”之类的割裂问题。
5.3 鼓点缩放冲击
有了上面的坐标换算公式,鼓点缩放就变得很直接:在节拍事件里,先把缩放值快速推高,再让它慢慢回落到100。
// 缩放冲击:快速拉近再复位 当接收到 [下个节拍 v] 重复执行 (6) 次 将 [缩放值 v] 增加 (5) 等待 (0.02) 秒 重复执行 (10) 次 将 [缩放值 v] 增加 (-3) 等待 (0.02) 秒 如果 <(缩放值) < (100)> 那么 将 [缩放值 v] 设为 (100) end注意最后那个“小于100就设为100”的兜底逻辑。它防止因为等待时间被音乐播放器拖慢,导致缩放值没有完全回落到100就开始下一轮伸缩,累积几次之后画面会越来越小。
5.4 缩放状态下的模糊感
Scratch是矢量渲染的,放大到120以上时,位图素材会明显变糊。这个在镜头缩放里很难完全避免,只能从素材侧缓解:要求美术出图时按“放大后的最大尺寸”出图,而不是按原始尺寸出。比如计划最大放大到150%,那素材就按1.5倍尺寸绘制,这样缩放过程中视觉上几乎无损失。
6. 边拍边修bug:Part4实际开发流程复盘
6.1 为什么一定要“边拍边修”
Scratch没有断点调试,没有日志面板,你在运行时是看不到变量变化的。镜头移动这种动态效果,肉眼盯着编辑器根本看不出问题在哪一帧。所以我采用的工作流是:
录制演示视频 → 回放逐帧检查 → 定位异常帧 → 回编辑器改积木 → 重新录制验证。
这个循环听着简单,实际跑起来很考验耐心。一次录制可能只需要30秒,但回放、截图、对比往往要花10分钟。Part4的镜头部分,我前后录了二十多次短视频,每次都是为了确认一个细节。
6.2 一个典型的定位过程
举一个实际例子。我最初发现:镜头跟随歌手时,画面总会在某个特定位置“抖一下”。回放视频逐帧看,发现是歌手唱到特定台词时移动速度变快,镜头跟着快速平移,但此时缩放值恰好大于100,两套计算叠加导致画面产生了明显的跳变。
定位过程分成两步。第一步,把缩放值临时固定为100,重新录制,抖动消失,说明问题出在缩放和移动的叠加上;第二步,让镜头固定不动,只改变缩放值,再录制,发现缩放过程中有几个角色位置计算错误。
最后确认是舞台层角色的坐标公式里漏乘了缩放系数。问题看起来很简单,但如果不靠逐帧对比,光看代码根本发现不了。
6.3 风险管理:改之前先存一份
每次大改之前,先另存为一个带编号的版本,比如PlutoReprisal_Part4_v07。Scratch项目文件不大,多存几个版本不占什么空间,但能让你在改崩之后随时回退。
这件事我在Part4里体会特别深。有一次我为了调一个缩放冲击效果,连续改了十几处积木,最后效果还不如之前,但因为没存版本,只能靠记忆慢慢改回去,浪费了一整晚。
6.4 顺便聊聊bug的生命周期
在Scratch里做项目,“bug的生命周期”比写代码时更清晰:发现、复现、定位、修复、回归、记录。很多人只做到了前四步,修复完就跑了,结果同一类bug换个场景又出现。
我建议在项目里维护一个“Bug记录”列表,每次修完一个问题就把现象、原因、解决方案写进去,格式随意,哪怕是三行字也行。后期你会感谢这个列表的,因为第二部分的人物动画、第三部分的谱面逻辑,很多bug的根因都是一样的,查旧记录比从零排查快得多。
7. 常见Bug清单与排查思路
下面这些是我在Part4开发中实际遇到、以及同类项目里高频出现的问题,按现象整理成表。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 镜头突然跳回舞台中心 | 舞台容器的x/y被其他积木直接写死 | 搜索所有“移到x/y”和“将x坐标设为”积木 | 镜头位置只通过镜头X/Y变量赋予,禁止其他角色直接改容器坐标 |
| 放大后画面整体偏到一侧 | 缩放中心不是屏幕中心,坐标补偿缺失 | 缩放值改为非100,观察边缘角色位置 | 统一走“世界坐标→屏幕坐标”公式,不要直接改容器大小 |
| 鼓点缩放后画面越来越小 | 缩放值没有完全回落到100 | 在节拍事件末尾输出缩放值 | 加“小于100则设为100”的兜底逻辑 |
| 音符和判定线不同步 | 音符坐标计算和UI层混在一起 | 检查音符克隆体是否使用了镜头变量 | 音符克隆体只算“世界坐标”,UI层完全不参与镜头换算 |
| 角色出现闪烁或重影 | 同一画面有多个可见的重复角色 | 截图观察重叠区域 | 检查是否重复创建克隆体,或角色在多个图层都被放置 |
| 音乐播放时画面卡顿 | 项目文件过大或克隆体过多 | 查看克隆体数量上限提示 | 压音乐MP3、压缩图片尺寸、减少同时存在的特效克隆体 |
| 导入的PNG素材有白底 | JPG压缩或透明通道丢失 | 放大素材查看边缘 | 重做带透明通道的PNG,确认背景层未误用舞台背景 |
| 节拍事件触发两次 | 多个精灵都收到了同一个广播并各自执行 | 在事件响应里打印或计数 | 只让镜头控制器响应节拍广播,其他角色通过询问镜头控制器取值 |
| 缩放后点击碰撞判定错位 | 角色大小变化但碰撞范围还是旧坐标 | 用边缘点击测试 | 碰撞判定也走同一套坐标换算,或统一改用“如果碰到”积木 |
排查时我的固定顺序是:先复现,再看变量,再查积木。Scratch虽然没有调试器,但可以在角色上临时加一个“说(变量)”积木,把关键变量显示在舞台上,运行时就能实时观察数值变化。排查完记得删掉这些临时积木,否则正式演示时会穿帮。
8. 最佳实践与工程建议
8.1 把镜头系统做成一个独立角色
不要把所有积木堆在歌手或对手角色上。单独建一个“镜头控制器”角色,它不显示任何造型,只负责维护镜头X、镜头Y、缩放值三个变量,并接收节拍广播。其他所有角色都在自己的积木里根据这三个变量计算位置。
这种做法的好处是:镜头逻辑只存在一处,排查问题的时候只需要看一个角色的积木。否则镜头相关的积木散落在十几个角色里,改一处漏十处,bug根本无法收敛。
8.2 所有节拍时间用变量,不要写死
把BPM、每拍间隔、当前拍数都做成全局变量。写死数字的后果是,你想把一首歌从120BPM调到128BPM时,需要找遍所有积木里的0.5、0.468这些数字,改错一个就全乱。
用变量之后,所有时间计算都从“每拍间隔”变量读取。今后换歌只需要改BPM,这属于一次性投入、长期收益的实践。
8.3 慢速调试法
很多镜头bug在正常速度下根本看不出来。我的办法是做一个“慢速模式”开关变量。开启后,整个游戏的节拍频率降到原来的四分之一,音乐可以暂停,但镜头计算仍然每帧执行。这样就能在慢速播放中清楚看到镜头移动的每一帧轨迹。
具体实现不复杂:把音乐播放器的时间基准变量替换成一个“慢速时钟”,每帧只增加正常值的四分之一。音符逻辑、镜头逻辑都依赖这个时钟,整个游戏就会整体变慢,而不会出现“镜头慢了但音乐正常”的错乱。
8.4 演出效果不要和玩法逻辑耦合
Part4的镜头效果是偏演示向的,所以我把“演出脚本”和“判定逻辑”分开。演出脚本负责镜头移动、缩放、特效触发;判定逻辑只负责音符到达判定线、扣血加分。
耦合的典型症状是:镜头一个抖动的bug会导致音符判定偏移。这完全不应该发生。镜头只是画面表现,不应该影响游戏核心数据。
8.5 导出演示视频时注意帧率
Scratch桌面版自带的录制功能在复杂场景下会有掉帧,而镜头平滑效果对帧率极其敏感。如果你录出来的视频看起来一顿一顿,先确认不是播放器的问题,再考虑用外部录屏软件以60帧录制。
9. 总结与下一步方向
这一版Part4最核心的收获,是把“镜头移动”和“放大缩小”统一到了一套坐标变换公式里。所有角色通过“世界坐标→屏幕坐标”的换算显示,镜头变量、缩放变量成为唯一数据源,bug数量一下子少了很多。
边拍边修bug虽然听起来很狼狈,但它确实是Scratch项目最实用的调试方式。因为没有断点和日志,唯一可靠的反馈就是画面本身。养成“录制回放→定位→修复→回归”的习惯,比靠猜改积木有效得多。
接下来可以尝试的方向有三个:一是给镜头加旋转效果,这需要在坐标公式里引入三角函数,算是Scratch伪镜头进阶;二是做更复杂的演出脚本,比如镜头在某一小节切换到对手、再切回歌手;三是把鼓点缩放和音符判定线联动,配合血条演出做出更完整的FNF观感。
如果这篇复盘对你有帮助,建议收藏备用。等你做到镜头和缩放这关时,再回来看第5节和第7节,应该能少熬夜。