大家好,我最近在做《Friday Night Funkin》(也就是大家常说的《周五夜放克》)同人内容时,刷到了一条非常有意思的创作标题:“pico和岳父争抢BF”,配上了 #FNF#pico#FNFsoft#周五夜放克 这几个标签。稍微研究了一下才发现,这类“多角色同台、互相争夺主角”的剧情模组,已经是FNF社区里非常热门的一种玩法方向。今天这篇内容,我就围绕这一类模组的构建方式,从剧情设定、文件结构、角色配置到歌曲关卡制作,完整拆一遍实现思路。
我尽量把文章写成一套可以直接动手参考的教程,而不是单纯介绍某个视频或某个作品。如果你之前玩过FNF但没做过模组,这篇文章能帮你理清模组文件长什么样、每个配置项是干什么用的;如果你已经会做基础关卡,也可以从“双角色争夺”这个综合案例里,找找交互设计和谱面编排的灵感。话不多说,我们直接开始。
1. 从“争抢BF”的剧情切入:FNF 与 FNFsoft 模组基础
1.1 先认识《周五夜放克》的基本玩法
很多朋友第一次看到FNF,会误以为它是简单的音游,其实它的核心机制更接近“节奏对战”。玩家扮演男主角 BF(Boyfriend),在女友和女友父亲的注视下,用歌唱和节拍对抗一个个对手。每一关就是一首歌,按照箭头提示在正确节拍按下方向键,唱得越准,对手就越接不住。
FNF 能火成一种文化现象,并不只是因为玩法简单,而是因为角色辨识度极高、美术风格统一、社区扩展性做得非常好。游戏里的每个角色都有固定的待机动作、歌唱动作、受伤动作和失败动作。在此基础上做同人模组,制作门槛比想象中低很多。
1.2 Pico 和岳父(Daddy Dearest)到底是谁
如果对剧情背景不熟悉,第一次看到“pico和岳父争抢BF”这个标题,可能会有点困惑。这里整理一下角色关系:
- BF(Boyfriend):玩家控制的男主角,标志性动作是歪嘴笑和不停摇晃身体。
- 岳父(Daddy Dearest):女朋友的父亲,在游戏第一周的剧情里作为第一个主要对手登场,性格严厉,总想拆散女儿和BF。
- Pico:BF的朋友,在额外周目中登场,名气同样不小,设定上是个比较硬核的角色。
所以“岳父”并不是一个随便称呼的角色,而是Daddy Dearest在国内玩家圈里的通用叫法。“争抢BF”则是同人创作者临时设定的剧情冲突:两位角色在同台演出的情况下,都试图在歌曲节奏中压制或争夺BF。这种多角色互相对抗的设定,刚好为模组开发提供了一个清晰的剧情骨架。
1.3 FNFsoft 代表什么
“FNFsoft”在FNF社区里并不是唯一的官方机构,而是一种创作团队/作品系列的常见署名方式。很多中文玩家会在模组作品标题里加上 FNFsoft 或类似字样,用来表示这个内容是自己或小团队制作的同人模组集合。
在这类作品中,创作者通常不只是放一首歌,而是会把剧情对话、角色切换、场景特效一起打包,形成更接近“微型游戏章节”的体验。我们今天要做的“争抢BF”模组,也可以理解成一次 FNFsoft 风格的轻量级剧情关卡开发。
1.4 为什么用“争夺”桥段做模组最合适
多角色同台战斗在FNF模组里非常讨喜,主要有三个原因:
- 视觉冲击力强:同一个舞台上两个对手轮流出现,观众在短时间内能看到更多角色动作演出。
- 谱面变化丰富:不同角色拥有不同的音符轨道策略,交互感比单一对手更足。
- 剧情容易理解:不需要复杂文字说明,玩家通过“谁在上一段压制、谁在下一段反击”就能感知剧情冲突。
对第一次做模组的朋友来说,这种渐进式多角色关卡,也比一口气做四个角色的完整周目更合理,因为大部分代码逻辑可以复用。
2. 环境准备与版本说明
2.1 模组开发前要准备什么
做FNF模组,并不需要从零写一个引擎,绝大多数开发工作是在已有游戏框架里补充资源文件。你需要准备的大致如下:
- 一台能跑FNF的电脑,Windows、macOS、Linux都可以,配置要求不高。
- 原版游戏或开源引擎:推荐使用社区最常用的Psych Engine分支,它对模组支持和配置项说明比较友好。
- 代码编辑器:VS Code、Sublime Text、Notepad++ 都行,主要用来编辑 JSON 和 Lua 脚本。
- 音频处理工具:Audacity 是免费且好用的选择,用来裁剪歌曲伴奏和录制人声部分。
- 图片处理工具:Aseprite 或 Photoshop 都可以,用来制作角色贴图,不会画图的话可以直接使用现有开源模组素材。
这里有一个需要说明的点:不同版本的Psych Engine,其目录命名和配置项会有细微差别。本文的示例以常见的 0.6.x/0.7.x 版本结构为准,重点演示配置思路,你实际使用时一定要以自己下载的引擎文档为准。
2.2 FNF 模组的基本目录结构
如果下载的是包含 mods 目录的引擎版本,模组通常放在:
mods/ └── MyFirstMod/ ├── data/ ├── images/ ├── songs/ └── characters/每一层目录的作用大致是:
| 目录 | 作用 |
|---|---|
| data | 存放剧情、周目、场景和歌曲的 JSON 配置文件 |
| images | 存放角色贴图、UI贴图、背景贴图等资源 |
| songs | 存放歌曲的伴奏和人声音频 |
| characters | 角色定义 JSON,描述动画帧、出场位置和血量图标 |
初学时最容易犯的错误,是把所有文件直接堆在 mods 根目录下,结果游戏识别不到。正确做法是每个模组独立文件夹,文件夹名称尽量使用英文或拼音,避免中文路径引起编码问题。
2.3 开发流程概览
一个模组从空文件夹到能运行,大致经历这些步骤:
- 创建模组文件夹,搭建目录骨架。
- 准备歌曲音频,并切割成伴奏和人声两部分。
- 编写角色配置文件,绑定贴图和动画。
- 编写歌曲配置文件,录入谱面数据。
- 编写剧情脚本,让角色对话按时间触发。
- 启动游戏,进入调试模式测试。
后面几个小节,我会把每个步骤的核心内容拆开讲。
3. 模组文件结构与核心配置拆解
3.1 角色 JSON:每个角色都是一个对象
在Psych Engine等主流模组引擎里,一个角色通常对应一个JSON文件,里面记录图片资源、动画列表、坐标位置、缩放比例和血量图标。下面是一个简化的Pico角色配置示例,放在characters/pico.json:
{ "animations": [ { "anim": "idle", "prefix": "idle", "fps": 24, "loop": true }, { "anim": "singLEFT", "prefix": "singLEFT", "fps": 24, "loop": false }, { "anim": "singDOWN", "prefix": "singDOWN", "fps": 24, "loop": false }, { "anim": "singUP", "prefix": "singUP", "fps": 24, "loop": false }, { "anim": "singRIGHT", "prefix": "singRIGHT", "fps": 24, "loop": false } ], "image": "pico", "scale": 1.0, "position": [0, 0], "healthicon": "pico", "flip_x": false }这里有几个字段需要解释:
animations:定义角色所有动画的数组,每个动画由anim名称、prefix贴图前缀、fps播放速度和loop循环开关组成。image:对应images目录下的贴图名。position:角色在舞台上的坐标,通常[x, y]形式。healthicon:血量图标,可以在UI里显示。
如果你要让岳父(Daddy Dearest)出场,只需要把image换成岳父贴图名,并定义好他的sing系列动画即可。从代码角度看,新增一个角色其实就是复制JSON、换贴图、改动画名。
3.2 歌曲 JSON:谱面的信息来源
歌曲配置是整个关卡最核心的部分。FNF的歌曲JSON一般包含歌曲名称、BPM、速度、双方角色标识,以及编排好的sections段。下面是一个简化示例,放在songs/songName/songName.json:
{ "song": { "song": "争抢BF", "bpm": 120, "speed": 1, "player1": "bf", "player2": "dad", "notes": [], "sections": [ { "bpm": 120, "mustHitSection": false, "sectionNotes": [ [0, 0, 0, 0], [140, 1, 0, 0], [280, 2, 0, 0] ] }, { "bpm": 120, "mustHitSection": true, "sectionNotes": [ [0, 3, 0, 0], [140, 0, 0, 0] ] } ] } }sectionNotes里的每个数组,通常表示[时间, 轨道, 长度, 类型]。轨道0、1、2、3分别对应左、下、上、右四个音符。mustHitSection字段用来控制这一段是玩家操作还是对手操作。
音乐制作人在做谱面时,一般会先用工具打开音频波形,把时间点对齐,再手动写入JSON。对新手来说,使用社区提供的谱面编辑器或直接参考现有模组文件,是最快的学习方式。
3.3 周目 JSON:把歌曲串成章节
剧情模组通常不止一首歌。“争抢BF”这类内容,可以设计成三首曲目的小周目:
- 第一首:岳父先盯上BF,双人对峙。
- 第二首:Pico加入,形成三方混战。
- 第三首:两人轮流上阵,争夺BF。
周目配置写在data/weeks/Week_MyMod.json里,主要作用是告诉游戏周目包含哪些歌曲、用什么背景和难度。简化结构如下:
{ "name": "Pico和岳父争抢BF", "songs": [ "对峙", "三方混战", "最终争夺" ], "weekCharacters": ["dad", "pico", "bf"], "difficulties": ["easy", "normal", "hard"] }周目文件的好处是,玩家可以在选曲界面看到完整章节,而不是只能通过调试模式进入歌曲,体验更完整。
3.4 Lua 脚本:控制剧情与特效
JSON负责“静态定义”,而剧情对话、镜头切换、角色出场退场和特效触发,通常由 Lua 脚本完成。Psych Engine 支持在歌曲加载时运行脚本,常用于播放对话、移动角色位置或改变舞台状态。
下面是示意脚本,存放在歌曲目录或脚本目录,重点展示结构:
function onCreate() -- 在歌曲开始时设置剧情 startDialogue("dialogue2", "dialogue") end function onStepHit() -- 第 500 步时让Pico移动到指定位置 if curStep == 500 then setProperty('pico.x', 400) setProperty('pico.y', 350) end end function onUpdate(elapsed) -- 可以在这里做持续动画或特效处理 endLua脚本的API和引擎版本关系很大,不同版本之间的函数名可能有所差异。写脚本时不要死记硬背,多查当前引擎版本自带的示例脚本就好。
4. 完整实战:“Pico和岳父争抢BF”综合关卡
4.1 剧情脚本设计
我在做这类剧情模组时,会先把整个关卡的流程写成时间线,而不是直接写代码。下面是“争抢BF”关卡的简易时间线示例:
| 时间/节拍 | 事件 |
|---|---|
| 前奏 | 岳父站在舞台左侧,Pico站在右侧,BF在中间 |
| 第1小节 | 岳父开口宣战,Pico表示也要跳 |
| 第10小节 | 岳父先攻,玩家防御 |
| 第20小节 | Pico插话,岳父后退一步 |
| 第30小节 | Pico进攻,岳父在后台观看 |
| 第50小节 | 两人同时向BF方向靠近,进入高潮段 |
| 第70小节 | BF反击,节奏加快 |
| 结尾 | 两人争论到底谁赢了,BF趁乱跑走 |
这个时间线的好处是,每一件剧情事件都能对应到Lua脚本里的onStepHit或onBeatHit,实现起来非常直观。你不需要一开始就把细节做得多花哨,先保证剧情事件和音乐节拍能对得上。
4.2 创建项目结构
根据上面的设计,在mods目录下创建你的模组文件夹:
mods/ └── BFWarMod/ ├── characters/ │ ├── pico.json │ └── dad.json ├── data/ │ └── weeks/ │ └── Week_BFWar.json ├── images/ │ ├── pico/ │ ├── dad/ │ └── stage/ ├── songs/ │ ├── dueling/ │ │ ├── dueling.json │ │ ├── dueling.wav │ │ └── duelingVoices.wav │ ├── threeWay/ │ │ └── ... │ └── finalWar/ │ └── ... └── lua/ └── main.lua这里我特意使用了英文文件夹名,因为很多版本的FNF对中文文件名支持不够稳定。角色贴图和音频文件,最好也统一使用小写英文或数字命名,不要混用中文和特殊符号。
4.3 编写角色和舞台配置
接下来是最基础的角色配置。假设images/dad文件夹里放的是岳父的贴图序列,那么characters/dad.json可以参考下面的写法:
{ "animations": [ { "anim": "idle", "prefix": "dad idle", "fps": 24, "loop": true }, { "anim": "singLEFT", "prefix": "dad left", "fps": 24, "loop": false }, { "anim": "singDOWN", "prefix": "dad down", "fps": 24, "loop": false }, { "anim": "singUP", "prefix": "dad up", "fps": 24, "loop": false }, { "anim": "singRIGHT", "prefix": "dad right", "fps": 24, "loop": false } ], "image": "dad", "scale": 0.9, "position": [-100, 100], "healthicon": "dad", "flip_x": false }这里想提醒的是,不同作者制作贴图时使用的prefix命名习惯不一样,一定要检查贴图序列帧的真实名称。设置动画前缀时,要确保与图片资源里的实际帧名一致,否则角色会出现无法播放动画的问题。
Pico的配置类似,只是把图片名、坐标和动画前缀替换成Pico的素材即可。如果想让Pico站在舞台右边,可以直接修改position字段:
{ "image": "pico", "scale": 1.0, "position": [450, 120], "flip_x": true }4.4 编写歌曲配置与谱面
角色配置好之后,再来处理歌曲。这里我把第一首“对峙”歌曲的JSON写成一个可参考的简化版本。
文件路径:songs/dueling/dueling.json
{ "song": { "song": "dueling", "bpm": 128, "speed": 1.2, "player1": "bf", "player2": "dad", "notes": [], "sections": [ { "bpm": 128, "mustHitSection": false, "sectionNotes": [ [0, 0, 0, 0], [140, 2, 0, 0], [280, 1, 0, 0], [420, 3, 0, 0] ] }, { "bpm": 128, "mustHitSection": true, "sectionNotes": [ [0, 1, 0, 0], [180, 0, 0, 0], [320, 2, 0, 0] ] } ] } }做谱面时,最忌讳直接凭感觉写时间数字,因为后期很容易出现音符和音乐节拍脱节的问题。更靠谱的做法是,打开音频波形图,通过视觉定位BPM和段落起点,再逐步填入时间。
如果完全没有谱面编排经验,建议先导入现成歌曲模板,把原曲音符全部清除后,再在编辑器里重新拖拽音符。虽然前期麻烦一些,但比手动算时间要稳定得多。
4.5 编写剧情脚本
为了让岳父和Pico看起来真的在“争夺”BF,剧情脚本需要做到两件事:控制角色位置、按节拍切换表演者。
下面是一段简化到只剩核心逻辑的Lua脚本思路,文件路径:lua/main.lua
function onCreate() -- 初始位置设定 setProperty('dad.x', 100) setProperty('dad.y', 120) setProperty('pico.x', 500) setProperty('pico.y', 120) end function onStepHit() if curStep == 64 then -- 第64步:岳父前进一步 setProperty('dad.x', 200) elseif curStep == 128 then -- 第128步:Pico前进一步 setProperty('pico.x', 400) elseif curStep == 192 then -- 第192步:两人同时靠近BF setProperty('dad.x', 250) setProperty('pico.x', 320) end end function onSongStart() -- 歌曲开始时播放对话,dialogue2是对话配置文件 startDialogue("dialogue2", "dialogue") end脚本里使用了setProperty修改角色位置,这是常见引擎里相对通用的做法。不同版本对Lua函数命名可能有所变化,如果你发现setProperty不生效,就去查当前引擎自带的脚本API文档,换成对应版本提供的方法。
4.6 运行与验证
把上述文件放入mods/BFWarMod目录后,启动游戏,在选项里开启调试模式,然后直接选择加载对应歌曲。如果配置正确,你会看到:
- 角色正常出现在舞台左右两方。
- 音符按设定时间下落。
- 剧情对话在歌曲开始后触发。
- 角色位置在指定节拍改变。
如果角色没有出现,优先检查image字段对应的图片文件夹名是否存在,或者动画prefix是否和贴图帧名完全一致。这类问题占模组制作前期报错的一半以上。
5. 常见问题与排查思路
5.1 角色不显示或显示为黄色方块
这是新手最容易遇到的问题,原因通常是贴图没有正确加载。
排查思路:
- 确认
images目录下存在角色图片文件夹,且大小写与JSON中的image字段一致。 - 确认贴图没有被多层级目录嵌套,应该是
images/dad/dad.png,而不是images/characters/dad.png。 - 确认动画
prefix与图片序列帧名称匹配。 - 查看游戏日志,日志里会明确指出缺失的文件路径。
5.2 歌曲加载失败
歌曲加载失败通常和文件名或音频格式有关。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 歌曲列表不显示 | 缺少周目配置或歌曲目录名不对 | 检查data/weeks和songs目录结构 |
| 有歌名但没有声音 | 音频文件命名不符合引擎规范 | 确认使用.ogg或.wav,且文件名和JSON一致 |
| 有人声没伴奏 | 伴奏文件播放路径错误 | 查Inst和Voices文件名是否精确匹配 |
5.3 对话不触发
对话系统依赖独立的对话配置文件,如果startDialogue("dialogue2", "dialogue")里的第一个参数对应的JSON文件不存在,对话自然不会弹出。检查data/dialogue2是否存在,同时确认对话文本使用UTF-8编码保存,避免中文乱码。
5.4 角色位置不对
不同版本引擎的标准舞台分辨率不同,位置偏移需要手动调试。调试时可以把角色坐标直接写入Lua脚本,利用onUpdate实时打印当前位置,反复调整数值,直到视觉效果满意。
6. 最佳实践与工程建议
6.1 文件命名统一
做模组最怕文件杂乱。我的建议是,所有资源文件统一采用小写英文命名,多个单词用下划线连接。例如:
pico_idle.png dad_sing_left.png song_dueling.ogg这样可以避免文件系统大小写敏感导致的问题,也方便以后打包分享。
6.2 做好版本管理
模组开发同样需要版本管理。哪怕只是自己一个人做,也建议使用Git管理整个模组文件夹。每次大改动前打一个标签,比如v0.1-basic-stage、v0.2-add-script,这样哪个改动引入报错时,可以快速回退。
6.3 谱面备份与自动化
谱面数据是模组里最耗时的部分,建议在上传测试前对歌曲JSON做备份。部分模组工具支持谱面导入导出,可以利用这些工具生成可读性更高的中间格式。若依赖手动JSON编辑,可以在脚本里写一个小工具,把时间点批量偏移,方便调整整段音符的提前量。
6.4 素材版权与发布规范
FNF官方和社区对素材再发布有明确规定。如果你要分享模组,请确认:
- 使用的贴图、音频素材允许用于非商业模组。
- 在模组说明里注明素材来源和原作者。
- 不要直接将收费素材或未授权素材打包进模组。
虽然是同人创作,但版权规范依然重要,良好的署名习惯也能让你的作品更容易被社区接受。
6.5 安全与测试环境
如果你下载的是别人打包好的引擎,建议先杀毒扫描再运行。在修改重要文件之前,复制一份原版项目备份。测试Lua脚本时,先在小范围内验证,避免因脚本错误导致游戏崩溃或卡死,不要直接在生产环境中反复尝试高风险操作。
7. 后续学习路线与进阶方向
做完“Pico和岳父争抢BF”这个基础关卡后,你可以继续往这几个方向深入:
7.1 学习 HaxeFlixel 基础
虽然大多数模组可以直接通过JSON和Lua完成,但如果想高度定制游戏行为,就需要读懂FNF底层代码。FNF基于Haxe和Flixel框架开发,学习Haxe基础语法、Flixel的状态机与对象系统,会帮助你理解角色切换和状态控制的本质。
7.2 制作高质量贴图与动画
模组想要吸引人,美术表现力非常重要。建议研究FNF官方的动画帧规律:角色通常由大量PNG序列帧组成,使用Aseprite之类的工具制作时,注意帧大小统一、锚点位置一致,避免游戏内角色抖动。
7.3 研究多角色同台交互方案
“争抢”类模组的进阶玩法,不只是两个角色轮流出现,还可以加入:
- 中途换人机制,同一小节里对手从岳父切换成Pico。
- 双人同时攻击,左右两侧箭头混合下落。
- 血量条变化与角色抬升动作联动。
这些交互效果可以通过Lua脚本和谱面事件配合实现,每一步都要回到歌曲配置里验证事件点是否准确。
7.4 发布到社区并收集反馈
模组做完后,发布到FNF模组社区或视频平台,观察玩家在评论区提到的按键手感、音符密集度和视觉卡顿问题。社区反馈是新手进步最快的方式,不要怕作品不成熟,先发布一版,再根据反馈迭代。
结语
“Pico和岳父争抢BF”这个标题初看只是有趣的同人梗,但真正做起来,它就是一套完整的FNF模组开发流程:剧情设计、角色JSON配置、歌曲谱面制作、Lua脚本控制和周目整合。如果你手头已经有FNF引擎,试着像我上面这样搭建一个简单的双角色关卡,把岳父和Pico放到同一个舞台上,让音符从两侧同时或轮流落下,感受一下“争夺”的节奏张力。等你把第一首曲子跑通,后续再做任何复杂剧情模组,都会有更清醒的框架意识。
希望这篇教程能给你带来一些实际的帮助,祝你顺利做出属于自己的第一个FNF模组。