7.1. 自定义渲染指南

7.1.1. 设计哲学

Sync Engine 采用 数据-渲染分离 架构:Sync 负责提供音符数据与时间轴 信息,你负责渲染。引擎不内置任何视觉组件——没有 Sprite、没有 MeshInstance3D、没有粒子。这个设计让你完全自由地实现自己的美术风格。

7.1.2. 地面轨道渲染

地面轨道(Ground Track)是下落式音游最基础的轨道类型,音符沿垂直 方向向下掉落。渲染流程:

  1. 获取可见音符:每帧调用 SyncChartPlayer.get_visible_note_ids_for_track(track_id) 获取当前可见窗口内的音符 ID 列表(C++ 按轨分桶,O(1) 直取)。

  2. 获取音符信息:对每个可见音符,调用 SyncTrack.get_note(note_id) 获取 SyncNote 实例,再通过 SyncTrack.get_note_hit_ms(note)SyncTrack.get_note_end_ms(note) 获取命中时间和 Hold 结束时间。 这两个值应在轨道初始化时缓存,每帧渲染循环中避免重复调用 beat_to_ms

  3. 位置计算

    • 使用 SyncChartPlayer.get_track_normalized_position_at(ms, track_id)SyncChartPlayer.get_note_normalized_position(note_id) 获取归一化位置。

    • 归一化位置0 = 判定线(Judge Line), 正值 = 音符正在接近,负值 = 音符已过线。

    • 地面轨道可进一步映射为屏幕 Y 坐标或 3D 空间 Z 坐标。

  4. 绘制:根据算得的位置绘制精灵(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. 对象池

音符渲染实例应做池化,避免频繁的 instantiatequeue_free。 引擎不提供池实现(池化是渲染关注点,不是谱面引擎关注点),在你自己 的 GDScript 里实现即可,要点只有两个:

  • 音符进入可见窗口时从池获取实例,设置位置和外观。

  • 音符离开窗口后归还实例到池(隐藏并重置),不销毁。

7.1.4.2. 缓存静态时间

在轨道 setup() 阶段缓存每个音符的 hit_msend_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)