最近在逛B站和YouTube时,经常看到一些《Friday Night Funkin‘》(FNF)的精彩手元视频,尤其是那些高难度Mod的Full Combo(FC)录像,观赏性极强。其中,使用《QT》和《Rewired》这类工具来录制、编辑甚至自动化生成游戏操作,是很多高玩和视频创作者提升效率和节目效果的秘密武器。如果你也对FNF Mod开发、游戏自动化测试,或者单纯想用技术手段复刻一段“Blissful”Erect难度的完美表演感兴趣,那么本文将为你提供一套从环境搭建到核心代码实现的完整实战指南。
本文将围绕如何使用Qt框架结合Rewired输入系统,模拟实现FNF游戏中的高精度按键操作这一主题展开。内容不仅涵盖Qt的基础项目创建、Rewired的集成,还会深入讲解如何解析FNF的谱面文件(.json)、实现毫秒级定时按键逻辑,最终完成一个可以“自动FC”特定难度曲目的演示程序。适合有一定C++和Qt基础,并对游戏逆向、自动化或FNF Mod制作感兴趣的开发者学习。
1. 项目背景与核心概念解析
在开始敲代码之前,我们有必要厘清几个关键概念,以及这个项目究竟要解决什么问题。
1.1 FNF、Mod与谱面文件《Friday Night Funkin‘》是一款使用HaxeFlixel引擎开发的开源节奏游戏。其核心玩法是跟随音乐节奏,在正确的时机按下对应的方向键(左、下、上、右)。游戏的各个关卡(Week)和大量的玩家自制模组(Mod)都包含多首歌曲,每首歌曲又分为多个难度(如Easy, Normal, Hard, Erect)。每个难度的谱面信息通常存储在一个JSON文件中,它精确记录了每个音符出现的时间点(以毫秒或节拍为单位)、音符类型以及需要按下的键位。
1.2 Qt框架在本项目中的作用Qt是一个跨平台的C++应用程序开发框架。我们选择它,是因为它提供了强大的图形界面(GUI)能力用于构建控制面板,以及高精度的定时器(QTimer、QElapsedTimer)用于控制按键时机。本项目虽然侧重于后台自动化逻辑,但一个简单的Qt界面可以方便我们启动、停止“自动演奏”,并实时显示状态,这比纯控制台程序更友好。
1.3 Rewired输入系统Rewired是一个Unity Asset Store上非常流行的输入管理插件,它统一处理键盘、手柄、鼠标等多种输入设备。但在我们的上下文中,“Rewired”更可能指的是一种模拟全局键盘输入的技术或库,用于让程序能够“欺骗”操作系统,产生如同真实玩家按下键盘按键一样的事件。这对于自动化操作FNF这类接收键盘输入的游戏至关重要。在C++/Qt环境中,我们通常使用Windows API(如keybd_event或SendInput)或跨平台的库(如X11库 for Linux)来实现类似功能。
1.4 项目目标因此,本项目的核心目标是:开发一个Qt桌面应用程序,它能够读取FNF Mod(例如“Blissful”歌曲Erect难度)的谱面文件(JSON),然后在歌曲播放时,通过系统级模拟键盘按键的方式,在精确到毫秒的时机触发对应的方向键按下和抬起事件,从而实现理论上的“Full Combo”效果。这本质上是一个针对特定游戏的、基于时序的自动化脚本。
2. 开发环境准备与项目创建
工欲善其事,必先利其器。首先确保你的开发环境就绪。
2.1 环境与工具清单
- 操作系统:Windows 10/11(本文以Windows为例,Linux/macOS原理类似但系统API不同)。
- Qt版本:Qt 5.15.x 或 Qt 6.x。建议使用长期支持版本。可以从 Qt官网 下载开源版本或商业版本。
- IDE:Qt Creator(推荐,与Qt集成度最高)或 Visual Studio with Qt VS Tools。
- 编译器:MSVC(Windows)或 MinGW。确保与Qt安装时选择的编译器匹配。
- 第三方库:
- JSON解析库:Qt自身提供了
QJsonDocument,QJsonObject,QJsonArray,无需额外安装。 - 模拟输入库:Windows下我们将使用
<windows.h>中的API。项目需链接User32.lib。
- JSON解析库:Qt自身提供了
2.2 创建Qt项目
- 打开Qt Creator,点击“New Project”。
- 选择“Application” -> “Qt Widgets Application”。
- 输入项目名称,例如
FNF_AutoPlayer。 - 在“Kit Selection”中,选择你安装的Qt版本和对应的编译器(如Desktop Qt 5.15.2 MSVC2019 64bit)。
- 一路点击“Next”,直到完成。
2.3 项目基础结构创建完成后,你的项目文件结构大致如下:
FNF_AutoPlayer/ ├── FNF_AutoPlayer.pro # Qt项目文件 ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui # Qt Designer界面文件 └── (其他自动生成的文件)2.4 配置项目文件 (.pro)我们需要在.pro文件中添加对Windows系统库的链接。打开FNF_AutoPlayer.pro,在末尾添加:
# 如果是Windows系统,链接User32库用于模拟按键 win32 { LIBS += -lUser32 }对于非Windows平台,你需要研究对应的模拟输入API(如X11的XTest),本文重点讲解Windows实现。
3. 核心原理与模块设计
在动手编码前,先进行顶层设计,将大问题分解为几个可实现的模块。
3.1 系统架构图(概念)
[Qt GUI 控制界面] | | (用户操作:加载谱面、开始、停止) V [核心控制器 CoreController] | | (解析JSON,管理定时) V [谱面数据模型 SongChart] | | (按时间排序的音符列表) V [输入模拟器 InputSimulator] ---(调用系统API)---> [操作系统] ---> [FNF游戏窗口]3.2 各模块职责
- SongChart (谱面模型):
- 负责加载和解析FNF谱面JSON文件。
- 将JSON数据转换为内部数据结构(如
QList<Note>)。 - 提供按时间顺序获取音符的接口。
- InputSimulator (输入模拟器):
- 封装Windows
SendInputAPI。 - 提供
pressKey、releaseKey、tapKey(按下并立即释放)等方法。 - 将FNF的箭头方向(LEFT, DOWN, UP, RIGHT)映射到实际的虚拟键码(VK_LEFT, VK_DOWN等)。
- 封装Windows
- CoreController (核心控制器):
- 项目的大脑,持有
SongChart和InputSimulator实例。 - 使用
QTimer进行高精度(1ms间隔)的计时循环。 - 在计时循环中,检查当前播放时间点是否有音符需要触发,若有则调用
InputSimulator执行按键。 - 处理歌曲的开始、暂停、停止逻辑。
- 项目的大脑,持有
- MainWindow (主界面):
- 提供用户界面:文件选择按钮、开始/停止按钮、当前状态显示(时间、COMBO数)。
- 将用户操作转发给
CoreController。
3.3 时间同步的关键最大的挑战是同步。我们的程序计时必须与FNF游戏内的歌曲播放进度保持同步。有两种思路:
- 绝对时间同步:从程序发出“开始演奏”信号的那一刻起,以系统时钟为基准,严格按照谱面中每个音符的毫秒时间戳触发按键。这要求FNF游戏也在同一精确时刻开始播放音乐,通常需要手动在程序中控制音乐播放,或与游戏进行进程间通信(IPC),难度较高。
- 相对时间同步(更实用):我们假设从程序启动自动化到用户手动在游戏中按下“Start”之间有可接受的微小延迟。程序内部维护一个从开始点计算的“已播放时间”,并以此为依据触发音符。这种方法实现简单,但需要用户手动对齐开始时刻。
本文采用第二种相对时间同步方案,因为它更易于实现和理解,且对于演示目的足够有效。
4. 完整实战代码实现
接下来,我们一步步实现上述模块。
4.1 定义数据结构 (note.h)首先,创建一个note.h文件来定义音符数据结构。
// note.h #ifndef NOTE_H #define NOTE_H #include <QString> // FNF中音符的类型(普通音符、长按音符等) enum class NoteType { Normal, Hold, // 可根据实际谱面扩展 }; // 代表一个音符 struct Note { double time; // 音符出现的时间点(单位:秒或毫秒,需统一) int direction; // 方向:0-左,1-下,2-上,3-右 (对应FNF标准) NoteType type; double duration; // 对于Hold音符,持续时长 Note(double t, int dir, NoteType nt = NoteType::Normal, double dur = 0.0) : time(t), direction(dir), type(nt), duration(dur) {} }; #endif // NOTE_H4.2 实现输入模拟器 (inputsimulator.h / .cpp)创建inputsimulator.h和inputsimulator.cpp。
// inputsimulator.h #ifndef INPUTSIMULATOR_H #define INPUTSIMULATOR_H #include <windows.h> class InputSimulator { public: InputSimulator(); bool pressKey(int vkCode); // 按下键 bool releaseKey(int vkCode); // 释放键 bool tapKey(int vkCode, int delayMs = 50); // 点击键(按下并释放) // 将FNF方向索引映射到Windows虚拟键码 static int fnfDirectionToVK(int direction); }; #endif // INPUTSIMULATOR_H// inputsimulator.cpp #include "inputsimulator.h" #include <QThread> InputSimulator::InputSimulator() {} bool InputSimulator::pressKey(int vkCode) { INPUT input = {0}; input.type = INPUT_KEYBOARD; input.ki.wVk = vkCode; input.ki.dwFlags = 0; // 按下 return SendInput(1, &input, sizeof(INPUT)) == 1; } bool InputSimulator::releaseKey(int vkCode) { INPUT input = {0}; input.type = INPUT_KEYBOARD; input.ki.wVk = vkCode; input.ki.dwFlags = KEYEVENTF_KEYUP; // 释放 return SendInput(1, &input, sizeof(INPUT)) == 1; } bool InputSimulator::tapKey(int vkCode, int delayMs) { bool pressOk = pressKey(vkCode); QThread::msleep(delayMs); // 短暂延迟,模拟按键时长 bool releaseOk = releaseKey(vkCode); return pressOk && releaseOk; } int InputSimulator::fnfDirectionToVK(int direction) { switch(direction) { case 0: return VK_LEFT; // 左箭头 case 1: return VK_DOWN; // 下箭头 case 2: return VK_UP; // 上箭头 case 3: return VK_RIGHT; // 右箭头 default: return VK_LEFT; // 默认 } }4.3 实现谱面解析器 (songchart.h / .cpp)创建songchart.h和songchart.cpp。这里需要你根据实际FNF Mod的JSON格式进行解析。以下是一个通用示例,假设谱面JSON结构类似于FNF基础游戏。
// songchart.h #ifndef SONGCHART_H #define SONGCHART_H #include "note.h" #include <QList> #include <QString> class SongChart { public: SongChart(); bool loadFromFile(const QString& filePath); const QList<Note>& getNotes() const { return m_notes; } void sortNotesByTime(); // 按时间排序 private: QList<Note> m_notes; QString m_songName; // 其他元数据... }; #endif // SONGCHART_H// songchart.cpp #include "songchart.h" #include <QFile> #include <QJsonDocument> #include <QJsonArray> #include <QJsonObject> #include <QDebug> SongChart::SongChart() {} bool SongChart::loadFromFile(const QString& filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "无法打开文件:" << filePath; return false; } QByteArray data = file.readAll(); file.close(); QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "JSON解析错误:" << parseError.errorString(); return false; } m_notes.clear(); QJsonObject rootObj = doc.object(); // 注意:不同Mod的JSON结构可能不同!这里是示例。 // 通常音符数据在 "song" -> "notes" 或 "notes" 数组中 if (rootObj.contains("song") && rootObj["song"].isObject()) { QJsonObject songObj = rootObj["song"].toObject(); if (songObj.contains("notes") && songObj["notes"].isArray()) { QJsonArray sections = songObj["notes"].toArray(); for (const QJsonValue& sectionVal : sections) { if (sectionVal.isObject()) { QJsonObject section = sectionVal.toObject(); // 可能包含 "sectionNotes" 字段 if (section.contains("sectionNotes") && section["sectionNotes"].isArray()) { QJsonArray notesArray = section["sectionNotes"].toArray(); for (const QJsonValue& noteVal : notesArray) { if (noteVal.isArray()) { QJsonArray noteArr = noteVal.toArray(); if (noteArr.size() >= 2) { double time = noteArr[0].toDouble(); // 时间戳(秒) int direction = noteArr[1].toInt(); // 方向 // 假设是普通音符 m_notes.append(Note(time, direction)); } } } } } } } } else if (rootObj.contains("notes") && rootObj["notes"].isArray()) { // 另一种可能的格式 QJsonArray notesArray = rootObj["notes"].toArray(); for (const QJsonValue& noteVal : notesArray) { // 解析逻辑... } } sortNotesByTime(); qDebug() << "成功加载谱面,音符数量:" << m_notes.size(); return true; } void SongChart::sortNotesByTime() { std::sort(m_notes.begin(), m_notes.end(), [](const Note& a, const Note& b) { return a.time < b.time; }); }4.4 实现核心控制器 (corecontroller.h / .cpp)这是逻辑最核心的部分。
// corecontroller.h #ifndef CORECONTROLLER_H #define CORECONTROLLER_H #include "songchart.h" #include "inputsimulator.h" #include <QObject> #include <QTimer> #include <QElapsedTimer> class CoreController : public QObject { Q_OBJECT public: explicit CoreController(QObject *parent = nullptr); ~CoreController(); bool loadChart(const QString& filePath); void startPlaying(); void stopPlaying(); signals: void playbackStarted(); void playbackStopped(); void comboUpdated(int combo); void errorOccurred(const QString& message); private slots: void onTimerTick(); private: SongChart m_chart; InputSimulator m_inputSim; QTimer* m_playbackTimer; QElapsedTimer m_elapsedTimer; bool m_isPlaying; int m_currentNoteIndex; double m_playbackStartTime; // 内部记录的起始时间(秒) }; #endif // CORECONTROLLER_H// corecontroller.cpp #include "corecontroller.h" #include <QDebug> #include <QThread> CoreController::CoreController(QObject *parent) : QObject(parent) , m_isPlaying(false) , m_currentNoteIndex(0) , m_playbackStartTime(0.0) { m_playbackTimer = new QTimer(this); m_playbackTimer->setTimerType(Qt::PreciseTimer); // 高精度定时器 m_playbackTimer->setInterval(1); // 1ms检查一次 connect(m_playbackTimer, &QTimer::timeout, this, &CoreController::onTimerTick); } CoreController::~CoreController() { stopPlaying(); } bool CoreController::loadChart(const QString& filePath) { stopPlaying(); return m_chart.loadFromFile(filePath); } void CoreController::startPlaying() { if (m_isPlaying) return; if (m_chart.getNotes().isEmpty()) { emit errorOccurred("未加载谱面或谱面为空!"); return; } m_isPlaying = true; m_currentNoteIndex = 0; m_elapsedTimer.start(); m_playbackStartTime = 0.0; // 重置内部计时 m_playbackTimer->start(); emit playbackStarted(); qDebug() << "开始自动播放..."; } void CoreController::stopPlaying() { if (!m_isPlaying) return; m_isPlaying = false; m_playbackTimer->stop(); // 释放所有可能按下的键 for(int i=0; i<4; ++i) { m_inputSim.releaseKey(InputSimulator::fnfDirectionToVK(i)); } emit playbackStopped(); qDebug() << "停止自动播放。"; } void CoreController::onTimerTick() { if (!m_isPlaying) return; // 计算自开始播放以来经过的时间(秒) double currentTime = m_playbackStartTime + m_elapsedTimer.elapsed() / 1000.0; const QList<Note>& notes = m_chart.getNotes(); // 检查并触发当前时间点及之前未触发的音符 while (m_currentNoteIndex < notes.size()) { const Note& note = notes.at(m_currentNoteIndex); // 定义一个小的提前量(例如-0.010秒)以补偿系统延迟,这个值需要微调 double triggerTime = note.time - 0.010; if (currentTime >= triggerTime) { // 触发按键 int vk = InputSimulator::fnfDirectionToVK(note.direction); m_inputSim.tapKey(vk, 50); // 模拟按下50ms后释放 qDebug() << "触发音符 @" << note.time << "s, 方向:" << note.direction; m_currentNoteIndex++; // 可以在这里发射combo更新信号 } else { // 时间还没到,跳出循环 break; } } // 如果所有音符都触发完毕,停止播放 if (m_currentNoteIndex >= notes.size()) { stopPlaying(); qDebug() << "所有音符已触发完毕。"; } }4.5 实现主界面 (MainWindow)最后,我们需要修改Qt Creator自动生成的MainWindow类,将其与控制器连接。
// mainwindow.h (部分修改) #include <QMainWindow> #include "corecontroller.h" QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void on_loadChartButton_clicked(); void on_startButton_clicked(); void on_stopButton_clicked(); void onControllerPlaybackStarted(); void onControllerPlaybackStopped(); void onControllerComboUpdated(int combo); void onControllerError(const QString& msg); private: Ui::MainWindow *ui; CoreController* m_controller; };// mainwindow.cpp #include "mainwindow.h" #include "ui_mainwindow.h" #include <QFileDialog> #include <QMessageBox> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_controller(new CoreController(this)) { ui->setupUi(this); // 连接控制器的信号到UI槽函数 connect(m_controller, &CoreController::playbackStarted, this, &MainWindow::onControllerPlaybackStarted); connect(m_controller, &CoreController::playbackStopped, this, &MainWindow::onControllerPlaybackStopped); connect(m_controller, &CoreController::comboUpdated, this, &MainWindow::onControllerComboUpdated); connect(m_controller, &CoreController::errorOccurred, this, &MainWindow::onControllerError); ui->stopButton->setEnabled(false); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_loadChartButton_clicked() { QString filePath = QFileDialog::getOpenFileName(this, "选择FNF谱面文件", "", "JSON Files (*.json)"); if (!filePath.isEmpty()) { if (m_controller->loadChart(filePath)) { ui->statusLabel->setText("谱面加载成功: " + QFileInfo(filePath).fileName()); ui->startButton->setEnabled(true); } else { ui->statusLabel->setText("谱面加载失败!"); ui->startButton->setEnabled(false); } } } void MainWindow::on_startButton_clicked() { ui->startButton->setEnabled(false); ui->stopButton->setEnabled(true); ui->loadChartButton->setEnabled(false); m_controller->startPlaying(); } void MainWindow::on_stopButton_clicked() { m_controller->stopPlaying(); ui->startButton->setEnabled(true); ui->stopButton->setEnabled(false); ui->loadChartButton->setEnabled(true); } void MainWindow::onControllerPlaybackStarted() { ui->statusLabel->setText("状态:播放中..."); } void MainWindow::onControllerPlaybackStopped() { ui->statusLabel->setText("状态:已停止"); ui->startButton->setEnabled(true); ui->stopButton->setEnabled(false); ui->loadChartButton->setEnabled(true); } void MainWindow::onControllerComboUpdated(int combo) { ui->comboLabel->setText(QString("COMBO: %1").arg(combo)); } void MainWindow::onControllerError(const QString& msg) { QMessageBox::warning(this, "错误", msg); onControllerPlaybackStopped(); // 出错时恢复到停止状态 }同时,你需要使用Qt Designer(打开mainwindow.ui)拖放几个按钮(加载、开始、停止)和标签(状态、COMBO显示)。
4.6 运行与测试
- 编译项目:在Qt Creator中,点击左下角的构建并运行按钮。
- 准备测试谱面:找到一个FNF Mod(例如“Blissful”)的Erect难度JSON谱面文件。你需要研究该Mod具体的文件结构,并可能需要对
SongChart::loadFromFile函数进行适配。 - 测试流程:
- 运行程序,点击“加载谱面”按钮选择JSON文件。
- 打开FNF游戏,并进入对应歌曲的练习模式或开始界面。
- 将游戏窗口置于前台。
- 点击程序的“开始”按钮,然后迅速(在1-2秒内)在游戏中按下开始键(通常是空格键)。这一步是为了对齐时间。
- 观察程序是否在正确的时机触发按键,以及游戏中的角色是否能够击中音符。
5. 常见问题与排查思路
在开发和运行过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
程序编译错误:unknown module(s) in qt: xlsx | 项目.pro文件中包含了未安装的Qt模块(如QT += xlsx)。 | 移除.pro文件中不必要的模块引用。本项目不需要xlsx模块。 |
| 按键模拟无效,游戏无反应 | 1. 游戏窗口未激活。 2. 虚拟键码映射错误。 3. 游戏以管理员权限运行,而程序没有。 4. 防作弊软件或游戏本身屏蔽了模拟输入。 | 1. 确保游戏窗口是当前活动窗口。 2. 检查 fnfDirectionToVK映射是否正确,尝试用VK_A等字母键测试。3. 尝试以相同权限级别(都管理员或都非管理员)运行程序和游戏。 4. 某些游戏或安全软件会拦截 SendInput。可以尝试更底层的驱动级模拟(如Arduino模拟键盘),但这更复杂且可能有风险。 |
| 按键时机不准,总是Miss | 1. 时间同步问题。 2. 定时器精度不足。 3. 音符触发提前量( triggerTime)设置不当。4. 系统负载高导致延迟。 | 1. 这是最难调的部分。尝试在startPlaying()中让程序“嘀”一声或闪屏,你同时按游戏开始键,练习同步。2. 已使用 Qt::PreciseTimer。在Windows下可考虑QElapsedTimer的monotonic时钟。3. 调整 onTimerTick中的triggerTime偏移量(-0.010),可能需要反复测试。4. 关闭不必要的程序,确保游戏和本程序有足够的CPU资源。 |
| 程序崩溃或卡死 | 1. JSON文件格式不匹配,解析出错。 2. 在非Windows系统上编译了Windows API代码。 3. 多线程访问冲突。 | 1. 使用调试器或添加日志,检查loadFromFile函数,确保它能处理目标Mod的JSON格式。2. 使用 #ifdef _WIN32宏包裹Windows API相关代码。3. 确保所有对 m_notes等共享数据的访问都在主线程(或加锁)。本例中所有操作都在主线程定时器触发,相对安全。 |
| 只能触发部分音符 | 1. 谱面解析不完整,只解析了一种音符类型。 2. 长按音符(Hold)未处理。 3. 音符时间戳单位错误(可能是毫秒而非秒)。 | 1. 仔细分析目标JSON文件的实际结构,完善loadFromFile函数。2. 当前代码只处理了 tapKey。对于Hold音符,需要实现pressKey和延迟releaseKey的逻辑。3. 打印几个音符的 time值,看是像0.5(秒)还是500(毫秒)。如果是毫秒,在比较时需要转换为秒(/1000.0)。 |
6. 最佳实践与进阶优化方向
一个基础可用的自动化程序已经完成,但要使其更健壮、更精准,可以考虑以下优化:
6.1 工程结构优化
- 配置文件:将键位映射、触发提前量、定时器间隔等参数提取到配置文件中(如
config.ini),方便调整而无需重新编译。 - 日志系统:使用
QFile和QTextStream实现简单的日志记录,记录每个音符的触发时间、实际系统时间、是否成功等信息,便于后期分析时序问题。 - 错误处理:在
InputSimulator和CoreController中增加更细致的错误返回值检查,并提供友好的错误信息反馈到UI。
6.2 性能与精度优化
- 时间同步算法:实现更精确的同步。可以考虑在程序开始时发送一个“开始信号”(如模拟按下某个功能键),并在游戏中将该键绑定为“开始演奏”,实现软同步。或者尝试读取游戏进程内存中的当前音乐播放时间(涉及游戏逆向,复杂度高)。
- 动态提前量:不同的电脑、不同的游戏帧率可能导致延迟不同。可以设计一个校准功能:程序自动触发几个测试按键,用户反馈是否命中,程序据此计算出一个动态的延迟补偿值。
- 使用高分辨率定时器:对于Windows,可以研究
timeSetEvent或CreateWaitableTimer等高精度多媒体定时器,但要注意它们在Qt中的集成方式。
6.3 功能扩展
- 支持更多Mod和格式:抽象出
ChartParser接口,为不同格式的FNF Mod谱面实现不同的解析器。 - 可视化谱面:利用Qt的Graphics View框架,在程序内绘制一个简单的下落式谱面预览,增强用户体验。
- 手动录制与回放:除了自动播放,还可以增加“录制模式”,记录用户手动游玩的按键时序,并可以保存和回放,用于研究最佳击打时机。
- 多线程处理:将高频率的定时检查逻辑放入一个独立的
QThread中,避免阻塞UI线程导致界面卡顿。
6.4 伦理与使用提醒
- 仅用于学习与测试:本项目的主要目的是学习Qt、系统输入模拟和游戏数据解析等技术。在公开场合使用此类自动化工具完成游戏并宣称成绩,可能违反游戏社区规则和公平竞技精神。
- 尊重开发者:FNF及其Mod是开发者心血的结晶。使用自动化工具应限于单机练习、技术研究或视频制作辅助,切勿用于破坏他人游戏体验或进行作弊。
- 注意系统安全:模拟输入功能强大,请确保你的程序不会在无意中触发恶意操作。编译后的程序可能被安全软件误报,属于正常现象。
通过这个项目,你不仅能够深入理解Qt的事件循环、定时器和文件处理,还能掌握Windows系统输入模拟和JSON数据解析等实用技能。更重要的是,你学会了如何将一个有趣的想法(自动玩FNF)分解为具体的技术模块并逐步实现。尽管最终的程序可能需要针对特定的Mod进行适配和调优,但整个开发框架和思路是通用的,可以应用于其他节奏游戏或需要自动化键盘操作的场景。