从零开始:基于HaxeFlixel与Qt的FNF模组开发与周目重写实战
2026/8/19 7:54:16 网站建设 项目流程

在实际游戏开发或二次创作中,为《Friday Night Funkin'》(FNF)制作模组是一个结合了创意、美术和编程的综合项目。其中,“QT”通常指代Qt框架,一个用于开发跨平台应用程序的C++库,而“pico”可能指代游戏中的角色Pico,或是硬件平台树莓派Pico。将“Rewrite pico 周”理解为对FNF游戏中以角色Pico为主题的某个周目(Week)进行重写或复刻,是一个典型的模组开发场景。这个过程不仅涉及对游戏原有逻辑的理解,还需要使用合适的工具链来修改资源、编写脚本甚至重写部分游戏机制。

本文将以一个开发者的视角,带你从零开始理解如何为一个类似FNF的游戏框架(使用HaxeFlixel引擎)创建或修改一个角色周目,并探讨如何利用Qt这类工具进行辅助开发或资源管理。虽然FNF原版并非使用Qt开发,但Qt在创建关卡编辑器、资源打包工具或性能分析器等方面,可以作为强大的辅助工具链的一部分。我们将聚焦于技术实现路径,涵盖环境准备、项目结构解析、关键代码修改、资源替换以及使用外部工具进行辅助开发的具体步骤。无论你是想深入学习Haxe和HaxeFlixel引擎,还是希望了解如何系统化地进行游戏模组开发,这篇文章都将提供一条清晰的实践路线。

1. 理解FNF模组开发的技术栈与核心概念

在动手改写一个“周目”之前,必须清楚原始游戏的技术构成。FNF原版游戏使用Haxe编程语言和HaxeFlixel游戏框架开发。Haxe可以编译到多个目标平台(如Windows、Mac、HTML5等),而HaxeFlixel则是基于Flixel的2D游戏引擎。

一个“周目”通常包含以下几个核心部分:

  • 对话脚本 (Dialogue): 角色在歌曲开始前和结束后的对话内容,通常是JSON或TXT格式。
  • 周目数据 (Week Data): 定义该周目包含哪些歌曲、角色、对话文件等元数据。
  • 歌曲数据 (Song Chart): 定义音符下落的时间、类型和轨道,通常是.json文件。
  • 角色精灵图 (Character Sprites): 角色在不同状态(闲置、演唱、失败等)下的动画帧。
  • 背景与舞台 (Stage): 周目专属的背景图片、前景元素以及舞台布局脚本。
  • 音乐与音效 (Music/Sound): 歌曲的伴奏(Inst)和人声(Voices)音频文件。

“重写”一个周目,意味着你可能需要替换以上全部或部分内容。而“QT”在这个语境下,更可能是指使用Qt框架来制作一个辅助工具,例如:

  1. 关卡(音符)编辑器: 可视化地创建和编辑.json格式的音符谱面。
  2. 对话编辑器: 编辑对话脚本,并生成游戏所需的格式。
  3. 资源管理/打包工具: 管理大量的图片、音频资源,并可能进行压缩或打包。
  4. 调试工具: 实时查看游戏状态、内存使用或日志输出。

理解了这个分工,我们就可以将任务拆解为两部分:游戏本体的模组开发可选的外部工具链开发

2. 环境准备与依赖配置

要开始FNF模组开发,你需要搭建Haxe开发环境并获取游戏源代码。

2.1 搭建Haxe开发环境

  1. 安装Haxe:

    • 访问 Haxe官网 下载并安装最新稳定版Haxe。
    • 安装完成后,打开命令行(终端/PowerShell),运行haxe --version验证安装。
  2. 安装Haxelib(Haxe的包管理器):

    • Haxe安装包通常包含Haxelib。运行haxelib --version验证。
    • 通过Haxelib安装FNF开发所需的核心库:
      haxelib install lime haxelib install openfl haxelib install flixel haxelib install flixel-tools
    • 安装完成后,使用haxelib run flixel-tools setup进行Flixel工具链的初始设置。
  3. 安装集成开发环境(IDE):

    • Visual Studio Code是推荐选择。安装VSCode后,从扩展市场搜索并安装Haxe Extension Pack,它提供了代码补全、语法高亮和调试支持。

2.2 获取FNF引擎源代码

