概述

noctilink 以 .yzcpkg 包的形式分发歌曲,这是标准 ZIP 压缩格式,内部结构如下:

  • song.json:包级元数据与各难度索引
  • 每个难度一个 .yzc.json 文件(Stable / Drifting / Fracture / Relink)
  • 一个音频文件(.mp3.ogg
  • 一个封面图文件(可选,.jpg.png

两种 JSON 格式当前版本均为 1song.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": "Fracture", "level": "10", "chart_constant": 10.0, "chart": "Fracture.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""Fracture""Relink" 之一。读取端会把旧值 "Faint" 兼容为 "Fracture";新保存/导出不会再写旧值。
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": [ ... ]
}

编辑器保存谱面时会规范化数值输出,以缩小 .yzc.json 文件体积:时间类字段按秒保留 5 位小数,归一化坐标保留 3 位小数,大多数非坐标数值参数保留 6 位小数。编辑过程中的内存值仍保留完整浮点精度,直到保存时才量化。

metadata

字段 类型 说明
title string 歌曲名
artist string 艺术家
charter string 制谱者
cover_source string 封面来源说明(作者、链接、委托备注);如 song.json 的 charts[] 条目里也写了会覆盖此处
difficulty_name string 必须与父 song.json 条目中的 difficulty 一致
difficulty_level string 难度等级,自由格式字符串
display_bpm string 可选 — 不为空时作为该谱面在选曲页显示的 BPM 文本;为空或缺省时继续从该谱面的 timing 数据计算
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 数值
reset_beat_color bool 可选 — 为 true 时,从该事件处重新计算 beat-snap note 颜色,使该时间点的 note 被视为正拍红色起点;默认 false
beats_per_measure int 可选 — 编辑器时间轴拍号分子,默认 4
beat_unit int 可选 — 编辑器时间轴拍号分母,默认 4

beats_per_measurebeat_unit 只影响编辑器的小节线 / 标尺显示,不改变游戏内判定时序。 reset_beat_color 只影响游戏与编辑器预览中的 beat-snap 颜色分类,不改变时序、吸附、判定或小节编号。

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_dur0 表示瞬间切换、无渐变。time0.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 表示加速

缩圈速度事件只影响预判视觉运动。原始进度以 note 判定时刻的 1.0 为终点锚点,并且不截断:越过 0.01.0 后会保留全部超出量,之后反向运动必须先补偿完这部分,视觉才会重新跨过边界。音频、BPM、谱面时间和判定时间都不会倒退。

进度处于 [0, 1] 时,note 本体、approach circle 和 Slide 轨迹使用同一个进度。瞬时 note 在区间外隐藏;Hold / Slide 在进度大于 1 时按无副作用的虚拟 autoplay 时间继续显示并快进到自己的终点,之后若倒退补偿回来仍可重新出现。虚拟快进不会触发得分、判定、音效、触摸归属或游玩特效。

notes

音符对象数组。建议按 time 升序排列,但非强制要求——引擎加载时会重新排序。


坐标系统

所有位置使用归一化坐标,范围 [0.0, 1.0]

  • x0.0 = 左边缘,1.0 = 右边缘
  • y0.0 = 上边缘,1.0 = 下边缘

该设计保证谱面跨分辨率一致。


音符类型

每个音符必有 typetimexy。特定类型附带额外字段。以下可选字段可出现在任意类型上:

字段 类型 说明
ring_time_mult float 传统出现时长倍率,1.0 = 使用全局值。等于 1.0 时省略;approach_speed_events 非空时忽略
ring_scale_mult float 提示圈大小倍率,1.0 = 使用全局值。等于 1.0 时省略
approach_speed_events array 单 note 分段预判运动;非空数组会选择分段模式,取代 ring_time_mult

单 note 的每条 approach_speed_events 包含相对判定时刻的秒数 offsetoffset <= 0)和有符号 speed。事件按 offset 排序;offset 重复时以后者为准。第一条事件是该 note 的激活 / 生成时刻,不是隐含的进度 0 起点,因此激活时的原始进度可以是任意值。激活后实际速度为“全局速度 × 单 note 速度”;ring_scale_mult 在两种计时模式下都独立生效。

