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" 节。