305 lines
13 KiB
C#
305 lines
13 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Ichni.RhythmGame;
|
||
using UnityEngine;
|
||
|
||
namespace Ichni
|
||
{
|
||
/// <summary>
|
||
/// 集中式元素更新调度器。
|
||
/// 将所有 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 后移除。
|
||
/// </summary>
|
||
public class ElementUpdateScheduler
|
||
{
|
||
#region [常量] Constants
|
||
|
||
/// <summary>阶段总数(TimeDuration..Misc,步长 10,共 9 个)</summary>
|
||
private const int PhaseCount = 9;
|
||
|
||
/// <summary>阶段枚举值到数组索引的步长除数</summary>
|
||
private const int PhaseStep = 10;
|
||
|
||
private static readonly UpdatePhase[] AllPhases = (UpdatePhase[])Enum.GetValues(typeof(UpdatePhase));
|
||
|
||
/// <summary>TickEarly 执行的最大 Phase(含)</summary>
|
||
private const UpdatePhase EarlyPhaseCutoff = UpdatePhase.Apply;
|
||
|
||
#endregion
|
||
|
||
#region [阶段元素列表] Phase Element Lists
|
||
|
||
/// <summary>
|
||
/// 每个阶段的已注册 IScheduledElement 列表。
|
||
/// 使用数组替代 Dictionary<UpdatePhase, ...>,
|
||
/// 索引通过 (int)phase / 10 直接映射,消除哈希查找开销。
|
||
/// </summary>
|
||
private readonly List<IScheduledElement>[] _phaseElements;
|
||
|
||
/// <summary>将 UpdatePhase 枚举值转换为数组索引。</summary>
|
||
private static int PhaseIndex(UpdatePhase phase) => (int)phase / PhaseStep;
|
||
|
||
#endregion
|
||
|
||
#region [轨道 Spline 列表] Track Spline List
|
||
|
||
/// <summary>Phase 3 专用:需要手动重建 SplineComputer 的 Track 列表(保留供后续优化)</summary>
|
||
private readonly List<Track> _trackSplines = new List<Track>(50);
|
||
|
||
#endregion
|
||
|
||
#region [内部管理器] Internal Managers
|
||
|
||
/// <summary>音符管理器:由调度器内部持有,Phase 6 调用</summary>
|
||
private readonly NoteManager _noteManager;
|
||
|
||
/// <summary>BeatmapContainer 延迟提供器(项目加载后才可用)</summary>
|
||
private readonly Func<BeatmapContainer> _beatmapContainerProvider;
|
||
|
||
/// <summary>公开 NoteManager 以供 NoteBase 等外部类注册/更新音符信息</summary>
|
||
public NoteManager NoteScheduler => _noteManager;
|
||
|
||
#endregion
|
||
|
||
#region [缓存] Cached State
|
||
|
||
/// <summary>TickEarly 中缓存的 songTime,供 TickLate 使用</summary>
|
||
private float _cachedSongTime;
|
||
|
||
/// <summary>TickEarly 中缓存的 BeatmapContainer 引用</summary>
|
||
private BeatmapContainer _cachedBeatmapContainer;
|
||
|
||
#endregion
|
||
|
||
#region [构造函数] Constructor
|
||
|
||
/// <summary>
|
||
/// 创建调度器实例。NoteManager 在内部创建,不再依赖外部 Singleton。
|
||
/// </summary>
|
||
/// <param name="beatmapContainerProvider">
|
||
/// BeatmapContainer 延迟提供器。因 BeatmapContainer 在项目加载后才可用,
|
||
/// 使用 Func 延迟解析,避免构造时空引用。
|
||
/// </param>
|
||
public ElementUpdateScheduler(Func<BeatmapContainer> beatmapContainerProvider)
|
||
{
|
||
_noteManager = new NoteManager();
|
||
_beatmapContainerProvider = beatmapContainerProvider;
|
||
|
||
_phaseElements = new List<IScheduledElement>[PhaseCount];
|
||
for (int i = 0; i < PhaseCount; i++)
|
||
{
|
||
_phaseElements[i] = new List<IScheduledElement>();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region [注册与注销] Registration
|
||
|
||
/// <summary>将元素注册到指定的更新阶段。同一元素可注册到多个阶段。</summary>
|
||
public void Register(UpdatePhase phase, IScheduledElement element)
|
||
{
|
||
var list = _phaseElements[PhaseIndex(phase)];
|
||
if (!list.Contains(element))
|
||
{
|
||
list.Add(element);
|
||
}
|
||
}
|
||
|
||
/// <summary>将元素从指定阶段注销。</summary>
|
||
public void Unregister(UpdatePhase phase, IScheduledElement element)
|
||
{
|
||
_phaseElements[PhaseIndex(phase)].Remove(element);
|
||
}
|
||
|
||
/// <summary>注册 Track 的 SplineComputer 到 Phase 3(SplineRebuild)。</summary>
|
||
public void RegisterTrackSpline(Track track)
|
||
{
|
||
if (!_trackSplines.Contains(track))
|
||
{
|
||
_trackSplines.Add(track);
|
||
}
|
||
}
|
||
|
||
/// <summary>从 Phase 3 注销 Track 的 SplineComputer。</summary>
|
||
public void UnregisterTrackSpline(Track track)
|
||
{
|
||
_trackSplines.Remove(track);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region [主循环 - 早期阶段] Main Tick — Early Phases (Update)
|
||
|
||
/// <summary>
|
||
/// 早期阶段调度,由 EditorManager.Update() 调用。
|
||
/// 执行 Phase 0(TimeDuration)→ Phase 1(Animation)→ Phase 2(Apply)。
|
||
/// Phase 2 完成后 Transform 已更新;SplineComputer 将在后续的 Update [order 0] 中检测变化。
|
||
/// </summary>
|
||
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)
|
||
|
||
/// <summary>
|
||
/// 晚期阶段调度,由 EditorManager.LateUpdate() 调用。
|
||
/// 此时所有 Update() 已完成,SplineComputer 已检测 transform.hasChanged 并重新采样。
|
||
/// 各跟踪器通过 RebuildImmediate() 获取最新 Spline 采样后再 SetPercent()。
|
||
/// </summary>
|
||
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
|
||
|
||
/// <summary>
|
||
/// 执行指定阶段的所有已注册 IScheduledElement。
|
||
/// 倒序遍历防止更新途中元素自行销毁导致越界。
|
||
/// </summary>
|
||
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] 过渡期内联逻辑(元素迁移后移除)
|
||
|
||
/// <summary>
|
||
/// [Legacy] Phase 0 内联逻辑:遍历 gameElementList 执行 TimeDuration 检查。
|
||
/// 当所有 IHaveTimeDurationSubmodule 元素迁移到调度器后,此方法将被移除。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// [Legacy] Phase 2 内联逻辑:遍历 gameElementList 执行 DirtyRefresh / Transform / Color。
|
||
/// 当所有相关子模块接口元素迁移到调度器后,此方法将被移除。
|
||
/// </summary>
|
||
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
|
||
}
|
||
}
|