你通常不会直接修改原版FNF,而是基于一个社区维护的、更易于模组开发的引擎分支,例如Psych EngineKade Engine。这里以Psych Engine为例:

  1. 克隆源代码:
    git clone https://github.com/ShadowMario/FNF-PsychEngine cd FNF-PsychEngine
  2. 安装项目特定依赖:
    • 进入项目根目录,运行haxelib install命令(如果项目提供了setup.ps1setup.sh,则运行对应脚本)。
    • 这个步骤会读取项目根目录的haxelib.jsonProject.xml文件,安装所有指定的库依赖。

2.3 (可选)搭建Qt开发环境

如果你计划开发辅助工具,需要安装Qt。

  1. 下载Qt:
    • 访问 Qt官网 ,下载Qt Online Installer
    • 运行安装程序,在组件选择页面,至少勾选:
      • 最新稳定版的Qt(如 Qt 6.5.x)
      • 对应版本的Qt Creator(集成开发环境)
      • 对应编译器的开发套件 (如MSVC 2019 64-bit用于Windows)
  2. 验证安装:
    • 打开Qt Creator,创建一个默认的Qt Widgets Application项目,编译并运行,确保环境正常。

3. 项目结构解析与“Pico周”定位

以Psych Engine为例,其目录结构清晰地分离了代码和资源:

FNF-PsychEngine/ ├── source/ # Haxe源代码 │ ├── WeekData.hx # 定义所有周目的数据 │ ├── PlayState.hx # 游戏主逻辑,包括歌曲加载、音符生成 │ └── ... # 其他核心类 ├── assets/ # 所有游戏资源 │ ├── data/ # 歌曲数据、周目数据、对话 │ │ ├── pico/ # (示例) Pico周的数据可能在这里 │ │ │ ├── pico.json # 歌曲音符谱面 │ │ │ └── events.json # 特殊事件 │ ├── images/ # 图片资源 │ │ └── characters/ # 角色精灵图 │ │ └── Pico.json # Pico的动画描述文件 │ │ └── Pico.png # Pico的精灵图表 │ ├── music/ # 音乐文件 │ └── sounds/ # 音效文件 └── mods/ # 模组目录(社区标准) └── YourModName/ # 你的模组文件夹,结构与assets/类似

要重写“Pico周”,你需要找到原版Pico周对应的资源。通常需要查看source/WeekData.hx

// 在WeekData.hx中寻找类似结构 public static var weekData:Array<Dynamic> = [ { 'name': 'Pico', // 周目名称 'songs': ['Philly', 'Blammed', 'Fresh'], // 歌曲列表 'characters': ['pico', 'bf', 'gf'], // 每周角色顺序 'weekBackground': 'philly', // 背景 'weekBefore': 'week2', 'storyName': 'PICO', 'difficulties': '' // 难度 }, // ... 其他周目 ];

这段代码定义了周目的元数据。歌曲‘Philly’对应assets/data/philly/目录下的谱面文件。角色‘pico’对应assets/images/characters/Pico.jsonPico.png

4. 核心修改步骤:重写一个周目

假设我们要创建一个名为“Pico Rewrite Week”的新周目,包含一首新歌‘Revenge’

4.1 创建新的周目数据

source/WeekData.hxweekData数组末尾添加你的新周目:

