7.1. 自定义渲染指南
7.1.1. 设计哲学
Sync Engine 采用 数据-渲染分离 架构:Sync 负责提供音符数据与时间轴 信息,你负责渲染。引擎不内置任何视觉组件——没有 Sprite、没有 MeshInstance3D、没有粒子。这个设计让你完全自由地实现自己的美术风格。
7.1.2. 地面轨道渲染
地面轨道(Ground Track)是下落式音游最基础的轨道类型,音符沿垂直 方向向下掉落。渲染流程:
获取可见音符:每帧调用
SyncChartPlayer.get_visible_note_ids_for_track(track_id)获取当前可见窗口内的音符 ID 列表(C++ 按轨分桶,O(1) 直取)。获取音符信息:对每个可见音符,调用
SyncTrack.get_note(note_id)获取SyncNote实例,再通过SyncTrack.get_note_hit_ms(note)和SyncTrack.get_note_end_ms(note)获取命中时间和 Hold 结束时间。 这两个值应在轨道初始化时缓存,每帧渲染循环中避免重复调用beat_to_ms。位置计算:
使用
SyncChartPlayer.get_track_normalized_position_at(ms, track_id)或SyncChartPlayer.get_note_normalized_position(note_id)获取归一化位置。归一化位置:
0= 判定线(Judge Line), 正值 = 音符正在接近,负值 = 音符已过线。地面轨道可进一步映射为屏幕 Y 坐标或 3D 空间 Z 坐标。
绘制:根据算得的位置绘制精灵(2D)或 3D 网格节点。
示例流程(GDScript):
func _refresh_visible_notes(track: SyncTrack, player: SyncChartPlayer) -> void:
var visible_ids: PackedInt32Array = player.get_visible_note_ids_for_track(track_id)
for note_id in visible_ids:
var normalized: float = player.get_note_normalized_position(note_id)
# 正值 = 还没到判定线,映射到屏幕上方
if normalized >= 0.0:
var screen_y: float = judge_line_y - normalized * pixels_per_unit
draw_note(note_id, screen_y)
7.1.3. 轨道动画渲染
轨道可携带动画轴(位置、旋转、颜色、透明度)。 每个动画轴都有独立的关键帧曲线,需要逐帧采样:
SyncTrack.vector2_axes[0]:轨道位置偏移SyncTrack.float_axes[0]:轨道旋转SyncTrack.vector3_axes[0]:轨道颜色(RGB,替换填充色,非乘算)SyncTrack.float_axes[1]:轨道透明度
采样方法:
# 在给定时间点采样
var pos: Vector2 = track.vector2_axes[0].sample(time_ms, bpm_axis)
var rot: float = track.float_axes[0].sample(time_ms, bpm_axis)
var color: Vector3 = track.vector3_axes[0].sample(time_ms, bpm_axis)
var opacity: float = track.float_axes[1].sample(time_ms, bpm_axis)
7.1.4. 性能优化
7.1.4.1. 对象池
音符渲染实例应做池化,避免频繁的 instantiate 和 queue_free。
引擎不提供池实现(池化是渲染关注点,不是谱面引擎关注点),在你自己
的 GDScript 里实现即可,要点只有两个:
音符进入可见窗口时从池获取实例,设置位置和外观。
音符离开窗口后归还实例到池(隐藏并重置),不销毁。
7.1.4.2. 缓存静态时间
在轨道 setup() 阶段缓存每个音符的 hit_ms 与 end_ms ,
渲染循环中直接使用缓存值,禁止 在每帧热路径中重复调用
bpm_axis.beat_to_ms() 。
7.1.5. 坐标系约定
所有归一化位置共享同一坐标系:
归一化值 |
含义 |
|---|---|
0 |
判定线(Judge Line) |
正值 |
音符正在接近判定线 |
负值 |
音符已通过判定线 |
1.0 |
轨道远端(生成点附近) |
对于 3D 轨道,归一化位置是 SpeedAxis 沿轨道方向的**积分距离** (
米制),可使用 get_note_position_fast() 获取实际空间坐标。
完整的渲染循环示例(GDScript 伪代码):
func _process(delta: float) -> void:
for track_id in track_ids:
var visible_ids = chart_player.get_visible_note_ids_for_track(track_id)
for note_id in visible_ids:
if not _active_notes_set.has(note_id):
continue
var normalized = chart_player.get_note_normalized_position(note_id)
if normalized > 0:
var pos = track.get_note_position_fast(
_hit_ms_cache[note_id],
_end_ms_cache[note_id],
note.kind,
track_id
)
var instance = _get_or_create_instance(note_id)
instance.global_position = pos
# 采样透明度写入 shader
var opacity = track.float_axes[1].sample(current_ms, bpm_axis)
_set_instance_opacity(instance, opacity)
else:
_release_instance(note_id)