如果你试着把一个 FNF 模组的标题改成“fnf vs 享利(但是Pico)”,你会发现社区里这类“换成某个角色再打一遍”的模组,远没有看起来那么简单。很多人以为它只是把对手贴图换掉,实际动手改完才发现:角色动画不播、音符没有对象、音频对不上时间轴,甚至游戏直接白屏。这篇文章想讲清楚的,正是一个 FNF 模组从标题到可运行之间,那些真正容易被忽略的技术环节。
这里先给一个明确判断:FNF 模组开发的核心不是美术替换,而是角色动画、谱面时间轴、音频文件三套数据的对齐。你花多少时间画素材,就要花差不多的时间确保 JSON 里的每个字段都被引擎正确读取。
这篇文章会从一个社区常见的“vs 享利(但是Pico)”类模组切入,讲清楚 FNF 模组目录结构、角色数据格式、谱面 chart 格式、环境搭建、完整实现、运行验证、常见坑和工程建议。无论你是刚接触 FNF 模组的新手,还是想系统理解 FNF 数据结构的开发者,都可以按文中的流程跑通一个最小角色替换模组。
1. 这篇文章真正要解决的问题
先回答最实际的问题:为什么现在的 FNF 模组社区里,“XX 但是 XX”这种命名非常多?因为 FNF 的模组体系允许作者复用现成谱面、音轨和角色形象,只替换其中某个角色或难度,就能产生一个玩法完全不同的新关卡。这类模组的改动量看似小,实际涉及 FNF 引擎对角色和谱面读取的完整链路。
具体来说,“fnf vs 享利(但是Pico)”这类模组的技术本质是两件事:
- 角色替换:把原谱面中的对手角色(或玩家角色)从 A 换成 Pico,要求 Pico 的动画、血量图标、缩放、镜头位置都被正确加载。
- 谱面兼容:原“vs 享利”谱面的音符时间点、BPM、长条、事件标记,必须在新角色下仍然能正确命中、播放和判分。
如果你只是在图片层面把角色换掉,运行时会发现很多问题:比如角色一直保持闲置动画,按下方向键后显示的是默认 Boyfriend 动画;或者角色显示成空白的“TXT”占位符;更严重的是,如果 mod 里的角色 json 字段写错,整个歌曲加载都会中断。
因此,这篇文章不打算只教你怎么“改名”,而是把 FNF 角色模组的完整技术链路拆开:环境、目录、素材、JSON 格式、验证、排错。读完你应该能做到:
- 理解 FNF 官方引擎和社区引擎分支的模组加载机制;
- 搭建一个能调试和打包 FNF 模组的环境;
- 创建一个包含 Pico 角色的最小模组,并正确接入“vs 享利”类型的谱面;
- 遇到加载失败时,有能力按结构排查而不是重新下载别人打包好的文件。
如果你之前完全没有接触过 FNF 模组,也不用担心,本文会从目录结构讲起;如果你已经做过简单换皮 mod,可以直接跳到第 5 章看 chart 与角色数据的完整示例。
2. FNF 模组的结构与原理:角色替换背后的文件层级
2.1 一个 FNF 模组由哪些文件组成
FNF 的模组本质上是一组“覆盖文件”,它会按照固定目录结构加载图片、音频、数据和脚本。以社区最常见的 Psych Engine 分支为例,一个标准的模组目录通常长这样:
mods/ └── MyMod/ ├── pack.json ├── data/ │ ├── characters/ │ │ └── pico.json │ └── songs/ │ └── vs-henry/ │ ├── henry-hard.json │ ├── henry-hard-events.json │ └── ... ├── images/ │ ├── pico.png │ └── pico.xml ├── songs/ │ └── vs-henry/ │ ├── henry-hard.ogg │ └── henry-hard-inst.ogg └── scripts/ └── ...注意,不同引擎分支的目录细节略有差异,但核心思想一致:游戏运行时会根据 mod 文件夹内的相对路径,覆盖到它自带的同名资源上。也就是说,你不需要修改游戏本体,只需要把文件放对位置,引擎启动时就会优先读取你的版本。
2.2 角色(Character)加载的完整链路
在 FNF 中,一个“角色”不只是图片,它是一套数据契约。当你在谱面 JSON 中写"player2": "pico"时,引擎会按以下顺序查找:
- 在
data/characters/pico.json找到角色配置文件; - 根据其中的
image字段,在images/目录加载对应的图集 XML/PNG; - 根据
animations数组,建立“动画名称 → 图集帧区间”的映射; - 根据空闲、向左、向下、向上、向右、跳跃等动画名称,绑定输入响应;
- 在 HUD 上显示
healthicon指向的血量图标。
这个链路中任何一环断了,就会出现“角色加载了但是不播放对应动画”或者“角色直接变成紫色报错方块”的情况。所以角色替换的工程核心,其实是保证这条链路的五个环节都闭合。
2.3 谱面 Chart 的角色字段与音符数据
FNF 的歌曲谱面文件是一个 JSON,里面保存了 BPM、玩家角色、对手角色、音符数组等信息。以一首“vs 享利(但是 Pico)”的 hard 难度为例,最核心的角色字段在"song"对象中:
{ "song": { "song": "henry-hard", "bpm": 140, "speed": 2.5, "player1": "bf", "player2": "pico", "gf": "gf", "notes": [] } }player1:玩家操纵的角色,通常是 BF(Boyfriend)。player2:对手角色,这里改成"pico"后,引擎会去加载 Pico 的角色配置。gf:伴唱角色,不是每首曲子都有。
如果你只是想让 Pico 站在对立面,核心字段就是player2。但如果你希望玩家操纵的角色变成 Pico,需要把player1也改成"pico",并且要确保 Pico 的角色配置里包含玩家侧需要的动画(比如idle、singLEFT、singDOWN、singUP、singRIGHT和跳跃动画)。
很多“但是 Pico”类模组,实际上是把 Pico 放进玩家侧,然后把原谱面的 note 排列改成 Pico 相关主题。严格来说,这已经不是简单的“替换贴图”,而是同时改角色配置和谱面映射。
2.4 为什么不能只改图片名
社区里最常见的误解是:把henry.png改成pico.png就可以了。实际运行时你会遇到两个典型问题:
- 动画名称对不上:原角色 XML 图集里子图名称可能叫
Henry idle0000,而你新角色 XML 里叫Pico idle0000,引擎找不到对应帧,就会播放失败。 - json 里的 image 路径指向旧图集:角色 json 里如果写
"image": "henry",即使文件名已经改成 pico,引擎仍然按 json 里的字段去加载henry.png。
因此,角色替换必须做到“图片、XML、角色 json、谱面 json”四个文件同步修改,而不是改文件名。这也是本文后面所有实操步骤都围绕“数据对齐”展开的原因。
3. FNF 模组开发环境准备与工具链
3.1 方案选择:源码构建 vs 现成引擎加载
在开始写 mod 之前,你要先确定技术路线:
| 方案 | 适合人群 | 优点 | 缺点 |
|---|---|---|---|
| 直接使用社区引擎加载 mods | 只做角色替换、谱面配置 | 不需要编译环境,改文件即可 | 依赖具体引擎分支,部分新特性不可用 |
| 从源码构建 FNF 引擎 | 需要自定义玩法、改引擎逻辑 | 可控性最强,能深度调试 | 需要安装 Haxe/OpenFL 工具链,构建耗时 |
对于“vs 享利(但是Pico)”这种角色替换型 mod,用社区引擎直接加载是完全够用的。但作为 CSDN 读者,我建议至少把源码构建环境也准备一份。原因很实际:当你的 mod 出现问题,源码环境可以让你输出完整日志,还能在关键代码处加 trace 定位变量值。
3.2 最小环境清单
以下是经过社区验证的常见组合,版本号请以对应项目和引擎分支的 README 为准:
- 操作系统:Windows 10/11 或 macOS,开发机建议 8GB 以上内存。
- Git:用于拉取 FNF 源码和模组模板。
- Haxe:FNF 源码的核心语言,安装后能用
haxelib管理库。 - OpenFL 与 Lime:Haxe 下的跨平台多媒体框架,负责窗口、图形、音频。
- 文本编辑器:VS Code 或任何能高亮 Haxe/JSON 的编辑器。
- 音频工具:Audacity 或类似工具,用于检查和裁剪 OGG 音轨。
如果你选择源码构建,安装完 Haxe 后,在终端里执行:
haxelib install openfl lime haxelib install flixel haxelib install flixel-addons haxelib install flixel-ui然后进入项目目录:
git clone <FNF官方或社区引擎仓库地址> cd 项目目录 lime setup lime test windows如果一切顺利,系统会打开一个可运行的 FNF 窗口。第一次构建时间较长,因为需要下载依赖和编译资源,请耐心等待,不要在中途强制关闭。
3.3 目录规划建议
在开始动手前,建议把工作目录分成三块:
FNF-Project/ ├── engine/ # 引擎源码或社区引擎程序 ├── mods/ # 你正在开发的 mod 目录 └── tools/ # 音频、图片、谱面辅助工具这样可以避免把测试素材混进引擎源码,减少不必要的冲突。
4. 角色替换与谱面适配的核心流程拆解
4.1 准备 Pico 角色素材
Pico 是 FNF 社区非常熟悉的角色,如果你没有现成素材,通常可以在社区提供的角色包里获取。拿到素材后,必须确认三样东西齐全:
pico.png:角色全帧图集,通常是一张包含所有动画帧的长图或九宫格图。pico.xml:Sparrow 图集描述文件,记录每个子图名称和坐标。- 角色配置文件
pico.json:定义动画、缩放、血量图标、镜头位置等。
如果没有 XML,只有单独的帧图,你可以自己用工具生成图集和 XML。这里不推荐手工填坐标,容易出错。
4.2 建立模组目录并挂载
在引擎的 mods 目录下新建你的 mod 文件夹,例如MyPicoVsHenry,然后创建以下子目录:
mkdir -p MyPicoVsHenry/data/characters mkdir -p MyPicoVsHenry/data/songs/henry mkdir -p MyPicoVsHenry/images mkdir -p MyPicoVsHenry/songs/henry目录建好后,把pico.png和pico.xml放进images/,把歌曲音频放入songs/henry/,把谱面和角色配置放入data/对应位置。
4.3 编写角色配置 JSON
这一步是角色替换的关键。下面是一个符合常见引擎分支格式的pico.json简化示例,字段含义我已经写在注释中。注意 FNF 的 JSON 不支持注释,这里只是为了便于说明,实际文件里不能写这些注释:
{ "animations": [ { "name": "idle", "prefix": "Pico idle", "fps": 24, "loop": true }, { "name": "singLEFT", "prefix": "Pico left", "fps": 24, "loop": false }, { "name": "singDOWN", "prefix": "Pico down", "fps": 24, "loop": false }, { "name": "singUP", "prefix": "Pico up", "fps": 24, "loop": false }, { "name": "singRIGHT", "prefix": "Pico right", "fps": 24, "loop": false } ], "image": "pico", "scale": 0.85, "sing_duration": 3.85, "healthicon": "pico", "flip_x": false }animations[].name:引擎识别的动画名称。idle是空闲动画,四个sing是对应方向键的唱歌动画。prefix:动画在 XML 图集中的子图前缀。例如 XML 里子图名是Pico idle0000,前缀就是Pico idle。fps:动画帧率,FNF 通常使用 24。loop:是否循环播放。image:图集加载名,与images/pico.png对应。healthicon:血量图标文件名,通常位于 HUD 素材包中。
关于sing_duration:这个字段控制角色唱歌动画持续的时间,单位是毫秒、帧还是拍,不同引擎分支有不同解释。更稳妥的做法是参考你使用的引擎分支自带角色 json 里的数值。取一个与 BF 角色一致的值能减少踩坑。
4.4 准备谱面 JSON 并指定角色
如果你的 mod 是复用原“vs 享利”谱面,那么你需要把原 JSON 中song.player2改成"pico",同时检查song.notes里的 note 类型是否与 Pico 角色机制冲突。
谱面文件命名必须遵循引擎规则,通常是:
data/songs/henry/henry-hard.json文件名里的hard是难度标识,不能随意修改。如果引擎要求herry-hard.json和herry-hard-events.json成对出现,那你需要保留事件文件,否则某些谱面事件会丢失。
4.5 处理音轨对齐
角色替换完成不代表谱面能玩。如果“vs 享利”原版音轨里包含亨利的人声,而你换成 Pico,理论上应重新录制或混音 Pico 的 vocal 部分。如果暂时拿不到合适的音轨,可以暂时使用原音轨,但要在song.needsVoices等字段中保持原有取值,避免引擎误判。
音频格式必须转换为 OGG Vorbis,且采样率建议保持一致。转换工具推荐 Audacity,导出设置为 OGG 默认品质即可。如果音频和谱面时间轴对不上,进入游戏后会出现“音符到了但没有声音”或“音轨已经到下一段但谱面还在前一段”的体验错位。
5. 完整示例:让 Pico 以最小成本替换到谱面中
5.1 示例场景定义
我们用最小化方式实现这样一个目标:
- 原谱面:一首名为 “henry” 的 hard 难度歌曲。
- 角色配置:新增 Pico 角色,作为对手
player2。 - 效果:启动游戏后,可在自由游玩中选择该歌曲,对手显示为 Pico,并且按方向键时 Pico 会播放对应方向的动画。
5.2 文件结构总览
假设你的 mod 名为PicoHenry,最终文件结构如下:
mods/PicoHenry/ ├── pack.json ├── data/ │ ├── characters/ │ │ └── pico.json │ └── songs/ │ └── henry/ │ ├── henry-hard.json │ └── henry-hard-events.json ├── images/ │ ├── pico.png │ └── pico.xml └── songs/ └── henry/ ├── henry-hard.ogg └── henry-hard-inst.ogg5.3 pack.json
pack.json是模组的元数据文件,主要用于引擎识别模组名称、描述和优先级。简化写法如下:
{ "name": "PicoHenry", "description": "vs Henry but Pico", "priority": 10 }priority决定资源冲突时优先加载哪个模组,数字越大优先级越高。开发初期建议保持一个模组加载,避免多模组资源互相覆盖。
5.4 角色动画 XML 示例
Pico 的图集描述文件pico.xml是 Sparrow 格式。它把一张大图中的每个动画帧都划分出坐标和尺寸:
<TextureAtlas imagePath="pico.png"> <SubTexture name="Pico idle0000" x="0" y="0" width="96" height="96"/> <SubTexture name="Pico idle0001" x="96" y="0" width="96" height="96"/> <SubTexture name="Pico left0000" x="192" y="0" width="96" height="96"/> <SubTexture name="Pico left0001" x="288" y="0" width="96" height="96"/> <SubTexture name="Pico down0000" x="384" y="0" width="96" height="96"/> <SubTexture name="Pico down0001" x="480" y="0" width="96" height="96"/> <SubTexture name="Pico up0000" x="576" y="0" width="96" height="96"/> <SubTexture name="Pico up0001" x="672" y="0" width="96" height="96"/> <SubTexture name="Pico right0000" x="768" y="0" width="96" height="96"/> <SubTexture name="Pico right0001" x="864" y="0" width="96" height="96"/> </TextureAtlas>注意这里name必须和pico.json里的prefix一致。比如 JSON 里的singLEFT使用前缀Pico left,XML 中有Pico left0000,这样引擎才能把singLEFT动画解析成 XML 里的帧序列。
很多角色动画播放异常的 bug,根源都出在这个“前缀对不上”上。一旦 XML 里是Pico Left而 JSON 里是Pico left,大小写不同,引擎就会找不到帧。
5.5 谱面 JSON 角色字段示例
下面是一个最小可用的谱面片段,只展示角色和音符结构。实际谱面会有很多小节,请用你的源谱面替换:
{ "song": { "song": "henry", "bpm": 140, "speed": 2.5, "player1": "bf", "player2": "pico", "gf": "gf", "notes": [ { "sectionNotes": [ [0, 0, 0, ""], [250, 1, 0, ""], [500, 2, 0, ""], [750, 3, 0, ""] ] } ] } }sectionNotes中每一项数组的含义通常是:
- 第一个值:音符出现的毫秒时间点;
- 第二个值:轨道编号(0~3,对应左、下、上、右,具体以你的引擎按键映射为准);
- 第三个值:音符持续时间(0 表示普通短音符,大于 0 表示长条);
- 第四个值:音符类型,字符串通常为空。
如果你的原谱面是“vs 享利”的现成 chart,不需要手写这些数组,你只需要把player2改为"pico",然后重新保存 JSON。一定要用能校验 JSON 格式的编辑器保存,否则一个多余逗号就会导致加载失败。
5.6 HScript/Lua 可选扩展
如果引擎支持 HScript 或 Lua,你还可以在scripts/目录下写脚本来实现额外机制。比如 Pico 在被击中后进入特殊状态,或播放一段额外动画。下面是一个示意脚本,具体 API 取决于引擎分支:
function onUpdate(elapsed) if opponentName == 'pico' then health = math.max(health - 0.01 * elapsed, 0.2) end end这种脚本适合加防白给机制,但不适合零基础用户。建议先把无脚本版本跑通,再逐步加机制。
6. 运行结果与效果验证
6.1 启动测试
启动游戏后,进入自由游玩模式,选择你这首henry歌曲。启动命令在你的引擎仓库目录下执行:
lime test windows如果引擎直接支持 mods 加载,你也可以直接把编译好的游戏运行文件打开,在 mods 菜单里启用PicoHenry。
6.2 验证点清单
不要只看“能进入歌曲”就认为 mod 成功。建议按以下顺序检查:
- 对手是否显示为 Pico:进入歌曲后,右侧角色应是 Pico 而不是亨利。若显示默认 BF 或空模型,说明
player2字段没有生效。 - 动画是否随方向播放:对手角色在唱歌时,会随着音符方向播放
singLEFT、singDOWN等动画。如果一直播放 idle,检查角色 JSON 里的prefix是否和 XML 匹配。 - 血量图标是否正常:血量条中间显示的是 Pico 图标,而不是默认图标。如果显示问号,说明
healthicon引用的资源不存在。 - 音频踩点是否准确:音符接近判定线时,音轨中的鼓点和人声是否在同一拍。如果偏移明显,回到谱面 JSON 调整 BPM 或在编辑器里微调音符时间。
- 是否出现红色错误弹窗:如果引擎弹出类似
Error: Could not find image: pico的信息,说明图片加载路径不对。
6.3 开发模式下的日志定位
在源码构建环境下,终端控制台会实时输出 Haxe trace 和 Flixel 日志。当你的 mod 出现定位困难时,优先看终端,而不是反复重启游戏。日志里通常会给出具体缺失文件或字段名的提示。
如果使用现成引擎加载 mods,通常在游戏目录logs/下也能找到日志文件。定位问题时,先注意以下关键词:
Could not find:资源缺失。Invalid JSON:JSON 格式错误。Animation:动画匹配失败。Note:音符数据读取异常。
7. 常见问题与排查思路
这里把 FNF 角色替换类模组最常见的坑整理成一张表,遇到异常时可以直接对表排查:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模组列表里看不到你的 mod | mod 目录缺少pack.json,或 pack.json 格式错误 | 检查 mods 目录下文件夹是否包含pack.json | 补齐或修正 pack.json 字段 |
| 进入歌曲后角色显示为黑块/紫块 | 图集 PNG 未加载,或 XML 与 PNG 文件名不对应 | 看日志是否提示找不到 image | 确认images/下的文件命名与 json 的image字段一致 |
| 角色一直播放空闲动画 | 动画 prefix 与 XML 子图名称不一致 | 对比 json 的prefix和 xml 的name | 统一命名,注意大小写 |
| 角色唱歌时播放了错误方向的动画 | 动画列表顺序或轨道编号映射错误 | 检查singLEFT、singDOWN等 name 是否填反 | 按引擎源码或原版角色 json 对照修正 |
| 加载歌曲时弹 JSON 解析错误 | 文件里有多余逗号、中文引号或 BOM | 用 JSON 校验工具检查 | 删除多余逗号,使用英文引号,另存为 UTF-8 without BOM |
| 音符对不上音轨 | BPM 或音频长度不匹配 | 在 Audacity 中查看音频拍点,和 chart 的 BPM 对比 | 重新设置 chart BPM,或重新导出音频 |
| 长条音符无法命中 | note length 字段用法错误 | 检查引擎分支对长条音符的约定 | 使用原版 chart 中的长条片段比对格式 |
| 换角色后 BF 的动画也乱了 | 角色 json 同时被其他 mod 覆盖 | 检查 packs 优先级 | 调整priority,或一次只启用一个 mod |
每个问题背后,几乎都能追溯到某个“字段没对齐”。FNF 模组开发中,“字段对齐”比“代码逻辑”更值得重视,因为大多数引擎分支已经实现了完整逻辑,你只需要用正确的数据触发它。
8. 最佳实践与工程建议
8.1 素材命名统一
建议创建一套固定命名规则,例如所有角色文件都用小写英文,XML 里的子图名都采用角色名 动作名 + 帧号的格式,且帧号补足四位。这样无论是自己排查还是交给队友维护,都能减少认知负担。
8.2 先做最小模组再叠加功能
第一次开发时,只做一个包含“一首歌 + 一个角色”的最小模组,不要贪多。把最小链路跑通后,再逐步添加事件、脚本、多难度、故事模式。否则你很难分辨是角色配置问题还是谱面问题,调试会非常痛苦。
8.3 保留原版 chart 备份
在你把player2修改为"pico"之前,一定先复制一份原版 JSON 到备份目录。角色替换 mod 最忌讳直接覆盖原文件:一旦改错,你连恢复基准都没有。建议长期保存一个“未改动原版素材”目录,只读不写。
8.4 避免直接修改引擎源码
如果你的目标只是角色替换,不要一开始就改引擎源码。源码分支更新频繁,直接改源码会让你的模组无法跟随上游更新。优先用 mods 目录和脚本实现。只有当脚本 API 无法满足机制需求,才考虑 fork 引擎源码维护自己的分支。
8.5 注意素材授权
社区模组大量使用他人的角色素材。FNF 的角色、音轨和动画都有自己的版权归属,发布前记得检查素材授权。如果项目只用于个人学习,问题不大;如果要在社区公开分享,最好使用开源或明确注明允许二次创作的角色素材。
8.6 性能与体积控制
Pico 的图集如果包含大量高分辨率帧,会导致加载变慢和内存占用上升。建议在保证清晰度的前提下压缩 PNG,控制图集宽度为 2 的幂次方尺寸。音频也建议压低码率,因为一个模组如果包含多首歌曲,体积会迅速膨胀。
9. 总结与后续学习方向
现在你已经清楚,“fnf vs 享利(但是Pico)”这类 mod 的工程核心,是让 Pico 的角色动画、谱面 chart 和音频数据在引擎里对齐。你也能理解为什么一个看似简单“换角色”的 mod,会涉及data/characters、data/songs、images、songs等多个目录的协同。
如果你按本文流程跑通了一个最小模组,下一步建议按顺序深入三个方向:
- 谱面设计:学习 chart 编辑器,理解 BPM 变化、事件谱、长条、双人大按键等技术。
- 脚本机制:研究引擎分支的 HScript/Lua API,尝试给 Pico 设计专属技能或状态。
- 多难度与故事模式:把单个 hard 谱面扩展到 easy/normal/hard,并接入故事模式菜单,让 mod 更完整。
最后提醒一句:FNF 模组开发天然是“改一点、测一点”的过程,不要试图一次完成所有功能。每次改动画、改谱面、改音频后都运行一次,确认上一个环节没有破坏,再进入下一环。这样你的“但是 Pico” 版本不仅跑得通,还会成为别人愿意下载的完整模组。