{ 'name': 'Pico Rewrite', 'songs': ['Revenge'], 'characters': ['pico', 'bf', 'gf'], 'weekBackground': 'schoolEvil', // 使用一个现有的或自定义背景 'weekBefore': 'week3', // 在哪个周目之后解锁 'storyName': 'PICO\'S REVENGE', 'difficulties': 'Hard' // 默认难度 }

4.2 创建歌曲谱面数据

assets/data/下创建文件夹revenge/,并创建谱面文件revenge.json。这是一个高度简化的示例:

{ "song": { "song": "Revenge", "notes": [ { "sectionNotes": [ [0, 2, 0], // [时间戳(毫秒), 轨道(0-3), 音符类型(0=普通)] [500, 0, 0], [1000, 3, 0] ], "lengthInSteps": 16, "typeOfSection": 0, "mustHitSection": true } // ... 更多小节 ], "bpm": 150, "needsVoices": true, "player1": "bf", "player2": "pico", "speed": 2.5 } }
  • sectionNotes: 每个数组定义了一个音符。[0, 2, 0]表示在0毫秒时,在轨道2(从0开始计数)出现一个普通音符。
  • mustHitSection: 为true时,该小节是玩家(BF)需要演唱的部分。
  • speed: 音符下落的速度。

4.3 添加音乐文件

将你的伴奏文件Revenge_Inst.ogg和人声文件Revenge_Voices.ogg放入assets/songs/revenge/文件夹。引擎会自动根据歌曲名加载。

4.4 (可选)修改或创建角色精灵图

如果你想修改Pico的外观,需要编辑assets/images/characters/Pico.json和对应的Pico.png文件。.json文件定义了动画帧的矩形区域和帧率:

{ "animations": [ { "name": "idle", // 动画名称 "prefix": "pico idle dance", // 精灵图前缀(如果有多张图) "offsets": [0, 0], // 偏移量 "indices": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] // 使用的帧序列 }, { "name": "singLEFT", "prefix": "pico left note", "offsets": [30, -10], "indices": [0, 1, 2, 3] } // ... 其他动画如 singRIGHT, singUP, singDOWN, hey, scared ], "global_offsets": [0, 0], // 全局偏移 "scale": 1.0 // 缩放比例 }

你需要确保Pico.png精灵图表中的图片排列与这个索引定义相匹配。修改图片后,可能需要调整offsets以使角色在舞台上对齐。

4.5 编译与测试

  1. 编译项目: 在项目根目录打开命令行,根据你的目标平台运行编译命令。例如,编译Windows版本:
    lime build windows
    或者,如果你想快速测试,可以编译到HTML5并在浏览器中运行:
    lime build html5 lime run html5
  2. 在游戏中测试: 运行编译后的游戏,进入“故事模式”,你应该能看到新添加的“Pico Rewrite”周目。选择并游玩,检查音符时机、音乐同步、角色动画是否正确。

5. 使用Qt开发辅助工具:谱面编辑器示例

手动编写JSON谱面极其繁琐且容易出错。我们可以用Qt创建一个简单的可视化谱面编辑器。这个工具的核心功能是:将鼠标点击转换为时间戳和轨道,并生成对应的JSON数据

5.1 Qt项目设置

  1. 在Qt Creator中创建新的Qt Widgets Application项目。
  2. 在项目文件.pro中添加必要的模块,例如core gui

5.2 核心界面与逻辑设计

我们将设计一个主窗口,包含一个用于模拟音符轨道的绘图区域,和一个用于显示/编辑JSON的文本区域。

mainwindow.h关键部分:

#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QList> #include <QPointF> QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); protected: void mousePressEvent(QMouseEvent *event) override; void paintEvent(QPaintEvent *event) override; private slots: void on_generateJsonButton_clicked(); void on_clearButton_clicked(); private: Ui::MainWindow *ui; QList<QPointF> notes; // 存储音符:x坐标代表时间(缩放后),y坐标代表轨道(0-3) float currentTime = 0.0f; // 当前时间游标 float bpm = 150.0f; int beatsPerMeasure = 4; // ... 其他成员变量和辅助函数 }; #endif // MAINWINDOW_H

mainwindow.cpp关键部分:

#include "mainwindow.h" #include "ui_mainwindow.h" #include <QMouseEvent> #include <QPainter> #include <QJsonDocument> #include <QJsonObject> #include <QJsonArray> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // 初始化UI,例如连接按钮信号到槽 } void MainWindow::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { QPointF pos = event->position(); // 假设绘图区域左上角为(0,0),宽度代表时间,高度平均分为4个轨道 QRectF canvas = ui->canvasWidget->geometry(); // 需要一个自定义的绘图Widget if (canvas.contains(pos)) { // 计算相对位置 float time = (pos.x() - canvas.left()) / canvas.width() * 60000 / bpm * beatsPerMeasure; // 转换为毫秒 int lane = static_cast<int>((pos.y() - canvas.top()) / canvas.height() * 4); lane = qBound(0, lane, 3); // 限制在0-3轨道 notes.append(QPointF(time, lane)); update(); // 触发重绘,显示新音符 ui->statusBar->showMessage(QString("添加音符: 时间=%1ms, 轨道=%2").arg(time).arg(lane)); } } } void MainWindow::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); // 1. 绘制背景和轨道线 // 2. 遍历notes列表,根据时间和轨道位置绘制矩形或圆形代表音符 for (const QPointF &note : notes) { float x = mapTimeToX(note.x()); // 实现一个将毫秒映射到X坐标的函数 float y = mapLaneToY(note.y()); // 实现一个将轨道映射到Y坐标的函数 painter.drawEllipse(QPointF(x, y), 5, 5); // 绘制一个圆点 } // 3. 绘制当前时间线 } void MainWindow::on_generateJsonButton_clicked() { QJsonObject songJson; QJsonObject songDetails; songDetails.insert("song", ui->songNameEdit->text()); songDetails.insert("bpm", bpm); // ... 设置其他歌曲属性 QJsonArray sectionsArray; // 将notes列表按时间排序并分组到不同的小节(section) // 这是一个简化的逻辑,实际需要根据BPM和节拍计算小节 QJsonObject section; QJsonArray notesArray; for (const QPointF &note : notes) { QJsonArray noteArray; noteArray.append(note.x()); // 时间 noteArray.append(note.y()); // 轨道 noteArray.append(0); // 音符类型 notesArray.append(noteArray); } section.insert("sectionNotes", notesArray); section.insert("lengthInSteps", 16); section.insert("mustHitSection", true); sectionsArray.append(section); QJsonArray notesWrapperArray; notesWrapperArray.append(sectionsArray); // Psych Engine的格式要求notes是一个数组包裹另一个数组 songDetails.insert("notes", notesWrapperArray); songJson.insert("song", songDetails); QJsonDocument doc(songJson); ui->jsonTextEdit->setPlainText(doc.toJson(QJsonDocument::Indented)); }

这个示例展示了核心思路:捕获点击事件、将坐标转换为游戏数据、并生成JSON。一个完整的编辑器还需要实现时间轴滚动、网格对齐、音符拖拽、删除、BPM调整、小节划分、不同难度支持等功能。

5.3 工具的使用流程

  1. 运行Qt编写的谱面编辑器。
  2. 设置歌曲BPM和节拍。
  3. 在画布上点击,放置音符。
  4. 点击“生成JSON”按钮,右侧文本编辑器会生成格式化的谱面数据。
  5. 将生成的JSON内容复制到assets/data/your_song/下的.json文件中。
  6. 在游戏中测试谱面,根据手感微调音符时间点,再回到编辑器修改。

6. 常见问题排查与调试

在模组开发过程中,你一定会遇到各种问题。以下是常见问题的排查路径。

6.1 游戏编译或运行失败

问题现象可能原因检查方式处理建议
haxelib命令未找到Haxe未正确安装或环境变量未配置。命令行输入haxe --version重新安装Haxe,并确保安装时勾选“添加到PATH”。
lime build失败,提示缺少库项目依赖未安装完整。查看错误信息,通常包含缺失的库名。在项目根目录运行haxelib install。或手动安装指定库:haxelib install [库名]
编译通过,但游戏黑屏或闪退资源路径错误、资源格式不支持、或关键脚本语法错误。1. 检查命令行编译输出是否有警告或错误。
2. 检查assets/下资源文件是否存在且命名正确。
3. 检查新增的Haxe代码是否有语法错误。
1. 确保所有引用的图片、音频文件都在正确路径。
2. 图片格式建议PNG,音频格式建议OGG。
3. 使用VSCode的Haxe插件进行语法检查。
特定周目无法选择或加载WeekData.hx中的周目数据格式错误,或对应的歌曲文件夹缺失。1. 检查WeekData.hx中该周目对象的JSON格式,确保括号、逗号正确。
2. 检查assets/data/下是否存在歌曲名对应的文件夹。
1. 对照其他正常周目的格式进行修改。
2. 确保歌曲文件夹名称与周目数据中的songs数组内的名称完全一致(包括大小写)。

6.2 游戏内问题

问题现象可能原因检查方式处理建议
音符对不上音乐节奏1. 谱面JSON中的bpm值与音乐实际BPM不符。
2. 音符时间戳计算错误。
3. 歌曲音频文件有空白开头。
1. 用音频软件(如Audacity)测量歌曲BPM。
2. 检查编辑器生成时间戳的逻辑。
3. 用音频软件裁剪掉歌曲开头的静音部分。
1. 校准BPM,并重新生成或调整谱面。
2. 在游戏测试时,使用调试模式(如果引擎支持)显示当前播放时间,与音符时间对比。
角色动画不显示或错位1. 精灵图JSON文件中的动画名称与代码中调用名称不匹配。
2.offsets偏移量设置不当。
3. 精灵图PNG的帧排列与JSON中的indices定义不符。
1. 检查代码中调用动画的字符串(如playAnim(‘singLEFT’))。
2. 在游戏中暂停,观察角色坐标。
3. 用图片编辑软件查看精灵图,核对帧序列。
1. 确保JSON中的name与代码调用名一致。
2. 调整offsets值,正值通常向下/向右偏移。
3. 重新制作精灵图或修改indices数组。
自定义背景不显示1. 背景图片未放在正确路径。
2. 周目数据中weekBackground指定的名称与代码中加载逻辑不匹配。
3. 背景对应的加载脚本缺失。
1. 检查assets/images/stages/目录。
2. 搜索源代码中weekBackground被使用的地方(通常在PlayState.hx)。
1. 将背景图片放入assets/images/stages/,并确保文件名正确。
2. 可能需要仿照现有背景,在PlayState中编写特定的背景加载和更新逻辑。

6.3 Qt工具开发问题

问题现象可能原因检查方式处理建议
Qt程序编译失败,提示“找不到头文件”.pro文件中的QT模块未正确添加。检查错误信息中缺失的类属于哪个模块。.pro文件中添加对应模块,如QT += widgets charts(如果用了图表)。
点击画布没有反应1. 事件过滤器未安装。
2. 鼠标事件被父组件拦截。
3.mousePressEvent未被重写或连接。
1. 确认自定义绘图Widget已设置setMouseTracking(true)(如果需要跟踪移动)。
2. 在mousePressEvent中打印调试信息。
1. 确保处理事件的Widget是焦点Widget且未被覆盖。
2. 在构造函数中调用setFocusPolicy(Qt::StrongFocus)
生成的JSON格式游戏不识别JSON结构不符合FNF引擎要求。将生成的JSON与游戏原版谱面JSON进行逐层对比。使用在线的JSON格式化工具和对比工具,仔细检查键名、数组嵌套结构是否完全一致。特别注意Psych Engine的notes字段是双层数组。

7. 最佳实践与扩展方向

7.1 模组开发最佳实践

  1. 版本控制: 务必使用Git管理你的模组项目。每次做出重大修改或添加新功能前进行提交。这能让你在改坏东西时轻松回退。
  2. 模块化与组织:
    • 将你的模组内容放在mods/目录下独立的文件夹里,而不是直接修改assets/下的原版文件。这便于管理和分发。
    • 在模组文件夹内,仿照assets/的目录结构组织你的资源。
  3. 备份原文件: 在修改任何原版引擎文件(如.hx源文件)前,先备份。更好的做法是,通过引擎提供的模组API或继承机制来扩展功能,而非直接修改。
  4. 增量测试: 不要一次性修改大量内容。改一首歌、一个角色动画,就立刻编译测试,确保它能工作,再继续下一步。
  5. 利用社区资源: Psych Engine等社区引擎有详细的Wiki和活跃的Discord社区。遇到问题时,先查阅文档,再在社区提问。

7.2 Qt工具开发最佳实践

  1. 分离数据与界面: 将谱面数据模型(如Note,Section类)与Qt的界面控件分离。这样便于单元测试和未来替换UI框架。
  2. 实现撤销/重做: 对于编辑器类工具,撤销功能至关重要。可以使用Qt的QUndoStackQUndoCommand框架来实现。
  3. 支持导入/导出: 除了生成Psych Engine格式,还可以考虑支持导出为其他常见格式(如.chart),或从原版FNF谱面导入。
  4. 添加可视化辅助线: 在编辑器中绘制节拍线、小节线,并提供吸附到网格的功能,让谱面编辑更精准。
  5. 进行性能优化: 如果谱面非常长,直接绘制所有音符可能导致卡顿。可以实现基于视图范围的裁剪绘制,只绘制可视区域内的音符。

7.3 扩展方向

完成基础的周目重写后,你可以尝试更有挑战性的方向:

  1. 编写自定义游戏机制: 修改PlayState.hx,为你的周目添加独特的游戏玩法,例如双轨音符、需要按特定顺序击打的连招、会移动的障碍物等。
  2. 创建全新的角色与动画: 从零开始设计一个角色,绘制其所有动画帧(idle, singLEFT, singRIGHT, singUP, singDOWN, hey, scared等),并编写对应的JSON描述文件。
  3. 开发更复杂的Qt工具:
    • 对话编辑器: 可视化编辑对话树,支持分支选项,并导出为游戏可用的JSON或脚本格式。
    • 角色动画编辑器: 可视化地编辑精灵图偏移、帧序列和帧率,并实时预览动画效果。
    • 性能分析器: 通过进程间通信或读取日志,实时监控游戏运行时的帧率、内存占用,帮助优化模组性能。
  4. 集成到模组管理器: 了解社区模组管理器(如GameBanana或引擎内置的模组菜单)是如何加载模组的,将你的周目打包成一个标准的、可被其他玩家轻松安装的模组文件。

模组开发是深入理解游戏引擎和游戏设计的绝佳途径。从重写一个周目开始,逐步掌握资源管理、数据结构和事件驱动编程,最终你将能够创造出拥有自己独特风格和创意的完整游戏体验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询