
这次我们来看一个挺特别的 FNF 同人模组项目Plutos Reprisal Part4。开发环境不是 Unity不是 Godot而是 Scratch。FNF 也就是 Friday Night Funkin核心是一套节奏对战玩法按歌曲节拍按下方向键角色跟着拍子演唱和表演。很多人以为这类游戏只能在专业引擎里做实际上用 Scratch 的积木就能还原核心体验。Part4 这次做到的功能是镜头系统的移动、放大和缩小这也是 FNF 里最容易被低估的技术点。从标题信息看这个版本的核心成果就是镜头系统开发过程用作者的话说就是“边拍边修 bug”。这句话不是吐槽而是小型运镜项目的真实状态镜头变化会同时影响场景里所有角色坐标、尺寸、缩放、节拍事件任何一个环节算错录屏回放里就能看到角色跑偏、背景露边、画面突然跳动。这篇文章不评价模组的剧情和美术只聊技术实现Scratch 里做 FNF 模组需要准备什么、镜头移动和缩放的坐标换算怎么做、为什么开发过程中 bug 特别多、以及怎么用录屏回放的方式把 bug 一个个修掉。如果你想在 Scratch 里做一个节奏游戏或者想把 FNF 的运镜复刻出来这篇可以直接照着试。1. 项目核心能力速览能力项说明项目类型用 Scratch 编写的 FNF 同人模组项目名称Plutos Reprisal Part4开发平台Scratch 网页编辑器 / Scratch 离线编辑器硬件门槛无特殊要求普通桌面浏览器即可运行开发方式图形化积木编程不需要写代码和配置环境本次核心成果镜头移动、镜头放大缩小、运镜跟随玩法基础四方向键节奏判定、音符下落、角色演出是否需要 GPU不需要CPU 渲染即可是否支持 API不适用是否支持批量任务不适用单局游戏为主输出形式Scratch 在线分享链接 / 本地 .sb3 项目文件适合人群Scratch 游戏作者、FNF 同人创作者、想学节奏游戏实现的人需要说明表里只有镜头功能来自项目标题信息其余玩法参数属于 FNF 标准玩法的一般描述具体要以你实际项目为准。判定窗口大小、音符速度、镜头缩放的上下限都要按自己测试的结果去填不要照搬任何默认值。2. 适用场景与使用边界2.1 适合谁做、解决什么问题Scratch 版 FNF 模组适合三类人。第一类是学过 Scratch 基础积木、想挑战完整项目的开发者。节奏游戏比普通小游戏多了一个“时间轴”维度做完之后对事件协作、克隆体管理、全局变量规划的理解会明显上升。第二类是 FNF 同人创作者想快速验证一个角色、一个谱面或一段运镜在玩法上是否成立。Scratch 用最低的代码成本给出了可试玩的原型比直接在完整引擎里搭工程快得多。第三类是教学场景镜头坐标换算本身就是一个很好的数学和编程结合案例。这个项目解决的核心问题是在 Scratch 没有原生摄像机的前提下用坐标和尺寸计算模拟出“摄像机”。FNF 里角色唱歌时镜头逼近、节拍变化时画面缩放这不仅是特效更是节奏演出的一部分。镜头系统做得好不好直接决定模组有没有“FNF 味”。2.2 不适合什么场景Scratch 不适合做高负荷的完整商业游戏。如果模组有几十个角色、上万个音符、复杂的粒子特效和长音频Scratch 的渲染和脚本执行性能会明显吃紧。更稳妥的路线是先用 Scratch 做玩法原型验证确认真实体验后再迁移到 Godot、Unity 或 LÖVE 这类环境。另外如果你打算发布到 Steam 或用于商业发行Scratch 的社区分享形态也不合适应该尽早切换引擎。2.3 版权、授权与隐私边界FNF 本体代码使用开源协议发布但原版的美术、音乐、角色资产协议和代码协议并不是一回事。做同人模组时要注意几件事使用 FNF 原版角色和物料时需要确认对应素材的许可个人练习和公开发布是两种场景。引用其他模组的角色、曲目时要取得原作者授权并注明来源。发布到 Scratch 社区时要遵守平台的内容与版权规则不能上传盗用素材。如果模组里使用真人肖像、录音素材必须事先获得授权并注意隐私保护。素材合规问题提前理清楚后面开发和发布才不会踩坑。接下来看环境与素材准备。3. 环境准备与素材规划3.1 开发环境Scratch 开发不用装复杂依赖主流方式有两种网页版打开 scratch.mit.edu登录后直接新建项目。优点是自动保存和在线分享适合边开发边录屏。离线版下载 Scratch DesktopWindows/macOS适合网络不稳定或者需要本地备份的场景。如果要做录屏调试建议额外准备 OBS Studio 或系统自带的录屏功能后面会讲“边拍边修 bug”的完整工作流。3.2 素材清单一个 FNF 同人模组至少需要以下几类素材素材类型内容说明歌曲音频一首歌的完整音频格式建议 wav 或高质量 mp3角色美术己方与对手的 idle、sing 四方向、miss 等造型每个状态对应一个 Scratch 造型背景美术场景图需要比舞台大预留镜头缩放余量音符贴图左/下/上/右四个方向箭头通常还分普通与按住两类谱面数据音符出现时间、方向、长度可以整理成列表或文本文件镜头事件表镜头位置和缩放触发时间自己设计后文给示例结构角色的 FNF 演出状态一般至少包括idle待机、singLeft、singDown、singUp、singRight四个方向演唱、miss失误。每个状态可以画一两个造型演出时按节拍切换。3.3 谱面与镜头事件整理谱面用表格或文本提前整理好比边写边查省很多事。推荐结构时间(秒) | 方向 | 备注 0.00 | 下 | 前奏 0.50 | 上 | 第一句 1.00 | 左 | 与对手对唱镜头事件单独一张表时间(秒) | 镜头x | 镜头y | 缩放 0.0 | 0 | 0 | 1.0 2.0 | -60 | 0 | 1.1 8.0 | 60 | 0 | 0.9 16.0 | -40 | 20 | 1.25在 Scratch 里这些数据通常存成列表。可以每个字段一个列表也可以把一行拼成“0|0|0|1.0”这样的字符串再解析后者列表元素更少处理起来更集中。4. 从 FNF 到 Scratch架构设计4.1 FNF 的核心循环FNF 看起来是节奏游戏本质是一个“时间轴驱动”的演出系统。每一帧要回答四个问题现在歌曲进行到哪一拍哪些音符应该出现、移动到哪里玩家这次按键的判定是 perfect、good、bad 还是 miss角色和镜头应该表演什么状态在 Scratch 里这四个问题分别由谱面列表、音符克隆体、按键判定逻辑、镜头与造型切换协作完成。先把这个循环想清楚再开始搭积木后面才不会反复推翻结构。4.2 Scratch 角色划分一个 FNF 场景通常有很多角色职责划分建议如下Scratch 角色职责歌曲控制器播放音频、重置计时器、广播开始/暂停/结算节拍器根据 songTime 计算当前拍广播节拍事件音符发生器读取谱面列表按时间生成音符克隆体音符/箭头每个音符克隆体负责移动、判定、销毁判定按钮 receptor显示四个固定按键位接收键盘输入对手角色按谱面时间自动演唱玩家角色根据按键结果切换造型背景角色替代舞台背景承担镜头变换镜头控制器计算 camX/camY/camZoom供其他角色读取UI 角色血条、分数、连击、判定文字这里有一个关键点舞台背景不能作为“背景角色”来缩放因为 Scratch 没有积木能直接改变舞台背景的尺寸和位置。要做镜头缩放背景必须是普通角色用角色的“大小”积木来控制。4.3 全局变量设计镜头系统要跨角色使用建议用“适用于所有角色”的全局变量并统一加前缀变量名含义数值范围举例songTime歌曲时间秒从 0 到歌曲长度bpm每分钟节拍数90~180beat当前拍序号从 0 开始camX, camY镜头中心的世界坐标与场景范围一致camZoom镜头缩放倍率0.8~1.5targetCamX, targetCamY镜头目标位置每帧向目标插值targetCamZoom镜头目标缩放每帧向目标插值noteCount当前音符克隆体数量用于监控泄漏变量命名清晰排查问题时能少花一半时间。下面进入本节核心镜头移动和缩放怎么实现。5. 镜头移动与放大缩小的实现5.1 没有摄像机就自己算坐标Scratch 舞台的坐标范围是 x ∈ [-240, 240]y ∈ [-180, 180]中心是 (0, 0)。所有角色显示时Scratch 直接把“角色坐标 角色大小”画到舞台上不存在摄像机的概念。FNF 的镜头系统本质上是把世界对象经过一个变换后显示到屏幕。按两步模拟用镜头位置camX, camY对世界坐标做平移。用镜头缩放camZoom对平移后的坐标和角色大小做缩放。公式如下屏幕x (世界x - 镜头x) × 镜头缩放 屏幕y (世界y - 镜头y) × 镜头缩放 角色大小 基础大小 × 镜头缩放验证几个固定值缩放为 1、镜头在原点时角色屏幕坐标等于世界坐标缩放变成 2 时(100, 50) 会显示在 (200, 100)同时视觉尺寸变成两倍如果镜头恰好移到 (100, 50)该角色就显示在舞台正中心 (0, 0)。这三个断言通过说明坐标变换没有方向性问题。5.2 镜头控制的 Scratch 积木把上面的公式做进一个自定义积木勾选“运行时不刷新屏幕”定义 应用镜头变换 (世界x)(世界y)(基础大小) 运行时不刷新屏幕 将 [渲染x] 设为 (((世界x) - (camX)) * (camZoom)) 将 [渲染y] 设为 (((世界y) - (camY)) * (camZoom)) 将x坐标设为 (渲染x) 将y坐标设为 (渲染y) 将大小设为 ((基础大小) * (camZoom)) %所有“世界对象”背景、对手、玩家角色在每帧都要调用这个积木。音符、判定按钮、血条、分数属于 UI 层不要应用镜头变换否则镜头一缩放玩家就看不清按键了。5.3 平滑跟随不要瞬移FNF 的镜头不会瞬间跳到目标而是每帧向目标移动一部分。这种“平滑插值”在 Scratch 里用一个变量更新即可将 [camX] 设为 ((camX) ((目标X) - (camX)) * (0.15)) 将 [camY] 设为 ((camY) ((目标Y) - (camY)) * (0.15)) 将 [camZoom] 设为 ((camZoom) ((目标缩放) - (camZoom)) * (0.2))系数越小越钝越大越跟手0.1 到 0.2 是常见区间。如果镜头出现“抖动”或“跳跃”先检查是不是目标值突变或者系数设置大于 0.5。镜头帧更新放在镜头控制器的“重复执行”里每帧调用一次。它还可以顺便处理镜头事件表根据 songTime 找到当前应该生效的事件把它写入 targetCamX 等目标变量。5.4 节拍驱动的运镜FNF 运镜和音乐节拍强相关常见做法有两种。第一每拍脉冲。收到节拍器的“第几拍”消息时把 targetCamZoom 临时设为基础值加 0.05下一拍再减回来。这样画面会有一张一弛的呼吸感。第二段落事件。谱面遇到副歌、间奏、角色切换时直接更新镜头目标。事件表数据可以整理成 JSON 便于对照{ bpm: 120, offset: 0.0, camera_events: [ { time: 0.0, cam_x: 0, cam_y: 0, zoom: 1.0 }, { time: 2.0, cam_x: -60, cam_y: 0, zoom: 1.1 }, { time: 8.0, cam_x: 60, cam_y: 0, zoom: 0.9 }, { time: 16.0, cam_x: -40, cam_y: 20, zoom: 1.25 } ] }Scratch 里没有原生的 JSON 解析实际开发时通常先把这张表转成 Scratch 列表每个字段一个列表或者把一行拼成“0|0|0|1.0”再解析。开发早期建议先写固定值测试镜头效果确认平滑系数和事件逻辑没问题再接入真实事件表。5.5 背景素材要留出“出血”背景是镜头缩放最容易穿帮的地方。因为缩放倍数大于 1 时可见区域只占世界坐标的一部分当 camZoom1.5 时可见世界区域是 480/1.5320 宽、360/1.5240 高。镜头如果在范围内移动背景至少还要覆盖镜头偏移出去的部分。所以背景图片要画得比舞台大常见做法是 720×540 或 960×720 像素并在边缘留出出血位避免缩放时露出舞台底色。具体数值取决于你的 camX/camY 范围和最大缩放测试时把镜头拉到极限位置看边缘就能确认。5.6 参考实现这里给一个 Python 版本的镜头换算参考方便理解公式和做单元验证。在 Scratch 里实现的是同一个逻辑# camera.py - 镜头换算参考实现 def world_to_screen(wx, wy, cam_x, cam_y, zoom): return (wx - cam_x) * zoom, (wy - cam_y) * zoom def smooth(value, target, factor0.1): return value (target - value) * factor # 验证用例 assert world_to_screen(100, 50, 0, 0, 1) (100, 50) assert world_to_screen(100, 50, 0, 0, 2) (200, 100) assert world_to_screen(100, 50, 100, 50, 1) (0, 0) cam_x, cam_y, zoom 0, 0, 1.0 for _ in range(60): cam_x smooth(cam_x, -60, 0.15) cam_y smooth(cam_y, 0, 0.15) zoom smooth(zoom, 1.1, 0.2) print(cam_x, cam_y, zoom)Scratch 里没有断言积木但可以在“应用镜头变换”前放一个固定测试角色人为输入 (100, 50, 100) 检查输出是不是 (100, 50, 100%)再输入缩放 2 检查 (200, 100, 200%)。这一步能提前排除一大类坐标 bug。6. 边拍边修 bug开发调试与录屏回放6.1 为什么运镜项目 bug 特别多镜头本身不会产生 bugbug 来自它“影响面太大”。一个全局坐标和缩放的改动会同时影响背景、对手、玩家三个角色的位置和大小。常见的连锁问题包括角色坐标算错画面里所有人都一起偏移。切换造型后大小被重置角色突然变大变小。背景素材不够大镜头一移就露出底面。镜头平滑系数设置过大出现抖动。缩放公式忘乘或者乘错音符和角色对不上。这些问题都发生在移动和缩放同时进行时用肉眼盯着编辑器很难发现必须录屏回放。6.2 录屏回放调试法“边拍边修 bug”本质上是把录屏当成调试工具。推荐流程每完成一个功能先不急着加下一个而是录一段 30 秒左右的试玩视频。录的时候故意跑到极限情况镜头拉到最远、放大到最大、音符最密集的段落。回放时逐帧暂停观察角色的世界坐标、镜头值、缩放值是否合理。发现问题后先复现用固定 songTime、固定镜头目标值跑同一段确认 bug 是否稳定出现。修复后重新录一遍做前后对比。录屏还有一个额外的好处它天然形成了一份视频版变更记录。翻看往期录屏能快速定位哪个版本引入了哪个 bug比靠记忆、靠聊天记录找问题要快得多。做类似项目时每个里程碑保留一段录屏排查问题时先看录屏再去看积木定位效率会明显高于直接从积木堆里找。6.3 Scratch 里的调试手段变量监视器把 camX、camY、camZoom、songTime 拖到舞台上实时观察数值变化。说话积木在镜头控制器里加一个“如果 bug标记1 说 (组合(camX))”的临时积木定位问题后删掉。测试消息给镜头控制器发“测试居中”“测试缩放1.5”这样的广播让它在固定输入下跑一小段方便对比。舞台备注在舞台备注里记录每次修改的日期和现象回看时知道当前版本修了什么。6.4 这类项目最典型的三个 bug第一个是“切换造型后尺寸恢复默认”。Scratch 的“将大小设为”是角色级状态切换造型后不会自动保持。如果“应用镜头变换”只在初始化时调用一次切造型后会瞬间变回 100%。解决办法是把尺寸设置也放进每帧的镜头变换积木里。第二个是“音符与音乐不同步”。Scratch 播放音频时拿不到当前播放进度常见做法是用计时器计时歌曲开始时“重置计时器”之后 songTime计时器。但这样做每次微小的延迟都会累积。更稳妥的玩法是让节拍器读事件表用一拍一秒换算不依赖音频播放状态。第三个是“克隆体泄漏”。音符克隆体在判定后如果没有明确“删除此克隆体”数量会越积越多最后拖垮帧率。要在监控变量里持续观察 noteCount确认每次命中或失误后对象都能回收。7. 功能测试与效果验证7.1 镜头系统测试镜头是本次核心测试要覆盖六个维度测试项操作预期结果通过标准初始居中绿旗启动后不操作场景中心对准目标点camX/camY 为初始值画面无偏移平移跟随播放到镜头事件 2.0s镜头平滑移向左/右无跳变中途无卡顿放大缩小触发 zoom 1.25 事件背景和角色同步放大UI 层不放大无黑边节拍脉冲进入副歌段落每拍有轻微缩放波动波动值稳定在 0.03~0.06场景极限拉满 camX/camY 边界背景不露出舞台底色边缘留白有效结束后复位歌曲结束镜头回到初始值变量重置正确7.2 玩法测试镜头改完后玩法侧也要同步验证音符生成开头第一个音符是否在正确时间出现位置是否在对应 Lane。判定窗口在 perfect/good/bad/miss 边界各按一次确认结果稳定。角色演出对手自动演唱的造型切换是否和节拍对齐玩家 miss 后是否有正确演出。结算流程歌曲结束、血量归零时是否进入正确界面。每个测试都要记录现象和当时的镜头值。测试记录表参考日期版本功能测试输入实际输出是否通过备注第一天镜头居中songTime0居中正常通过无第一天镜头缩放 1.25事件 16s背景露边不通过素材需要扩大8. 常见问题与排查方法问题现象可能原因排查方式解决方案镜头缩放后角色位置偏移坐标公式没有乘缩放用固定值 (100,50) 验证统一走镜头变换积木切换造型后尺寸突变Scratch 切造型会重置大小检查切换后的尺寸监视器每帧调用“应用镜头变换”背景出现黑边/露底背景素材不够大或 cam 超出覆盖范围拉满镜头边界回放放大素材或限制镜头范围镜头突然跳动目标值突变或平滑系数过大看 targetCamX 是否有跳变事件表加过渡系数控制在 0.1~0.2音符和音乐对不上计时器误差或首拍 offset 不对对比 songTime 与音频节拍用表格事件驱动校准 offset克隆体越来越多音符判完未删除看 noteCount 监控判完立即“删除此克隆体”卡顿/掉帧克隆体过多或每帧广播过多用性能监视器定位复用克隆体减少广播人物和背景不同步两者没有在同一帧入口更新确认都调用了镜头变换统一走镜头控制器入口9. 性能优化与最佳实践9.1 把镜头逻辑集中管理镜头不要由每个角色自己算而是由一个“镜头控制器”负责维护 camX/camY/camZoom其他角色每帧只读变量并应用变换。这样改平滑系数、改缩放上下限时只动一个位置不会出现改了背景忘了角色的情况。9.2 控制每帧的广播数量Scratch 的广播是有开销的。节拍器每拍广播一次没问题但如果每个音符克隆体都广播高密度段落性能就会危险。更好的做法是把音符状态写进变量UI 角色每帧读变量渲染而不是靠广播同步。9.3 音符克隆体的生命周期出现条件songTime 超过该音符的 time并且该音符还没被生成。移动根据 songTime 和音符 time 计算 y 位置。判定键盘按下时检测克隆体位置命中后立即删除。兜底音符 y 位置越过下方边界即删除防止 miss 后泄漏。9.4 素材与项目文件管理背景图不要无限大720×540 或 960×720 一般够用超高清位图会让 Scratch 项目启动变慢。音频尽量压缩到合理体积避免项目加载时间过长。项目文件(.sb3)经常备份每完成一个稳定版本另存一份。素材目录按 audio、assets、charts、events 分文件夹方便后期替换。9.5 工程化的小步验证每加一段功能先跑“最小验证”镜头递增测试、缩放边界测试、节拍器误差测试。通过后再接素材和玩法。这样能避免把坐标 bug 和素材 bug 混在一起。尤其当项目里既有镜头、又有音符、又有角色演出时问题一旦混在一起排查时间会成倍增加。10. 总结这次围绕 Plutos Reprisal Part4 聊的核心就一句话Scratch 里做 FNF 模组镜头系统不是特效是数学。镜头移动放大缩小的本质是把“世界坐标 → 屏幕坐标”的换算公式跑通并让所有世界对象在每一帧应用同一套变换。公式就三行屏幕坐标等于世界坐标减镜头位置再乘缩放角色大小等于基础大小乘缩放。验证它是否正确的办法也很简单用固定值跑三次缩放 1 时坐标原样、缩放 2 时坐标和尺寸翻倍、镜头对准某个角色时它应该出现在屏幕中心。如果你也想动手做一个 Scratch 版 FNF 模组最先应该验证的是计时器和音符生成的同步其次才是镜头。镜头属于演出层如果玩法本身和音乐不同步镜头再华丽也会让玩家觉得“画面和音乐脱节”。最容易踩的三个坑是切换造型导致尺寸重置、背景素材不够大导致露边、音符克隆体泄漏导致卡顿。这三个坑都可以通过录屏回放和变量监视器提前抓住。后续可以继续扩展的方向包括多段镜头切换、段落间淡入淡出、自定义谱面工具以及把 .sb3 原型迁移到 Godot 或 LÖVE 做更正式的版本。目前 Part4 的镜头实现已经足够支撑一个可试玩的 Scratch 节奏模组了。