6.2. 编辑器组件教程
本教程介绍 Sync 包的 3 个编辑器组件:
SyncProgressManager、SyncTimeline 和 SyncBeatGrid。
这些组件只提供**坐标映射和节拍线数据**,渲染和输入命中检测由你的 GDScript 完成。
基于 Sync/project/editor_demo_components.gd 的完整示例。
6.2.1. 步骤 1:SyncProgressManager — 进度管理
SyncProgressManager 是 SyncDocumentChart 的伴生播放引擎,
用于制谱/预览时的进度管理。时长模型 = 音乐时长 + 前后留白(默认各 5s)。
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())
6.2.2. 步骤 2:SyncTimeline — 归一化坐标映射
SyncTimeline 提供时间 ↔ 归一化坐标的映射(底部=0,顶部=1):
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)
6.2.3. 步骤 3:输入映射 — 点击位置 → 节拍
# 玩家在归一化位置 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])
6.2.4. 步骤 4:SyncBeatGrid — 节拍线数据
SyncBeatGrid 生成节拍线数据(位置、类型、标签、颜色),你负责绘制:
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
])
6.2.5. 步骤 5:自定义颜色
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.2.6. 步骤 6:BeatlineView — 完整的绘制 + 输入控件
以下是一个完整的 Control 子类,将 SyncBeatGrid 的数据绘制为
节拍线,并将点击位置映射回节拍:
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
])
6.2.7. 完整代码
以下是可以直接运行的完整示例:
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
6.2.8. 关键设计理念
SyncTimeline / SyncBeatGrid 只做数据输出,不做渲染 — 你可以在任何 UI 框架中自由绘制(Godot Control、自定义 shader 等)。
统一约定:时间 = 毫秒、坐标 = 归一化 0~1、
progress_ms=-1哨兵 = 取绑定的 progress_manager 当前进度。