这次来看《In Falsus》系列的两张自制谱面预览:New Vision FBD 11 与 Sin Utopia ULT 10。标题里写的是“铺面预览”,音游圈这两种说法都常见,下面统一用“谱面”。预览的核心不是贴两张截图完事,而是要把谱面从编辑器导出之后的数据变成一套可重复执行的检查流程:统计结构、扫描密度、渲染可视化页面、批量生成报表,最后再决定视频素材能不能作为预览内容发布。
先给结论:这份预览对硬件几乎没要求,不需要独立显卡,也不需要大显存,CPU 加一个能开浏览器的设备就能跑。真正的门槛在谱面格式理解和工具链组织上。FBD 和 ULT 从命名上看是两套不同的难度标签体系,FBD 11 和 ULT 10 分别指向各自体系中的较高等级;但由于本次材料里没有提供这两个标签的完整定义,本文不对缩写的具体含义做猜测,后文会建议把难度标签写进谱面元数据里,这样统计脚本才能自动识别和对比。
这篇文章适合三类人:自己写自制谱、需要在发布前做谱面预览的作者;想做谱面预览视频、但不想每次都用游戏编辑器录制的玩家;准备给音游谱面写管理工具的开发者。文章会把常用的 JSON 解析、NPS 统计、Canvas 本地渲染、批量 CSV 报表、OBS 录屏和 ffmpeg 压制全部拆开讲,并给出可以直接复制的代码段。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 预览对象 | 《In Falsus》New Vision FBD 11 与 Sin Utopia ULT 10 |
| 工作目标 | 自制谱面的结构统计、密度扫描、可视化渲染与发布前自检 |
| 难度标签 | FBD 11、ULT 10,标签体系含义以谱面发布说明为准 |
| 硬件需求 | CPU 即可,无强 GPU 依赖,不需要特殊显卡 |
| 核心工具 | 谱面 JSON 导出、Python 统计脚本、Canvas 渲染器、OBS/ffmpeg |
| 启动方式 | 本地直接打开 HTML 或运行 Python 脚本,无复杂服务 |
| 外部 API | 无外部 API,可启动本地 HTTP 服务辅助调试 |
| 批量任务 | 支持:扫描目录下所有谱面 JSON,生成 CSV 统计报表 |
| 主要产出 | 密度报表、可视化预览页面、可发布的预览视频 |
需要说明一点,谱面文件格式会因为编辑器不同而差别很大。下面所有脚本都以 JSON 格式为前提,并且提供了一套演示用的字段结构。如果你手里的谱面导出结果是 .osu、.mc、.txt 等其它格式,需要替换的是解析层,统计与渲染思路是一样的。
2. 适用场景与使用边界
这套预览流程适合谱面作者在正式发布前使用。一张谱面写到后期,很容易出现“自己打起来觉得哪里都不对,但说不出是哪一段不对”的情况。通过把谱面文件读出来做数值统计和视觉渲染,可以提前发现几个明显问题:开头段落太密容易劝退、高难段集中在歌曲最后导致体力崩盘、同轨道 note 间隔过近导致图形堆叠、hold 和 tap 的比例失衡等。这些都是可以通过数据观察到的。
它也适合做谱面预览视频的人。游戏内录制通常受判定线、皮肤、特效影响,观众看到的不一定是谱面原本的 note 结构。用自定义 Canvas 渲染器做预览,可以把每一条 note 直观画出来,还能人为标注密度峰值出现的时间点,比直接录游戏更清楚。
使用边界也需要说清楚。这套流程不能替代真实设备上的手感和判定测试。谱面预览渲染出来好看,不代表实际游玩舒服,最后仍然要回到游戏里跑几遍。另一方面,这不适合作为零基础音游谱面制作教程,因为文中默认你已经有可导出的谱面数据,并且熟悉自己编辑器里的 note 类型。
版权与安全部分不要跳过。自制谱面通常依赖某首歌曲或某个音源,发布预览视频前要确认背景音乐授权是否符合平台规则,不要直接分发未授权音源。社区里其它谱面的对齐参数、时间轴数据、visual 设计同样受作者约定约束,不能未经允许搬运。涉及任何商业曲目、人物肖像或受保护素材时,都要先确认可用范围。
3. 预览前环境准备与谱面目录整理
先搭一个固定目录,后面批量处理会省很多事。比较推荐的布局是这样:
in_falsus_preview/ ├── charts/ │ ├── new_vision_fbd_11.json │ └── sin_utopia_ult_10.json ├── audio/ │ └── bgm_preview.mp3 ├── scripts/ │ ├── chart_analyzer.py │ └── analyze_dir.py ├── out/ │ ├── report.csv │ └── screenshot/ ├── index.html └── render.html注意new_vision_fbd_11.json这种文件名只是本地整理用,不代表正式发布名。把谱面文件命名为“曲名_难度标签_等级”的好处是:后续批量统计时可以直接从文件名识别难度段落,即使 JSON 内部没有完整元数据,脚本也能输出可读列表。
工具准备很少。Python 需要能运行,检查方式是在命令行输入:
python --version能正常输出版本号即可。浏览器建议使用 Chrome 或 Edge,因为后面的 Canvas 渲染器主要测试目标是这两个浏览器。录制视频时用 OBS Studio 或者浏览器自带录制都行;如果是后期压制视频,再装一个 ffmpeg。
还有一个容易被忽略的步骤:确认谱面 JSON 里的字段结构。不同谱面编辑器的导出结果差别很大,有的时间单位是秒,有的是毫秒;有的 note 类型叫tap/hold,有的叫note/long。在写统计脚本前,先打开一份谱面 JSON,看清楚notes数组里每个对象有哪些字段。这个动作看起来简单,却能避免后面大量调试时间。
4. 谱面解析与密度统计:FBD、ULT 标签先量化再对比
4.1 演示用谱面数据格式
本文中的脚本基于下面这种简化 JSON 结构。它不会出现在 New Vision FBD 11 的真实文件里,但可以帮你理解解析思路:
{ "meta": { "title": "New Vision", "difficultyLabel": "FBD 11", "bpm": 180.0, "offset": 0.0 }, "notes": [ { "time": 1.00, "column": 0, "kind": "tap" }, { "time": 1.00, "column": 2, "kind": "tap" }, { "time": 1.50, "column": 1, "kind": "hold", "duration": 1.0 }, { "time": 2.00, "column": 3, "kind": "tap" } ] }实际读取时,如果编辑器使用毫秒,那就需要把time统一除以 1000 再进入统计,否则 NPS 会整体放大 60 倍。这也是谱面预览里最常见的翻车点之一。为了让脚本能同时处理 tap 和 hold,我建议每个谱面对象都保留一个统一单位的时间字段,hold 额外用duration或结束时间表示持续长度。脚本读取前先按time排序,避免不同编辑器导出顺序不一致造成后续扫描错乱。
4.2 Python 密度统计脚本
下面的chart_analyzer.py会读取谱面 JSON,输出总 note 数、类型分布、谱面时长、平均 NPS,以及一个两秒滑动窗口里的峰值 NPS。两秒窗口是节奏游戏谱面分析里比较常用的时间尺度,能看出“某一段是否突然过密”。
import json from collections import Counter from pathlib import Path def analyze_chart(path: Path, window: float = 2.0) -> dict: data = json.loads(Path(path).read_text(encoding="utf-8")) notes = data.get("notes", []) if not notes: return {"error": "notes is empty"} notes = sorted(notes, key=lambda n: n["time"]) times = [n["time"] for n in notes] chart_start = times[0] chart_end = times[-1] duration = max(chart_end - chart_start, 1e-6) kinds = Counter(n.get("kind", "tap") for n in notes) avg_nps = len(times) / duration best_count = 0 best_start = chart_start for left in times: right = left + window count = sum(1 for t in times if left <= t < right) if count > best_count: best_count = count best_start = left return { "total": len(times), "kinds": dict(kinds), "duration": round(duration, 2), "avg_nps": round(avg_nps, 2), "peak_window": window, "peak_nps": round(best_count / window, 2), "peak_start": round(best_start, 2), } if __name__ == "__main__": chart_list = [ "../charts/new_vision_fbd_11.json", "../charts/sin_utopia_ult_10.json", ] for chart_path in chart_list: result = analyze_chart(Path(chart_path)) print(chart_path, result)在这个脚本里,hold 的判定按头部时间计入 NPS,这样更符合“玩家某个瞬间需要处理的 note 数量”的直觉。如果你更习惯把 hold 按实际按住时间拆分,可以把kinds的逻辑改成按结束时间统计,两种口径各有道理,关键是要在预览结果里写清楚。
从输出结果能读出的信息分为三层:总数、平均值、峰值分布。New Vision FBD 11 和 Sin Utopia ULT 10 分属不同难度体系,直接拿两个分数对比没有意义,正确做法是先统计同系列同难度段的其它谱面,建立一个基准区间;如果某张谱面的峰值 NPS 明显高于基准区间,就要特别注意峰值出现的位置。峰值如果出现在歌曲前三分之一,玩家还没进入状态就容易摔;峰值如果集中在最后三十秒,则更考验体力储备。这些不能靠感觉,要用窗口扫描找出来。
4.3 峰值密度时间点的意义
密度扫描最实用的输出不是总分,而是peak_start。拿到这个时间点之后,回到谱面编辑器里把光标定位到该位置,检查这一段是不是作者有意设计的高潮段。如果是,那么峰值高合理,接下来要看 note 排列是否足够可读;如果不是,说明该段落存在无意识堆密度的问题,通常需要把一部分 note 顺移到前后的小节里。
从谱面预览角度看,New Vision FBD 11 的重点是观察高编号难度的峰值是否均匀分布,Sin Utopia ULT 10 则要看长持续段落的体力曲线是否平滑。这里先不预设这两张谱面内部一定有什么问题,而是把检查模板交给数据,跑完 PDF 报表后再下结论。
5. 本地 Canvas 谱面预览渲染器
密度统计是量化视角,可视化渲染是结构视角。写一个简单的 Canvas 渲染器,比每次打开游戏编辑器导出截图要省事得多。这里实现一个四轨下落式渲染器,支持 textarea 粘贴谱面 JSON,点击 Load 后渲染,Space 控制播放/暂停,A/D 前后跳转 1 秒,R 回到开头。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>谱面预览渲染器</title> <style> body { background: #0d1117; color: #eee; font-family: "Courier New", monospace; margin: 20px; } textarea { width: 900px; height: 130px; background: #161b22; color: #e6edf3; border: 1px solid #30363d; font-size: 12px; } button { padding: 8px 16px; background: #238636; border: none; color: #fff; cursor: pointer; } canvas { display: block; margin: 12px 0; border: 1px solid #30363d; background: #0d1117; } </style> </head> <body> <textarea id="chartSource"> { "meta": { "title": "Demo", "bpm": 180 }, "notes": [ { "time": 1.00, "column": 0, "kind": "tap" }, { "time": 1.00, "column": 2, "kind": "tap" }, { "time": 1.50, "column