游戏运行教程 ============ 本教程演示 Sync 的运行时流程:**构建谱面 → 编译 → 自写判定 → 命中收集**。Sync **不带判定引擎**——本教程演示的 就是"如何用最少的 GDScript 在引擎外面写一个最小的判定"。 基于 ``Sync/project/play_demo.gd`` 的真实示例。 步骤 1:构建谱面 ---------------- 创建 1 条 TAP 轨(含一个自定义种类的音符作为示例)和 1 条 HOLD 轨: .. code-block:: gdscript 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 步骤 2:创建播放器并编译 ------------------------ .. code-block:: gdscript var player := SyncChartPlayer.new() add_child(player) player.set_document_chart(chart) player.play() ``set_document_chart`` 内部完成编译;之后可以从 ``player.get_play_chart()`` 拿到编译后的 ``SyncChart`` 做枚举。 步骤 3:判初——枚举所有音符并缓存静态数据 ---------------------------------------- .. code-block:: gdscript 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() 步骤 4:每帧推进 + 自写判定 ---------------------------- 判定只读 ``info.hit_time_ms`` / ``info.end_time_ms`` 与当前 ``player.get_chart_progress_ms()`` 的差值——这就是最简判定: .. code-block:: gdscript 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() 判定层的关键数据(不修改引擎数据) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``SyncChart.get_note_info(note_id)`` 返回的字典里,判定需要的所有字段: - ``kind`` (int) — 0=TAP / 1=HOLD(游戏种类在 ``metadata``) - ``hit_time_ms`` (float) — 头部命中时间 - ``end_time_ms`` (float) — HOLD 结束时间;TAP 为 0 - ``is_hold`` (bool) — ``kind == 1`` 的便捷判断 - ``logic_id`` (String) — 唯一 ID - ``track_id`` (String) - ``metadata`` (Dictionary) — 游戏专属数据(如 ``note_type``) .. tip:: 渲染层每帧用 ``player.get_visible_note_ids_for_track(track_id)`` 拿"当前可见音符";判定层用 ``SyncChart.get_visible_note_ids(time_ms)`` 拿任意时间的可见集合(不受 ``seek_ms`` 缓存影响)。两个 API 在大谱面下都是 O(1)(C++ 按轨分桶)。 步骤 5:自定义种类的判定分发 ---------------------------- ``metadata["note_type"]`` 是约定的游戏种类字符串(也可以自己定)。 判定里 ``match``/``if`` 字符串即可: .. code-block:: gdscript 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:输出总结 ---------------- .. code-block:: gdscript func _finalize() -> void: _finalized = true print("=== final ===") print(" judged events = %d (notes = %d)" % [_hits, _notes.size()]) _player.stop() get_tree().quit() 完整代码 -------- 完整可运行示例见 ``OpenSource/Sync/project/play_demo.gd``: 约 130 行 GDScript(其中手写判定约 20 行),全部基于上面演示的 API。 判定层与渲染层的边界 -------------------- - **判定层**:使用 ``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()``,按可见音符集合更新 你的渲染节点。 两者**不共享任何状态**——引擎是纯无状态的"数据 + 时间"核心。 下一步 ------ - :doc:`/sync/overview` — "无判定引擎 — by design" 节 列出判定所需全部 API。 - :doc:`/sync/api_reference` — ``SyncChart`` / ``SyncChartPlayer`` 的完整 API。 - :doc:`/advanced/custom_rendering` — 渲染层落点。