自制音游谱面预览全流程:JSON解析、NPS统计与Canvas渲染
这次来看《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(encodingutf-8)) notes data.get(notes, []) if not notes: return {error: notes is empty} notes sorted(notes, keylambda 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 langzh-CN head meta charsetUTF-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 idchartSource { meta: { title: Demo, bpm: 180 }, notes: [ { time: 1.00, column: 0, kind: tap }, { time: 1.00, column: 2, kind: tap }, { time: 1.50, column