编辑器组件教程 ============== 本教程介绍 Sync 包的 3 个编辑器组件: ``SyncProgressManager``、``SyncTimeline`` 和 ``SyncBeatGrid``。 这些组件只提供**坐标映射和节拍线数据**,渲染和输入命中检测由你的 GDScript 完成。 基于 ``Sync/project/editor_demo_components.gd`` 的完整示例。 步骤 1:SyncProgressManager — 进度管理 -------------------------------------- ``SyncProgressManager`` 是 ``SyncDocumentChart`` 的伴生播放引擎, 用于制谱/预览时的进度管理。时长模型 = 音乐时长 + 前后留白(默认各 5s)。 .. code-block:: gdscript var pm := SyncProgressManager.new() add_child(pm) pm.bind_chart(chart) # seek 到第 4 拍(120 BPM 下 = 2000ms) pm.seek_beats(4.0) print("进度: %.1f ms" % pm.get_progress_ms()) print("时长: %.1f ms" % pm.get_duration_ms()) print("节拍文本: %s" % pm.get_beat_string()) 步骤 2:SyncTimeline — 归一化坐标映射 -------------------------------------- ``SyncTimeline`` 提供时间 ↔ 归一化坐标的映射(底部=0,顶部=1): .. code-block:: gdscript var timeline := SyncTimeline.new() timeline.progress_manager = pm # 当前进度在归一化坐标中的位置 var pos_now := timeline.time_to_normalized(pm.get_progress_ms()) # 未来 1000ms 的位置 var pos_future := timeline.time_to_normalized(pm.get_progress_ms() + 1000.0) # 归一化坐标 → 时间 var t_back := timeline.normalized_to_time(1.0) 步骤 3:输入映射 — 点击位置 → 节拍 ------------------------------------- .. code-block:: gdscript # 玩家在归一化位置 0.575 处点击 var click_pos := 0.575 var beat := timeline.normalized_to_beat(click_pos) print("点击 %d:%d/%d" % [beat.beat, beat.numerator, beat.denominator]) 步骤 4:SyncBeatGrid — 节拍线数据 ---------------------------------- ``SyncBeatGrid`` 生成节拍线数据(位置、类型、标签、颜色),你负责绘制: .. code-block:: gdscript var grid := SyncBeatGrid.new() grid.timeline = timeline var lines := grid.get_lines() for line: Dictionary in lines: if line.type != SyncBeatGrid.TYPE_SUBDIVISION: print("位置=%.3f 类型=%d 拍=%d 标签='%s'" % [ line.position, line.type, line.beat, line.label ]) 步骤 5:自定义颜色 ------------------ .. code-block:: gdscript grid.color_whole_beat = Color.GOLD # 整拍线 grid.color_half_beat = Color.DARK_GOLDENROD grid.color_quarter_beat = Color.GRAY grid.color_subdivision = Color.DIM_GRAY # 细分线 grid.color_judge_line = Color.RED # 判定线 步骤 6:BeatlineView — 完整的绘制 + 输入控件 --------------------------------------------- 以下是一个完整的 ``Control`` 子类,将 ``SyncBeatGrid`` 的数据绘制为 节拍线,并将点击位置映射回节拍: .. code-block:: gdscript class BeatlineView: extends Control var grid: SyncBeatGrid var timeline: SyncTimeline func _ready() -> void: mouse_filter = MOUSE_FILTER_STOP func _draw() -> void: if grid == null or timeline == null: return var h := size.y for line: Dictionary in grid.get_lines(): var pos: float = line.position if pos < 0.0 or pos > 1.0: continue # 裁剪视野外的线 # 归一化(底部=0, 顶部=1)→ 像素(顶部=0) var y := (1.0 - pos) * h var col: Color = line.color draw_line(Vector2(0, y), Vector2(size.x, y), col, line.thickness) if line.label != "": var font := get_theme_default_font() draw_string(font, Vector2(2, y - 2), line.label, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col) func _gui_input(event: InputEvent) -> void: if timeline == null: return var mb := event as InputEventMouseButton if mb != null and mb.pressed and mb.button_index == MOUSE_BUTTON_LEFT: # 像素 → 归一化 var pos := 1.0 - mb.position.y / size.y var beat := timeline.normalized_to_beat(pos) var snapped_ms := timeline.snap_time_ms( timeline.normalized_to_time(pos)) print("点击: pos=%.3f → beat %d:%d/%d, snapped %.1f ms" % [ pos, beat.beat, beat.numerator, beat.denominator, snapped_ms ]) 完整代码 -------- 以下是可以直接运行的完整示例: .. code-block:: gdscript extends Node # 引用 editor_demo_components.gd const ComponentsDemo := preload("res://editor_demo_components.gd") func _ready() -> void: var chart := _build_chart() ComponentsDemo.run(chart, self) get_tree().quit() func _build_chart() -> SyncDocumentChart: var chart := SyncDocumentChart.new() chart.bpm = 120.0 chart.music_duration_ms = 60000.0 var track := SyncTrack.new() track.track_id = "demo_track" for beat in 4: var n := SyncNote.new() n.local_id = "note_%d" % beat n.kind = 0 n.time = _beat(beat * 4, 0, 1) track.add_note(n) chart.add_root_track(track) return chart func _beat(b: int, n: int, d: int) -> SyncBeat: var bt := SyncBeat.new() bt.beat = b; bt.numerator = n; bt.denominator = d return bt 关键设计理念 ------------ - **SyncTimeline / SyncBeatGrid 只做数据输出,不做渲染** — 你可以在任何 UI 框架中自由绘制(Godot Control、自定义 shader 等)。 - 统一约定:时间 = 毫秒、坐标 = 归一化 0~1、 ``progress_ms=-1`` 哨兵 = 取绑定的 progress_manager 当前进度。