4. 快速开始

Sync 提供两条快速上手的路径:制谱(编辑器)播放(运行时)。 两者都在同一个包(addons/sync)里,你可以按需选择其一,或两者都试试。

4.1. 快速开始:编辑器(制谱)

以下代码展示如何用 Sync 创建一个谱面、添加轨道和音符、并保存为 JSON:

# 1. 创建谱面文档
var chart := SyncDocumentChart.new()
chart.bpm = 120
chart.offset_ms = 0

# 2. 添加 BPM 变化(可选的:在第一节拍处设 140 BPM)
chart.bpm_axis.add_keyframe_s(SyncBeat.from_beat_float(0), 140)

# 3. 创建第一条轨道(地面轨道 Lane 0)
var track := SyncTrack.new()
track.metadata["type"] = "GroundTrack"
track.metadata["lane"] = 0
chart.tracks.append(track)

# 4. 添加一个 TAP 音符(在第 4 拍上)
var note := SyncNote.new()
note.kind = 0                      # 0 = TAP
note.time = SyncBeat.from_beat_float(4)
track.notes.append(note)

# 5. 添加一个 HOLD 音符(第 8~16 拍)
var hold := SyncNote.new()
hold.kind = 1                      # 1 = HOLD
hold.time = SyncBeat.from_beat_float(8)
hold.end_time = SyncBeat.from_beat_float(16)
track.notes.append(hold)

# 6. 创建一条会移动、旋转的轨道:给它绑定动画轴。
#    位移 vector2_axes / 旋转 float_axes / 流速 speed_axes。
#
#    注意:绑定轴 ≠ 自动动画。除 speed / BPM 轴(播放引擎自动消费)
#    外,其它轴只是把关键帧数据挂在轨道上,数值需要你手动采样——
#    调用轴内置方法(如 sample(time_ms, bpm_axis))。采样要传当前
#    谱面进度,这就是 SyncProgressManager 出场的时候:
#    它统一管理谱面 / 音频进度(播放、seek、变速),供轴采样使用。
var child_track := SyncTrack.new()
child_track.vector2_axes.append(SyncVector2Axis.new())   # 位移
child_track.float_axes.append(SyncFloatAxis.new())       # 旋转
child_track.speed_axes.append(SyncSpeedAxis.new())       # 流速
track.tracks.append(child_track)

# 7. 添加音符
var note2 := SyncNote.new()
note2.kind = 0
note2.time = SyncBeat.from_beat_float(12)
child_track.notes.append(note2)

# 8. 保存为 JSON 文件
var json_str := chart.to_json()
var file := FileAccess.open("user://song.sync", FileAccess.WRITE)
file.store_string(json_str)
file.close()

# 9. 从 JSON 加载谱面
var loaded := FileAccess.open("user://song.sync", FileAccess.READ)
var chart2 := SyncDocumentChart.from_json(loaded.get_as_text())

关键要点:

  • SyncDocumentChart 是谱面根节点,持有 BPM 轴和轨道列表

  • SyncTrack 可以嵌套:track.tracks.append(child) 创建子轨道

  • 动画轴不自动播放——除 speed / BPM 轴(播放引擎自动消费)外, float / Vector2 等轴只是把关键帧数据挂在轨道上,绑定 ≠ 自动动画; 数值用轴内置方法手动取(如 sample(time_ms, bpm_axis))。 采样需要当前谱面进度——编辑器场景用 SyncProgressManager 统一管理谱面 / 音频进度(播放、seek、变速),运行时场景用 SyncChartPlayer 推进进度

  • 所有时间用 SyncBeat 表示(节拍 + 分子/分母),由 SyncBpmAxis 换算为毫秒

  • to_json() / from_json() 实现谱面序列化,方便脚本批处理

4.2. 快速开始:播放(运行时)

Sync 不带判定引擎——你在自己的 GDScript 里读 player.get_chart_progress_ms() 并对照每个音符的 hit_time_ms / end_time_ms 做判定。引擎保证判定所需的全部数据精确可用。

