noctilink
概述
noctilink 以 .yzcpkg 包的形式分发歌曲,这是标准 ZIP 压缩格式,内部结构如下:
song.json:包级元数据与各难度索引- 每个难度一个
.yzc.json文件(Stable / Drifting / Faint / Relink) - 一个音频文件(
.mp3或.ogg) - 一个封面图文件(可选,
.jpg或.png)
两种 JSON 格式当前版本均为 1。song.json 与各 .yzc.json 中的路径相对于包根目录解析。
song.json
包级描述文件。由编辑器写入,游戏在导入时读取。
{
"version": 1,
"id": "mynick_mysong",
"title": "Song Title",
"artist": "Artist Name",
"cover_color": [0.95, 0.70, 0.40],
"audio": "music.ogg",
"cover": "cover.jpg",
"charts": [
{ "difficulty": "Stable", "level": "5", "chart_constant": 5.0, "chart": "Stable.yzc.json" },
{ "difficulty": "Drifting", "level": "8", "chart_constant": 8.0, "chart": "Drifting.yzc.json" },
{ "difficulty": "Faint", "level": "10", "chart_constant": 10.0, "chart": "Faint.yzc.json" },
{ "difficulty": "Relink", "level": "12", "chart_constant": 12.0, "chart": "Relink.yzc.json" }
]
}
顶层字段
| 字段 | 类型 | 说明 |
|---|---|---|
version |
int | 格式版本号(当前为 1) |
id |
string | 歌曲唯一标识,需避开内置歌曲 ID 冲突;导入侧只接受小写英文字母、数字和下划线([a-z0-9_]) |
title |
string | 显示用标题 |
artist |
string | 艺术家或作曲者 |
cover_color |
array[3] | 选曲列表中歌曲行的背景色,RGB 浮点值,范围 [0.0, 1.0] |
audio |
string | 音频文件名(包根目录相对路径) |
cover |
string | 封面图文件名,可选 |
charts |
array | 难度索引(见下) |
charts 条目字段
| 字段 | 类型 | 说明 |
|---|---|---|
difficulty |
string | 必须为 "Stable"、"Drifting"、"Faint"、"Relink" 之一 |
level |
string | 显示给玩家的难度等级,如 "7"、"9+"、"11" |
chart_constant |
number | 可选 — rating 用的谱面定数。缺省时,整数 level 视为 .0,"9+" 这类整数加号视为 .5,非数字标签视为 0.0 |
difficulty_label |
string | 可选 — 特殊谱面的显示标签覆盖;留空时使用 difficulty |
chart |
string | 难度对应的谱面文件路径 |
title |
string | 可选 — 覆盖顶层 title |
artist |
string | 可选 — 覆盖顶层 artist |
audio |
string | 可选 — 该难度的独立音频覆盖 |
cover |
string | 可选 — 该难度的独立封面覆盖 |
charter |
string | 可选 — 该难度的制谱者署名 |
cover_source |
string | 可选 — 封面来源说明(作者、链接、委托备注);在选曲详情面板与 charter 并列显示 |
仅在与顶层值不同的情况下才写入覆盖字段。
<难度名>.yzc.json
每个难度对应一个文件,描述元数据、拍号和音符内容。
{
"version": 1,
"metadata": { ... },
"timing": { ... },
"notes": [ ... ]
}
metadata
| 字段 | 类型 | 说明 |
|---|---|---|
title |
string | 歌曲名 |
artist |
string | 艺术家 |
charter |
string | 制谱者 |
cover_source |
string | 封面来源说明(作者、链接、委托备注);如 song.json 的 charts[] 条目里也写了会覆盖此处 |
difficulty_name |
string | 必须与父 song.json 条目中的 difficulty 一致 |
difficulty_level |
string | 难度等级,自由格式字符串 |
audio_file |
string | 音频文件名(包根目录相对路径) |
preview_time |
float | 选曲界面试听起始点(秒) |
preview_end_time |
float | 试听结束点(秒),0 表示使用默认时长 |
background |
string | 封面图文件名(可选)。名字虽叫 background,但它其实是封面图;游玩背景请用下面的 gameplay_background。 |
gameplay_background |
string | 可选 — 游玩时背景图文件名,与封面相互独立。设置后游玩界面会显示这张图(模糊/变暗处理与封面一致);为空时回退到模糊后的封面。与其他资源一样随包导出、相对谱面目录解析。 |
timing
| 字段 | 类型 | 说明 |
|---|---|---|
offset |
float | 音频偏移量(秒);正值将谱面相对音频延迟(音频起始若有空白,使用正值把谱面零点推至音乐实际开始位置) |
bpm_changes |
array | BPM 变速点列表 |
每个 bpm_changes 条目:
| 字段 | 类型 | 说明 |
|---|---|---|
time |
float | 新 BPM 生效的时间点(秒) |
bpm |
float | BPM 数值 |
beats_per_measure |
int | 可选 — 编辑器时间轴拍号分子,默认 4 |
beat_unit |
int | 可选 — 编辑器时间轴拍号分母,默认 4 |
beats_per_measure 与 beat_unit 只影响编辑器的小节线 / 标尺显示,不改变游戏内判定时序。
theme_events
可选的谱面主题切换数组。不使用该功能时会省略。
| 字段 | 类型 | 说明 |
|---|---|---|
time |
float | 切换开始的谱面时间(秒) |
target_t |
float | 主题混合目标,0.0 = 暗色,1.0 = 亮色 |
transition_dur |
float | 切换时长(秒),默认 0.6 |
force |
bool | 可选 — 为 true 时强制应用该事件,即使玩家主题设置通常会覆盖谱面驱动主题 |
除数值 target_t 外,事件也可写 "mode": "dark" 或 "mode": "light",便于手改谱面(两者同时存在时以 target_t 为准)。transition_dur 为 0 表示瞬间切换、无渐变。time 为 0.0 的事件无论 transition_dur 为何都瞬切,以便在首帧绘制前确立谱面初始主题。若 theme_events 为空或缺省,引擎会在 0.0 秒插入一个隐含的暗色锚点(target_t 0.0),因此不使用该功能的谱面读作全暗。
approach_speed_events
可选的谱面内缩圈速度变更点数组。整首歌都使用普通 1.0x 缩圈运动时省略。
如果没有 0.0 秒事件,谱面等价于隐含存在 { "time": 0.0, "speed": 1.0 }。
| 字段 | 类型 | 说明 |
|---|---|---|
time |
float | 该缩圈速度开始生效的谱面时间点(秒) |
speed |
float | 非负缩圈运动倍率。0.0 表示暂停,1.0 表示正常速度,大于 1.0 表示加速 |
缩圈速度事件只影响音符本体/缩圈的出现时间与缩圈进度,不改变判定时间、BPM、音频时间、玩家 Ring Speed 设置,也不替代每个 note 自身的 ring_time_mult / ring_scale_mult;这些值会和谱面内缩圈速度事件复合计算。
notes
音符对象数组。建议按 time 升序排列,但非强制要求——引擎加载时会重新排序。
坐标系统
所有位置使用归一化坐标,范围 [0.0, 1.0]:
x:0.0= 左边缘,1.0= 右边缘y:0.0= 上边缘,1.0= 下边缘
该设计保证谱面跨分辨率一致。
音符类型
每个音符必有 type、time、x、y。特定类型附带额外字段。以下两个可选字段可出现在任意类型上:
| 字段 | 类型 | 说明 |
|---|---|---|
ring_time_mult |
float | 提示圈持续时间倍率,1.0 = 使用全局值。等于 1.0 时省略 |
ring_scale_mult |
float | 提示圈大小倍率,1.0 = 使用全局值。等于 1.0 时省略 |
Tap / Ex-Tap
标准单点音符。Ex-Tap 使用相同几何结构,不会出现 Pure+ 或 Break 以外的判定,并有视觉强调。
{ "type": "tap", "time": 1.5, "x": 0.3, "y": 0.6 }
{ "type": "ex-tap", "time": 2.0, "x": 0.4, "y": 0.6 }
Hold / Ex-Hold
在 time 按下并持续至 end_time。
{ "type": "hold", "time": 2.0, "end_time": 3.5, "x": 0.5, "y": 0.5 }
{ "type": "ex-hold", "time": 4.0, "end_time": 5.0, "x": 0.5, "y": 0.5 }
Slide / Ex-Slide
在第一个路径点的时间按下,然后手指拖动slide 沿路径移动到终点。外层 time、x、y 与第一个路径点一致。
{
"type": "slide",
"time": 4.0, "x": 0.2, "y": 0.5,
"path": [
{ "x": 0.2, "y": 0.5, "time": 4.0 },
{ "x": 0.5, "y": 0.3, "time": 4.5 },
{ "x": 0.8, "y": 0.5, "time": 5.0 }
]
}
中间节点与首末节点的区分按位置决定:index 0 为 head(画真节点圆点、进行 tap 判定、首节点 tap 音效),最末 index 为 tail(画真节点圆点、决定终点位置),其余 index 一律为中间节点(按倒角弧渲染——不画圆点、经过时不发出音效、不产生判定)。当前格式不再带 fake 字段;老谱面如果带了 fake 仍可正常加载(loader 静默忽略其值),编辑器重存后 fake 字段会从输出中消失。
Bezier 曲线段(可选):每个路径点还可以带 curve_in 字段描述该点的入边段形状——"line"(默认,省略不写)或 "bezier"。当 curve_in == "bezier" 时,必须配套写 cp_x / cp_y(二次 Bezier 的控制点,使用 chart 标准化坐标 0..1)。曲线公式:
P(t) = (1-t)² · P_prev + 2t(1-t) · CP + t² · P_this , t ∈ [0, 1]
首节点(index 0)没有入边段,其 curve_in 在加载时被强制为 "line"。游戏侧和编辑器侧都用相同的 N_BEZIER_SAMPLES = 16 采样密度,distance / centerline 投影 / 渲染都基于该采样的折线近似。fillet(拐角圆滑)在曲线段两侧自动跳过——曲线本身已经平滑过拐角,重复处理会显得怪。
中间节点的 time 字段:仍然有意义——驱动轨迹上 guide light(指引亮点)沿轨迹的运动节奏。Guide light 在 path[k].time 时刻位于 path[k] 对应的渲染曲线位置,提供”现在应该划到哪里”的视觉参考。
on_time 字段(可选,默认 true):每个路径点可显式写 "on_time": false 表示「该点不参与 autoplay / guide-light 的时间锚定」。当为 false,guide light 与编辑器预览体不会在该点的 time 锚定位置;而是把周围相邻的 on-time 节点对当作时间锚 A、B,按”经过时长比例 × A→B 的总弧长 = 目标弧长”匀速通过。典型用途:纯粹用来塑形的几何锚(Bezier 控制中段、急转弯等),其 time 是为画路径形状而填的而非节拍,标为非 on-time 可避免本体在那个时间点上视觉”卡顿”。首节点与末节点强制为 on-time(loader 自动校正)。判定窗口、得分、持有、末段判定、真节点 SFX 时机都不受影响——这个字段仅影响 autoplay 节奏。序列化器在值为 true 时省略字段,未启用该特性的旧谱字节级保留。
Slide 玩法(finger-driven 模型):
- 首节点按 tap 判定(同 tap note);
- 首节点判定确定后(真 tap 或 ±120ms 后自动 break),slide 进入拖动状态——slide 本体跟随按住它的手指(手指在 slide 当前位置 240px 内即视为持有);
- 中段分段判定:
[首节点 + 120ms, 末节点]按 240ms 切段(末尾余量 ≥ 180ms 自成一段;更短的余量并入前一段,不增加物量,让最后一个中段判定落在 slide 结束时刻)。每段是独立判定单元、drag 式二值:段内任一时刻”被持有 AND slide 位于轨迹 240px 内”→ Pure+,否则 → Break; - 终点判定(独立 1 单元):在末节点 ±120ms 内,本体到末节点的最近距离 ≤240px → Pure+,否则 Break;
- 首节点 tap 也是独立单元——没有合成评级:每个单元在其结算时刻立即独立计分。短 slide(<300ms)无中段(仅首节点 + 终点)。
Drag
无需点击。判定时间内任何手指位于命中区域即可触发。
{ "type": "drag", "time": 6.0, "x": 0.5, "y": 0.4 }
Flick
带方向的快速滑动。
{ "type": "flick", "time": 7.0, "x": 0.5, "y": 0.5, "direction": 90 }
direction:角度(度数)——0 = 右,90 = 上,180 = 左,270 = 下。
判定
每个音符的触发时机依据以下四档评定:
| 评级 | 含义 |
|---|---|
| Pure+ | 最高精度命中 |
| Pure | 标准命中 |
| Connect | 偏早或偏晚,但仍在判定窗口内 |
| Break | 漏掉或超出判定窗口 |
具体的时序判定逻辑依音符类型而异:
- Tap / Ex-Tap / Hold / Ex-Hold / Slide / Ex-Slide(起始点):按相对判定时间的绝对距离,在判定窗口内按差值分档。
- Drag:只要命中在判定窗口内,一律为 Pure+。
- Flick:窗口内方向匹配(±45°)的划动 → Pure+;其余情况(包括只点击不划动)→ Break。
- Hold / Ex-Hold 与 Slide / Ex-Slide(持续段):按独立判定单元计分——首节点 tap 一个单元;中段
[起始 + 120ms, 结束]每 240ms 一段(段内出现过有效按压 → Pure+,否则 Break,slide 另需本体在轨迹上),各一个单元;slide 终点再加一个二值单元。谱面总物量 = 全部单元之和,长 hold/slide 像一串 drag 一样贡献多个判定。短于 300ms 的不产生中段。
包结构示例
mynick_mysong.yzcpkg (ZIP)
├── song.json
├── Stable.yzc.json
├── Drifting.yzc.json
├── Faint.yzc.json
├── Relink.yzc.json
├── music.ogg
└── cover.jpg