using System; using System.Collections.Generic; using Ichni.RhythmGame; using UnityEngine; namespace Ichni { /// /// 集中式元素更新调度器。 /// 将所有 GameElement 的帧更新按 9 个阶段有序执行, /// 替代原先分散在 EditorManager.Update()、各子管理器、MonoBehaviour.Update() 中的零散更新。 /// /// 调度器分为两个执行阶段,分别在 Update 和 LateUpdate 中调用: /// /// TickEarly (Update, EditorManager [order -100]) /// Phase 0 TimeDuration — 判定元素激活/隐藏 /// Phase 1 Animation — 更新动画值,设置脏标记 /// Phase 2 Apply — 执行 DirtyRefresh + Transform + Color /// /// --- Unity 执行其他 Update (包括 SplineComputer.Update [order 0]) --- /// SplineComputer 检测 transform.hasChanged,重新采样,延迟通知订阅者。 /// /// TickLate (LateUpdate, EditorManager [order -100]) /// Phase 3 SplineRebuild — 保留占位(SplineComputer 已在 Update 中自行处理) /// Phase 4 TrackCore — 更新轨道时间、裁剪区间 /// Phase 5 TrackFollower — CrossTrackPoint / Tracker / HeadPoint / PercentPoint /// Phase 6 Note — 音符可见性、轨道位置、判定、特效 /// Phase 7 Effect — ParticleEmitter / TimeEffectsCollection / LookAt 旋转覆盖 /// Phase 8 Misc — SkyboxSubsetter / LowPriorityActions /// /// 此分离设计消除了 Dreamteck Spline 的一帧延迟: /// Phase 2 (Update) 修改 Transform → SplineComputer.Update() 检测 hasChanged 并重采样 /// → Phase 5/6 (LateUpdate) 调用 RebuildImmediate() 获取最新采样后 SetPercent()。 /// /// 所有通过调度器管理的 SplinePositioner 应设置 autoUpdate = false, /// 由调度器在 Phase 5/6 中手动调用 RebuildImmediate() 刷新采样。 /// /// 设计要点: /// - AnimationManager 和 TrackManager 已删除,其逻辑由各 GameElement 子类通过 /// IScheduledElement.ScheduledUpdate() 自行处理。 /// - NoteManager 因内部逻辑复杂(二分搜索、时间可逆),保留为纯 C# 类, /// 由本调度器内部持有和驱动。 /// - Phase 0(TimeDuration)和 Phase 2(Apply)仍有 [Legacy] 内联循环, /// 待后续将 TimeDurationSubmodule / TransformSubmodule / ColorSubmodule /// 迁移到 IScheduledElement 后移除。 /// public class ElementUpdateScheduler { #region [常量] Constants /// 阶段总数(TimeDuration..Misc,步长 10,共 9 个) private const int PhaseCount = 9; /// 阶段枚举值到数组索引的步长除数 private const int PhaseStep = 10; private static readonly UpdatePhase[] AllPhases = (UpdatePhase[])Enum.GetValues(typeof(UpdatePhase)); /// TickEarly 执行的最大 Phase(含) private const UpdatePhase EarlyPhaseCutoff = UpdatePhase.Apply; #endregion #region [阶段元素列表] Phase Element Lists /// /// 每个阶段的已注册 IScheduledElement 列表。 /// 使用数组替代 Dictionary<UpdatePhase, ...>, /// 索引通过 (int)phase / 10 直接映射,消除哈希查找开销。 /// private readonly List[] _phaseElements; /// 将 UpdatePhase 枚举值转换为数组索引。 private static int PhaseIndex(UpdatePhase phase) => (int)phase / PhaseStep; #endregion #region [轨道 Spline 列表] Track Spline List /// Phase 3 专用:需要手动重建 SplineComputer 的 Track 列表(保留供后续优化) private readonly List _trackSplines = new List(50); #endregion #region [内部管理器] Internal Managers /// 音符管理器:由调度器内部持有,Phase 6 调用 private readonly NoteManager _noteManager; /// BeatmapContainer 延迟提供器(项目加载后才可用) private readonly Func _beatmapContainerProvider; /// 公开 NoteManager 以供 NoteBase 等外部类注册/更新音符信息 public NoteManager NoteScheduler => _noteManager; #endregion #region [缓存] Cached State /// TickEarly 中缓存的 songTime,供 TickLate 使用 private float _cachedSongTime; /// TickEarly 中缓存的 BeatmapContainer 引用 private BeatmapContainer _cachedBeatmapContainer; #endregion #region [构造函数] Constructor /// /// 创建调度器实例。NoteManager 在内部创建,不再依赖外部 Singleton。 /// /// /// BeatmapContainer 延迟提供器。因 BeatmapContainer 在项目加载后才可用, /// 使用 Func 延迟解析,避免构造时空引用。 /// public ElementUpdateScheduler(Func beatmapContainerProvider) { _noteManager = new NoteManager(); _beatmapContainerProvider = beatmapContainerProvider; _phaseElements = new List[PhaseCount]; for (int i = 0; i < PhaseCount; i++) { _phaseElements[i] = new List(); } } #endregion #region [注册与注销] Registration /// 将元素注册到指定的更新阶段。同一元素可注册到多个阶段。 public void Register(UpdatePhase phase, IScheduledElement element) { var list = _phaseElements[PhaseIndex(phase)]; if (!list.Contains(element)) { list.Add(element); } } /// 将元素从指定阶段注销。 public void Unregister(UpdatePhase phase, IScheduledElement element) { _phaseElements[PhaseIndex(phase)].Remove(element); } /// 注册 Track 的 SplineComputer 到 Phase 3(SplineRebuild)。 public void RegisterTrackSpline(Track track) { if (!_trackSplines.Contains(track)) { _trackSplines.Add(track); } } /// 从 Phase 3 注销 Track 的 SplineComputer。 public void UnregisterTrackSpline(Track track) { _trackSplines.Remove(track); } #endregion #region [主循环 - 早期阶段] Main Tick — Early Phases (Update) /// /// 早期阶段调度,由 EditorManager.Update() 调用。 /// 执行 Phase 0(TimeDuration)→ Phase 1(Animation)→ Phase 2(Apply)。 /// Phase 2 完成后 Transform 已更新;SplineComputer 将在后续的 Update [order 0] 中检测变化。 /// public void TickEarly(float songTime) { _cachedBeatmapContainer = _beatmapContainerProvider?.Invoke(); _cachedSongTime = songTime; if (_cachedBeatmapContainer == null) return; // ─── Phase 0: TimeDuration ──────────────────────────────────────── TickPhase(UpdatePhase.TimeDuration, songTime); TickTimeDurationLegacy(_cachedBeatmapContainer, songTime); // ─── Phase 1: Animation ─────────────────────────────────────────── TickPhase(UpdatePhase.Animation, songTime); // ─── Phase 2: Apply ─────────────────────────────────────────────── TickPhase(UpdatePhase.Apply, songTime); TickApplyLegacy(_cachedBeatmapContainer); } #endregion #region [主循环 - 晚期阶段] Main Tick — Late Phases (LateUpdate) /// /// 晚期阶段调度,由 EditorManager.LateUpdate() 调用。 /// 此时所有 Update() 已完成,SplineComputer 已检测 transform.hasChanged 并重新采样。 /// 各跟踪器通过 RebuildImmediate() 获取最新 Spline 采样后再 SetPercent()。 /// public void TickLate() { if (_cachedBeatmapContainer == null) return; float songTime = _cachedSongTime; // ─── Phase 3: SplineRebuild ─────────────────────────────────────── // SplineComputer 使用默认 UpdateMode.Update,已在 Update 中自行处理。 // 此阶段保留占位,不执行手动 Rebuild。 TickPhase(UpdatePhase.SplineRebuild, songTime); // ─── Phase 4: TrackCore ─────────────────────────────────────────── TickPhase(UpdatePhase.TrackCore, songTime); // ─── Phase 5: TrackFollower ─────────────────────────────────────── // 各跟踪器在 ScheduledUpdate 中先调用 RebuildImmediate() 再 SetPercent() TickPhase(UpdatePhase.TrackFollower, songTime); // ─── Phase 6: Note ──────────────────────────────────────────────── TickPhase(UpdatePhase.Note, songTime); _noteManager.ManualTick(songTime); // ─── Phase 7: Effect ────────────────────────────────────────────── TickPhase(UpdatePhase.Effect, songTime); // ─── Phase 8: Misc ──────────────────────────────────────────────── TickPhase(UpdatePhase.Misc, songTime); _cachedBeatmapContainer.ExecuteLowPriorityActions(); } #endregion #region [阶段执行] Phase Execution /// /// 执行指定阶段的所有已注册 IScheduledElement。 /// 倒序遍历防止更新途中元素自行销毁导致越界。 /// private void TickPhase(UpdatePhase phase, float songTime) { var list = _phaseElements[PhaseIndex(phase)]; for (int i = list.Count - 1; i >= 0; i--) { var element = list[i]; if (element != null && element.IsScheduledActive) { element.ScheduledUpdate(phase, songTime); } } } #endregion #region [Legacy] 过渡期内联逻辑(元素迁移后移除) /// /// [Legacy] Phase 0 内联逻辑:遍历 gameElementList 执行 TimeDuration 检查。 /// 当所有 IHaveTimeDurationSubmodule 元素迁移到调度器后,此方法将被移除。 /// private void TickTimeDurationLegacy(BeatmapContainer beatmapContainer, float songTime) { for (int i = 0; i < beatmapContainer.gameElementList.Count; i++) { var element = beatmapContainer.gameElementList[i]; if (element == null) continue; if (element is IHaveTimeDurationSubmodule timeHost && !(element is NoteBase)) { timeHost.timeDurationSubmodule?.UpdateTimeDuration(songTime); } } } /// /// [Legacy] Phase 2 内联逻辑:遍历 gameElementList 执行 DirtyRefresh / Transform / Color。 /// 当所有相关子模块接口元素迁移到调度器后,此方法将被移除。 /// private void TickApplyLegacy(BeatmapContainer beatmapContainer) { for (int i = 0; i < beatmapContainer.gameElementList.Count; i++) { var element = beatmapContainer.gameElementList[i]; if (element == null) continue; if (element is IHaveDirtyMarkSubmodule dirtyHost) { dirtyHost.dirtyMarkSubmodule?.ExecuteDeferredRefresh(); } if (element.gameObject.activeSelf) { if (element is IHaveTransformSubmodule transformHost) { transformHost.UpdateTransform(); } if (element is IHaveColorSubmodule colorHost) { colorHost.UpdateColor(); } } } } #endregion } }