自定义渲染指南 ================ 设计哲学 -------- Sync Engine 采用 **数据-渲染分离** 架构:Sync 负责提供音符数据与时间轴 信息,**你负责渲染**。引擎不内置任何视觉组件——没有 Sprite、没有 MeshInstance3D、没有粒子。这个设计让你完全自由地实现自己的美术风格。 地面轨道渲染 ------------ 地面轨道(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): .. code-block:: 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) 轨道动画渲染 ---------------- 轨道可携带动画轴(位置、旋转、颜色、透明度)。 每个动画轴都有独立的关键帧曲线,需要逐帧采样: - ``SyncTrack.vector2_axes[0]``:轨道位置偏移 - ``SyncTrack.float_axes[0]``:轨道旋转 - ``SyncTrack.vector3_axes[0]``:轨道颜色(RGB,替换填充色,非乘算) - ``SyncTrack.float_axes[1]``:轨道透明度 采样方法: .. code-block:: gdscript # 在给定时间点采样 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) 性能优化 -------- 对象池 ^^^^^^ 音符渲染实例应做池化,避免频繁的 ``instantiate`` 和 ``queue_free``。 引擎不提供池实现(池化是渲染关注点,不是谱面引擎关注点),在你自己 的 GDScript 里实现即可,要点只有两个: - 音符进入可见窗口时从池获取实例,设置位置和外观。 - 音符离开窗口后归还实例到池(隐藏并重置),不销毁。 缓存静态时间 ^^^^^^^^^^^^ 在轨道 ``setup()`` 阶段缓存每个音符的 ``hit_ms`` 与 ``end_ms`` , 渲染循环中直接使用缓存值,**禁止** 在每帧热路径中重复调用 ``bpm_axis.beat_to_ms()`` 。 坐标系约定 ---------- 所有归一化位置共享同一坐标系: .. list-table:: :header-rows: 1 * - 归一化值 - 含义 * - **0** - 判定线(Judge Line) * - **正值** - 音符正在接近判定线 * - **负值** - 音符已通过判定线 * - **1.0** - 轨道远端(生成点附近) 对于 3D 轨道,归一化位置是 SpeedAxis 沿轨道方向的**积分距离** ( 米制),可使用 ``get_note_position_fast()`` 获取实际空间坐标。 完整的渲染循环示例(GDScript 伪代码): .. code-block:: 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)