以下代码展示最小化的"创建谱面 → 编译 → 每帧枚举可见音符 → 自写判定":

# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
# 初始化(通常在 _ready() 中)
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

# 1. 创建谱面
var chart := SyncDocumentChart.new()
chart.bpm = 160
var track := SyncTrack.new()
track.track_id = "main"
chart.add_root_track(track)

# 2. 添加 TAP 音符
var note := SyncNote.new()
note.kind = 0              # TAP
note.local_id = "tap_1"
note.time = _beat(4)
track.add_note(note)

# 3. 创建播放器(同步编译谱面)
var player := SyncChartPlayer.new()
add_child(player)
player.set_document_chart(chart)

# 可选:设备延迟(正延迟使音频提前播放)
player.set_delay_ms(40)

# 4. 启动播放
player.play()

# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
# 每帧循环(通常在 _process(delta) 中)
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

func _process(_delta: float) -> void:
    # 5. 当前谱面毫秒
    var now_ms: float = player.get_chart_progress_ms()

    # 6. 枚举当前可见音符(用于渲染与判定)
    for track_id in player.get_play_chart().get_track_ids():
        for note_id in player.get_visible_note_ids_for_track(track_id):
            var info: Dictionary = player.get_play_chart().get_note_info(note_id)
            # info.kind / info.hit_time_ms / info.end_time_ms / info.is_hold
            # / info.metadata
            # → 你的判定:把 hit_time_ms 与 now_ms 比一比就行

    # 7. 播放结束
    if now_ms >= player.get_duration_ms():
        get_tree().quit()

func _beat(b: int) -> SyncBeat:
    var bt := SyncBeat.new()
    bt.beat = b
    return bt

关键要点:

  • SyncChartPlayer 是墙钟驱动播放引擎。它推进 progress_ms, 暴露 get_chart_progress_ms()get_duration_ms(),并对每条 track 按轨分桶 O(1) 地维护可见音符 ID 列表(get_visible_note_ids_for_track)。

  • 关键静态字段: SyncChart.get_note_info 返回的字典包含 kind / hit_time_ms / end_time_ms / is_hold / track_id / metadata。 你的判定只读这些字段;beat_to_ms 不要在每帧调用(应该在 setup 阶段缓存)。

  • get_visible_note_ids_for_track渲染 用的(按时间窗裁剪、 按轨分桶的 O(1) 查询);判定可以走 SyncChart.get_note_info() 全谱枚举,或对照 get_visible_notes_info() 都行——见 Sync/project/play_demo.gd 的最小手写判定示例。

  • 自己的判定要决定 kind 怎么处理:引擎把任何非 kind=1 的音符 当作 TAP(单点时间语义);游戏专属种类(CATCH / VOID / 长按 / 自定义)放在 note.metadata (如 metadata["note_type"]), 在你的判定里按字符串分发。

4.3. 快速开始:对象池(SyncNodePool)

SyncNodePool 是绑定 PackedScene 的节点对象池(0.5.0 回归), 编辑与游玩侧共用。对象池复用音符等高频实例,避免反复 instantiate / queue_free

# 1. 创建并绑定场景
var pool := SyncNodePool.new()
add_child(pool)
pool.bind_scene(preload("res://note.tscn"))

# 2. 预热:预创建 16 个空闲实例
pool.prewarm(16)

# 3. 取用 / 归还
var node := pool.acquire()       # 有空闲则复用,否则新实例化
node.global_position = some_pos
# ... 使用 node ...
pool.release(node)               # 归还复用,不销毁

# 4. 查询状态
print(pool.is_bound(), pool.get_scene())
print(pool.get_active_count(), pool.get_free_count())

关键要点:

  • acquire() 返回一个节点实例;release(node) 把节点归还池中复用

  • prewarm(count) 提前创建 count 个空闲实例

  • get_active_count() / get_free_count() 分别统计已取出与 空闲的实例数量

4.4. 下一步

这两段代码展示了 Sync 的核心用法。更完整的示例参见 游戏运行教程 (含最小手写判定),以及 Sync 包概览 的 "No judgement engine — by design" 节。