Files
ichni_Creator_Studio/Assets/Scripts/Manager/ElementUpdateScheduler.cs
SoulliesOfficial 0fb72f5bba 同步
2026-06-05 06:47:24 -04:00

305 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 0TimeDuration和 Phase 2Apply仍有 [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&lt;UpdatePhase, ...&gt;
/// 索引通过 (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 3SplineRebuild。</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 0TimeDuration→ Phase 1Animation→ Phase 2Apply
/// 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
}
}