6.1. 构建谱面
本教程演示如何用 GDScript 从头构建一个 SyncDocumentChart,
并保存为 JSON 文件或 .res 资源文件。
6.1.1. 1. 创建文档谱面
一切从 SyncDocumentChart 开始:
var chart := SyncDocumentChart.new()
chart.spec_name = "MyChart"
chart.spec_version = "1.0.0"
chart.bpm = 120.0
chart.offset_ms = 0.0
chart.music_duration_ms = 60000.0 # 1分钟
6.1.2. 2. 设置元数据
var metadata := chart.get_metadata_dict()
metadata["creator"] = "YourName"
metadata["difficulty_label"] = "Easy"
metadata["music"] = "Song Title"
metadata["musician"] = "Artist Name"
chart.set_metadata_dict(metadata)
6.1.3. 3. BPM 轴与拍号
BPM 轴可以有多个关键帧实现变速:
# BPM 轴:初始 120,第 16 拍变为 180
var bpm_axis := SyncBpmAxis.new()
bpm_axis.init_value = 120.0
bpm_axis.add_keyframe(SyncBeat.new(), 120.0) # 起始关键帧
bpm_axis.add_keyframe(_beat(16, 0, 1), 180.0)
chart.set_bpm_axis(bpm_axis)
# 拍号:4/4
var tsm := SyncTimeSignatureMap.new()
tsm.add_keyframe(SyncBeat.new(), 4, 4)
chart.set_time_signature_map(tsm)
6.1.4. 4. 创建轨道并添加音符
var track := SyncTrack.new()
track.track_id = "track_001"
track.metadata = { "type": "GroundTrack" }
# TAP 音符(第 4 拍)
var tap := SyncNote.new()
tap.local_id = "tap_001"
tap.kind = 0 # TAP
tap.time = _beat(4, 0, 1)
track.add_note(tap)
# HOLD 音符(第 6 拍到第 8 拍)
var hold := SyncNote.new()
hold.local_id = "hold_001"
hold.kind = 1 # HOLD
hold.time = _beat(6, 0, 1)
hold.end_time = _beat(8, 0, 1)
track.add_note(hold)
# 另一个 TAP(第 10 拍)
var tap2 := SyncNote.new()
tap2.local_id = "tap_002"
tap2.kind = 0 # TAP
tap2.time = _beat(10, 0, 1)
track.add_note(tap2)
chart.add_root_track(track)
6.1.5. 5. 动画轴
每条轨道可以挂载多种动画轴:
# Float 轴(例如透明度)
var float_axis := SyncFloatAxis.new()
float_axis.axis_name = "opacity"
float_axis.init_value = 1.0
float_axis.add_keyframe(_beat(4, 0, 1), 0.5, SyncTrans.LINEAR, SyncEase.IN_OUT)
float_axis.add_keyframe(_beat(8, 0, 1), 1.0, SyncTrans.SINE, SyncEase.OUT)
track.add_float_axis(float_axis)
# Vector2 轴(例如 2D 位置)
var v2_axis := SyncVector2Axis.new()
v2_axis.axis_name = "position"
v2_axis.init_value = Vector2.ZERO
v2_axis.add_keyframe(_beat(2, 0, 1), Vector2(1.0, 0.0), SyncTrans.LINEAR, SyncEase.IN_OUT)
track.add_vector2_axis(v2_axis)
# Vector3 轴(例如 RGB 颜色)
var v3_axis := SyncVector3Axis.new()
v3_axis.axis_name = "color"
v3_axis.init_value = Vector3.ZERO
v3_axis.add_keyframe(_beat(4, 0, 1), Vector3(1.0, 0.5, 0.0), SyncTrans.STEP, SyncEase.IN_OUT)
track.add_vector3_axis(v3_axis)
# Vector4 轴(例如 RGBA 颜色)
var v4_axis := SyncVector4Axis.new()
v4_axis.axis_name = "color_rgba"
v4_axis.init_value = Vector4.ZERO
v4_axis.add_keyframe(_beat(4, 0, 1), Vector4(1.0, 0.0, 0.0, 1.0), SyncTrans.SINE, SyncEase.OUT)
track.add_vector4_axis(v4_axis)
# Speed 轴(速度变化)
var speed_axis := SyncSpeedAxis.new()
speed_axis.axis_name = "speed"
speed_axis.init_value = 1.0
speed_axis.add_keyframe(_beat(8, 0, 1), 2.0, SyncTrans.LINEAR)
track.add_speed_axis(speed_axis)
6.1.6. 6. 保存与加载
保存为 JSON:
var err := chart.save_json("user://my_chart.json")
if err == OK:
print("JSON 保存成功")
保存为 Godot 资源文件:
var err := chart.save_resource("user://my_chart.tres")
if err == OK:
print(".res 保存成功")
加载回来:
# 从 JSON 加载
var json_chart := SyncDocumentChart.new()
var err := json_chart.load_json("user://my_chart.json")
# 从 .res 加载
var res_chart: SyncDocumentChart = ResourceLoader.load("user://my_chart.tres",
"", ResourceLoader.CACHE_MODE_IGNORE)
6.1.7. 7. 验证
for track: SyncTrack in chart.root_tracks:
print("轨道 '%s': %d 个音符" % [track.track_id, track.get_note_count()])
for note: SyncNote in track.notes:
print(" %s kind=%d time=%d:%d/%d" % [
note.local_id, note.kind,
note.time.beat, note.time.numerator, note.time.denominator
])
print("谱面时长: %.1f ms" % chart.get_duration_ms())
6.1.8. 完整代码
以下是可以直接运行的完整示例(取自 Sync/project/editor_demo.gd):
extends Node
func _ready() -> void:
var chart := _build_sample_chart()
print("构建完成: %d 个轨道, %d 个音符" % [
chart.root_tracks.size(), _count_notes(chart)
])
# 保存验证
chart.save_json("user://demo_chart.json")
chart.save_resource("user://demo_chart.tres")
var reloaded := SyncDocumentChart.new()
reloaded.load_json("user://demo_chart.json")
print("重新加载: %d 个音符" % _count_notes(reloaded))
get_tree().quit()
func _build_sample_chart() -> SyncDocumentChart:
var chart := SyncDocumentChart.new()
chart.bpm = 120.0
chart.music_duration_ms = 60000.0
chart.offset_ms = 0.0
var metadata := chart.get_metadata_dict()
metadata["creator"] = "Tutorial"
metadata["difficulty_label"] = "Demo"
chart.set_metadata_dict(metadata)
var bpm_axis := SyncBpmAxis.new()
bpm_axis.init_value = 120.0
bpm_axis.add_keyframe(SyncBeat.new(), 120.0)
bpm_axis.add_keyframe(_beat(16, 0, 1), 180.0)
chart.set_bpm_axis(bpm_axis)
var tsm := SyncTimeSignatureMap.new()
tsm.add_keyframe(SyncBeat.new(), 4, 4)
chart.set_time_signature_map(tsm)
var track := SyncTrack.new()
track.track_id = "main"
var tap := SyncNote.new()
tap.local_id = "tap_001"; tap.kind = 0
tap.time = _beat(4, 0, 1)
track.add_note(tap)
var hold := SyncNote.new()
hold.local_id = "hold_001"; hold.kind = 1
hold.time = _beat(6, 0, 1)
hold.end_time = _beat(8, 0, 1)
track.add_note(hold)
var tap2 := SyncNote.new()
tap2.local_id = "tap_002"; tap2.kind = 0
tap2.time = _beat(10, 0, 1)
track.add_note(tap2)
chart.add_root_track(track)
return chart
func _count_notes(chart: SyncDocumentChart) -> int:
var total := 0
for track: SyncTrack in chart.root_tracks:
total += track.get_note_count()
return total
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.1.9. SyncBeat 构造说明
SyncBeat 表示一个音乐节拍位置:
beat— 拍号(整数部分)numerator— 分子(拍内偏移的分子)denominator— 分母(拍内偏移的分母)
例如 beat=4, numerator=0, denominator=1 表示第 4 拍整拍位置。
beat=4, numerator=1, denominator=4 表示第 4 拍后的 1/4 拍位置。
6.1.10. 音符种类参考
kind |
名称 |
说明 |
|---|---|---|
0 |
TAP |
单点音符,引擎按"点"语义处理(任何非 HOLD 的 kind 都视作 TAP) |
1 |
HOLD |
长按音符,需在 |
备注
引擎只保留 TAP 与 HOLD 两种时间语义——
CATCH / VOID 等游戏专属种类没有专用 kind,
写在 note.metadata (如
metadata["note_type"] = "catch"),由你的 GDScript
判定按字符串分发。引擎对它们"完全透明"——可见性、位置采样、
序列化行为与 TAP 完全一致。