6.3. 游戏运行教程
本教程演示 Sync 的运行时流程:构建谱面 → 编译 → 自写判定 → 命中收集。Sync 不带判定引擎——本教程演示的 就是"如何用最少的 GDScript 在引擎外面写一个最小的判定"。
基于 Sync/project/play_demo.gd 的真实示例。
6.3.1. 步骤 1:构建谱面
创建 1 条 TAP 轨(含一个自定义种类的音符作为示例)和 1 条 HOLD 轨:
func _build_chart() -> SyncDocumentChart:
var chart := SyncDocumentChart.new()
chart.bpm = 120.0
chart.offset_ms = 0.0
# Track 1:TAPs,每 4 拍 1 个;第三个带自定义种类 metadata
var taps := SyncTrack.new()
taps.track_id = "Track-Taps"
for beat in 8:
var n := SyncNote.new()
n.local_id = "tap_%d" % beat
n.kind = 0 # TAP
n.time = _beat(beat * 4, 0, 1)
if beat == 2:
n.metadata = { "note_type": "catch" } # 游戏专属种类
taps.add_note(n)
chart.add_root_track(taps)
# Track 2:单个 HOLD(8~16 拍)
var holds := SyncTrack.new()
holds.track_id = "Track-Holds"
var h := SyncNote.new()
h.local_id = "hold_0"
h.kind = 1 # HOLD
h.time = _beat(8, 0, 1)
h.end_time = _beat(16, 0, 1)
holds.add_note(h)
chart.add_root_track(holds)
return chart
6.3.2. 步骤 2:创建播放器并编译
var player := SyncChartPlayer.new()
add_child(player)
player.set_document_chart(chart)
player.play()
set_document_chart 内部完成编译;之后可以从
player.get_play_chart() 拿到编译后的 SyncChart 做枚举。
6.3.3. 步骤 3:判初——枚举所有音符并缓存静态数据
var _notes: Array = [] # 缓存 get_note_info() 结果
func _ready() -> void:
_chart = _build_chart()
_player = SyncChartPlayer.new()
add_child(_player)
_player.set_document_chart(_chart)
_play_chart = _player.get_play_chart()
# 一次性枚举所有 track × 所有 note,缓存 info
for track_id in _play_chart.get_track_ids():
for note_id in _play_chart.get_note_ids_for_track(track_id):
var info: Dictionary = _play_chart.get_note_info(note_id)
info["head_done"] = false
info["tail_done"] = false
_notes.append(info)
_player.play()
6.3.4. 步骤 4:每帧推进 + 自写判定
判定只读 info.hit_time_ms / info.end_time_ms 与当前
player.get_chart_progress_ms() 的差值——这就是最简判定:
func _auto_judge_tick() -> void:
var now := _player.get_chart_progress_ms()
var visible_now := _play_chart.get_visible_note_ids(now).size()
for info in _notes:
if not info.head_done and now >= info.hit_time_ms:
info.head_done = true
_hits += 1
_log("hit %s kind=%d note_type=%s t=%.0f (visible=%d)" % [
info.logic_id, info.kind,
info.metadata.get("note_type", "-"),
info.hit_time_ms, visible_now,
])
if info.is_hold and not info.tail_done and now >= info.end_time_ms:
info.tail_done = true
_hits += 1
_log("tail %s t=%.0f" % [info.logic_id, info.end_time_ms])
func _process(_delta: float) -> void:
_t_ms += TICK_MS
if _t_ms > RUN_MS:
_finalize()
return
_player.seek_ms(_t_ms)
_auto_judge_tick()
6.3.4.1. 判定层的关键数据(不修改引擎数据)
SyncChart.get_note_info(note_id) 返回的字典里,判定需要的所有字段:
kind(int) — 0=TAP / 1=HOLD(游戏种类在metadata)hit_time_ms(float) — 头部命中时间end_time_ms(float) — HOLD 结束时间;TAP 为 0is_hold(bool) —kind == 1的便捷判断logic_id(String) — 唯一 IDtrack_id(String)metadata(Dictionary) — 游戏专属数据(如note_type)
小技巧
渲染层每帧用 player.get_visible_note_ids_for_track(track_id)
拿"当前可见音符";判定层用 SyncChart.get_visible_note_ids(time_ms)
拿任意时间的可见集合(不受 seek_ms 缓存影响)。两个 API
在大谱面下都是 O(1)(C++ 按轨分桶)。
6.3.5. 步骤 5:自定义种类的判定分发
metadata["note_type"] 是约定的游戏种类字符串(也可以自己定)。
判定里 match/if 字符串即可:
if not info.head_done and now >= info.hit_time_ms:
info.head_done = true
match info.metadata.get("note_type", "tap"):
"tap":
_log("TAP hit %s" % info.logic_id)
"catch":
# 到线时手指在判定区即中;判定在更上层做
_log("CATCH target %s" % info.logic_id)
_:
_log("unknown kind hit %s" % info.logic_id)
引擎对自定义种类**完全透明**:任何非 kind=1 的音符都被当作 TAP
处理,可见性、位置采样、序列化全部按 TAP 工作。你的判定只需要识别
metadata 字符串就够了。
6.3.6. 步骤 6:输出总结
func _finalize() -> void:
_finalized = true
print("=== final ===")
print(" judged events = %d (notes = %d)" % [_hits, _notes.size()])
_player.stop()
get_tree().quit()
6.3.7. 完整代码
完整可运行示例见 OpenSource/Sync/project/play_demo.gd:
约 130 行 GDScript(其中手写判定约 20 行),全部基于上面演示的 API。
6.3.8. 判定层与渲染层的边界
判定层:使用
SyncChart.get_note_info()/get_track_ids()/get_note_ids_for_track()+player.get_chart_progress_ms()。 与玩家输入(你自己的 InputAgent)耦合;判定状态、分数、连击 都在 GDScript 里维护。渲染层:使用
player.get_visible_note_ids_for_track()/get_play_chart().get_note_info()+player.sample_track_*_axis()/track.get_note_position_fast(),按可见音符集合更新 你的渲染节点。
两者**不共享任何状态**——引擎是纯无状态的"数据 + 时间"核心。
6.3.9. 下一步
Sync 包概览 — "无判定引擎 — by design" 节 列出判定所需全部 API。
Sync API 参考 —
SyncChart/SyncChartPlayer的完整 API。自定义渲染指南 — 渲染层落点。