{
  "type": "tap",
  "time": 12.0,
  "x": 0.25,
  "y": 0.5,
  "ring_scale_mult": 1.2,
  "approach_speed_events": [
    { "offset": -3.0, "speed": 1.0 },
    { "offset": -2.0, "speed": -1.0 },
    { "offset": -1.0, "speed": 2.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 }

Drag Hold

Drag 头接 Hold 持续段。time 时不要求新点击:判定窗内只要已有触点位于命中区域,头判即为 Pure+;若到 time + 120ms 仍未命中则为 Break。头判始终立即作为一个独立单元结算,之后的中段分段、失误恢复与 Hold 相同。短于 300ms 时没有中段,只有这一个 Drag 头单元;音符本体仍显示到 end_time

{ "type": "drag-hold", "time": 5.0, "end_time": 6.0, "x": 0.5, "y": 0.5 }

命中前本体是普通 Hold 的 0.75× 空心外壳,内部保留 0.75 × 0.55R 的 Drag 环。命中后在 min(120ms, end_time - time) 内与 duration arc 同步放大至普通 Hold 尺寸,并用当前 note skin 的填充色由圆心向外覆盖内部 Drag 环。

Slide / Ex-Slide

在第一个路径点的时间按下,然后手指拖动slide 沿路径移动到终点。外层 timexy 与第一个路径点一致。

{
  "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 字段会从输出中消失。

曲线段(可选):每个路径点还可以带 curve_in 字段描述该点的入边段形状——"line"(默认,省略不写)、"bezier""arc"。当 curve_in"bezier""arc" 时,必须配套写 cp_x / cp_y参考点,使用 chart 标准化坐标 0..1)。加载时无法识别的 curve_in 值一律按 "line" 处理(向前兼容——旧版本打开新曲线类型会退化成直线而非报错)。

  • "bezier":参考点是曲线外的二次 Bezier 控制点(曲线被它「拉拽」但不经过它)。公式:

    P(t) = (1-t)² · P_prev + 2t(1-t) · CP + t² · P_this   ,   t ∈ [0, 1]
    
  • "arc":参考点是曲线的一点——线段是从上一个节点、经过参考点、到这个节点的圆弧(即 P_prev、CP、P_this 三点外接圆中包含 CP 的那段弧)。当三点共线时(参考点落在 P_prev→P_this 的直线上,外接圆半径无穷大),该段退化为直线。

首节点(index 0)没有入边段,其 curve_in 在加载时被强制为 "line"。游戏侧和编辑器侧对每种曲线采样一致——Bezier 用 N_BEZIER_SAMPLES = 16,arc 按扫掠角自适应(约每 3° 一段,宽弧也平滑、缓弧不浪费;共线 arc 退化为 2 点直线)。distance / centerline 投影 / 渲染都基于该采样的折线近似,保证预览与实机一致。拐角处用倒角圆弧平滑相接(极尖的拐角会跳过倒角,由曲线/线段本身定形)。

中间节点的 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 本体跟随已有 owner;owner 仍在本体当前位置 240px 内时保持持有,owner 失效后可由本体当前位置 400px 内最近的空闲触点重捕获;
  • 中段分段判定:[首节点 + 120ms, 末节点] 按 240ms 切段(末尾余量 ≥ 180ms 自成一段;更短的余量并入前一段,不增加物量,让最后一个中段判定落在 slide 结束时刻)。每段是独立判定单元、drag 式二值:段内任一时刻”被持有 AND slide 位于轨迹 240px 内”→ Pure+,否则先成为一个滚动 pending 失败单元,不立即 Break。下一段首次恢复有效输入的同一帧,pending 立即补为 Pure+,当前段自身仍在段尾结算;若下一段始终无效,旧 pending 在该段段尾 Break,当前失败段接替为 pending。slide 结束时剩余 pending 立即 Break,不把宽限延长到音符结束之后;
  • 终点判定(独立 1 单元):在末节点 ±120ms 内,本体到末节点的最近距离 ≤240px → Pure+,否则 Break;
  • 首节点 tap 也是独立单元——没有合成评级:每个单元在其结算时刻独立计分(当前有效段在段尾,获救 pending 在恢复输入帧)。短 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 / Drag Hold(起始点):只要触点在判定窗口内进入命中区域,一律为 Pure+;Drag Hold 未命中的头在 +120ms 结算 Break,随后仍可恢复中段。
  • Flick:窗口内方向匹配(±45°)的划动 → Pure+;其余情况(包括只点击不划动)→ Break。
  • Hold / Ex-Hold / Drag Hold 与 Slide / Ex-Slide(持续段):按独立判定单元计分——首节点一个单元;中段 [起始 + 120ms, 结束] 每 240ms 一段(段内出现过有效按压 → Pure+,slide 另需本体在轨迹上),各一个单元;slide 终点再加一个二值单元。失败中段先保留为至多一个滚动 pending,不立即 Break:下一段首次恢复有效输入的同一帧,pending 立即补为 Pure+;若连续第二段仍无效,旧 pending 在该段段尾 Break,当前失败段继续 pending;音符结束时剩余 pending 立即 Break。当前有效段自身仍在段尾结算,且宽限不延伸到音符结束之后。谱面总物量 = 全部单元之和,长 hold/drag-hold/slide 像一串 drag 一样贡献多个判定。短于 300ms 的不产生中段。

包结构示例

mynick_mysong.yzcpkg  (ZIP)
├── song.json
├── Stable.yzc.json
├── Drifting.yzc.json
├── Fracture.yzc.json
├── Relink.yzc.json
├── music.ogg
└── cover.jpg