This commit is contained in:
SoulliesOfficial
2026-06-05 06:47:24 -04:00
parent 3995beeb29
commit 0fb72f5bba
94 changed files with 650754 additions and 4839 deletions

View File

@@ -1,51 +0,0 @@
using System.Collections.Generic;
using Ichni.RhythmGame;
using SLSUtilities.General;
using UnityEngine;
namespace Ichni
{
/// <summary>
/// 编辑器 AnimationManager集中管理场上所有 AnimationBase 实例的逐帧更新。
/// 替代 AnimationBase.Update() 中大量零散的 MonoBehaviour 帧回调,
/// 由 EditorManager.Update 统一驱动,减少 Update() 调用开销。
/// 倒序遍历防止在更新途中某个动画自行销毁导致越界。
/// </summary>
public class AnimationManager : Singleton<AnimationManager>
{
#region [] Singleton Alias
public new static AnimationManager instance => Instance;
#endregion
#region [] Active Animation List
private readonly List<AnimationBase> _activeAnimations = new List<AnimationBase>(200);
#endregion
#region [] Registration
public void RegisterAnimation(AnimationBase anim)
{
if (!_activeAnimations.Contains(anim)) _activeAnimations.Add(anim);
}
public void UnregisterAnimation(AnimationBase anim) => _activeAnimations.Remove(anim);
#endregion
#region [] Manager-Driven Tick
/// <summary>
/// 由 EditorManager.Update 统一调度。
/// 倒序遍历以防在更新途中某个动画自行销毁导致列表越界。
/// </summary>
public void ManualTick(float songTime)
{
for (int i = _activeAnimations.Count - 1; i >= 0; i--)
{
var anim = _activeAnimations[i];
if (!anim.isActiveAndEnabled) continue;
if (anim.timeDurationSubmodule.CheckTimeInDuration(songTime))
{
anim.InvokeUpdate();
}
}
}
#endregion
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 5f068825cf8c22b4fa573d55654c2474

View File

@@ -16,6 +16,7 @@ namespace Ichni
/// 游戏/谱面数据由 ProjectContainer 持有;
/// 此处的属性均为转发属性,保持所有外部调用点零改动。
/// </summary>
[DefaultExecutionOrder(-100)]
public class EditorManager : Singleton<EditorManager>
{
#region [] Singleton Alias
@@ -37,13 +38,20 @@ namespace Ichni
public BackgroundController backgroundController;
public SimpleGridController gridController;
public CameraManager cameraManager;
public NoteManager noteManager;
public TrackManager trackManager;
public AnimationManager animationManager;
public Canvas judgeHintCanvas;
public Canvas inspectorCanvas;
public Timeline timeline;
public PanelDrawer panelDrawer;
/// <summary>集中式元素更新调度器,替代原先分散的帧更新逻辑</summary>
[NonSerialized]
public ElementUpdateScheduler updateScheduler;
/// <summary>
/// NoteManager 转发属性。NoteManager 现为纯 C# 类,由 updateScheduler 内部持有。
/// 保留此属性以兼容现有调用点。
/// </summary>
public NoteManager noteManager => updateScheduler?.NoteScheduler;
#endregion
#region [] Editor Preferences Forwarding Properties
@@ -124,6 +132,14 @@ namespace Ichni
// 不再直接依赖 EditorManager.instance.musicPlayer
CoreServices.TimeProvider = musicPlayer;
// 初始化集中式更新调度器:
// BeatmapContainer 在项目加载后才可用,使用 Func 延迟解析
// NoteManager 在调度器内部创建,不再依赖外部 Singleton
updateScheduler = new ElementUpdateScheduler(
() => beatmapContainer
);
CoreServices.UpdateScheduler = updateScheduler;
if (!ES3.FileExists(Application.streamingAssetsPath + "/EditorSettings.es3"))
{
editorSettings = new EditorSettings(300, 3, 100, 100, 60);
@@ -173,42 +189,14 @@ namespace Ichni
projectManager.autoSaveManager.UpdateAutoSave();
// 统一调度: Animation → Submodules → Track → Note
float songTime = CoreServices.TimeProvider.SongTime;
updateScheduler.TickEarly(songTime);
}
animationManager.ManualTick(songTime);
// 手动执行原本属于 UniRx 的每帧调度,消灭不可控的时序错乱
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);
}
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();
}
}
}
trackManager.ManualTick(songTime);
noteManager.ManualTick(songTime);
private void LateUpdate()
{
if (!isLoaded) return;
updateScheduler.TickLate();
}
#endregion

View File

@@ -0,0 +1,304 @@
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
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2086c4a4da4d364428cdff9ac604ad38

View File

@@ -1,10 +1,8 @@
using System.Collections;
using System.Collections.Generic;
using Ichni.RhythmGame;
using SLSUtilities.General;
using UnityEngine;
namespace Ichni.RhythmGame
namespace Ichni
{
/// <summary>
/// 编辑器 NoteManager集中管理场上所有 Note 的激活/隐藏与逐帧更新。
@@ -16,13 +14,12 @@ namespace Ichni.RhythmGame
/// - 时间区间内的 Note → 激活并更新
/// - 时间区间外的 Note → 隐藏
/// 保证无论时间如何跳转Note 的可见性和状态始终与 songTime 一致。
///
/// 此类为纯 C# 类,由 ElementUpdateScheduler 内部持有和驱动,
/// 不再继承 Singleton / MonoBehaviour。
/// </summary>
public class NoteManager : Singleton<NoteManager>
public class NoteManager
{
#region [] Singleton Alias
public new static NoteManager instance => Instance;
#endregion
#region [] Note Record
/// <summary>Note 条目:存储 Note 本身及其激活/消失的时间阈值</summary>
private struct NoteRecord
@@ -91,7 +88,7 @@ namespace Ichni.RhythmGame
#region [] Manager-Driven Tick
/// <summary>
/// 由 EditorManager.Update 统一调度
/// 由 ElementUpdateScheduler.Tick 在 Phase.Note 阶段调用
///
/// <para>
/// 编辑器时间可逆策略:每帧通过二分查找定位当前 songTime 覆盖的激活窗口,
@@ -124,9 +121,6 @@ namespace Ichni.RhythmGame
right = mid - 1;
}
}
// firstInWindow 此时指向最后一个 activationTime <= songTime 的 Note
// 从 0 向左扫描(也就是从头扫描到 firstInWindow都可能在窗口内
// 实际需要activationTime <= songTime && finishTime >= songTime
// 3. 遍历所有 Note判断是否在当前窗口内
for (int i = 0; i < count; i++)
@@ -156,4 +150,4 @@ namespace Ichni.RhythmGame
}
#endregion
}
}
}

View File

@@ -1,125 +0,0 @@
using System.Collections.Generic;
using Ichni.RhythmGame;
using SLSUtilities.General;
using UnityEngine;
namespace Ichni
{
/// <summary>
/// 编辑器 TrackManager集中管理场上所有轨道相关组件的逐帧更新。
/// 替代各组件自身持有的 Update() 调用,消除大量零散的 MonoBehaviour 帧回调开销。
/// 通过 ManualTick() 由 EditorManager 统一调度,确保时序可控。
/// </summary>
public class TrackManager : Singleton<TrackManager>
{
#region [] Singleton Alias
public new static TrackManager instance => Instance;
#endregion
#region [] Active Component Lists
private readonly List<Track> _activeTracks = new List<Track>(50);
private readonly List<CrossTrackPoint> _activeCrossPoints = new List<CrossTrackPoint>(50);
private readonly List<ObjectTracker> _activeObjectTrackers = new List<ObjectTracker>(50);
private readonly List<ParticleTracker> _activeParticleTrackers = new List<ParticleTracker>(50);
private readonly List<TrackHeadPoint> _activeHeadPoints = new List<TrackHeadPoint>(50);
private readonly List<TrackPercentPoint> _activePercentPoints = new List<TrackPercentPoint>(50);
#endregion
#region [] Registration
public void RegisterTrack(Track track)
{
if (!_activeTracks.Contains(track)) _activeTracks.Add(track);
}
public void UnregisterTrack(Track track) => _activeTracks.Remove(track);
public void RegisterCrossPoint(CrossTrackPoint point)
{
if (!_activeCrossPoints.Contains(point)) _activeCrossPoints.Add(point);
}
public void UnregisterCrossPoint(CrossTrackPoint point) => _activeCrossPoints.Remove(point);
public void RegisterObjectTracker(ObjectTracker tracker)
{
if (!_activeObjectTrackers.Contains(tracker)) _activeObjectTrackers.Add(tracker);
}
public void UnregisterObjectTracker(ObjectTracker tracker) => _activeObjectTrackers.Remove(tracker);
public void RegisterParticleTracker(ParticleTracker tracker)
{
if (!_activeParticleTrackers.Contains(tracker)) _activeParticleTrackers.Add(tracker);
}
public void UnregisterParticleTracker(ParticleTracker tracker) => _activeParticleTrackers.Remove(tracker);
public void RegisterHeadPoint(TrackHeadPoint point)
{
if (!_activeHeadPoints.Contains(point)) _activeHeadPoints.Add(point);
}
public void UnregisterHeadPoint(TrackHeadPoint point) => _activeHeadPoints.Remove(point);
public void RegisterPercentPoint(TrackPercentPoint point)
{
if (!_activePercentPoints.Contains(point)) _activePercentPoints.Add(point);
}
public void UnregisterPercentPoint(TrackPercentPoint point) => _activePercentPoints.Remove(point);
#endregion
#region [] Manager-Driven Tick
/// <summary>
/// 由 EditorManager.Update 统一调度。
/// </summary>
public void ManualTick(float songTime)
{
// 1. Track更新轨道时间子模块
for (int i = 0; i < _activeTracks.Count; i++)
{
var track = _activeTracks[i];
if (!track.isActiveAndEnabled) continue;
if (track.timeDurationSubmodule.CheckTimeInDuration(songTime))
{
(track.trackTimeSubmodule as TrackTimeSubmoduleMovable)?.UpdateTrackPart(songTime);
}
}
// 2. CrossTrackPoint更新跨轨切分点
for (int i = 0; i < _activeCrossPoints.Count; i++)
{
var point = _activeCrossPoints[i];
if (!point.isActiveAndEnabled) continue;
point.ManualTick(songTime);
}
// 3. ObjectTracker更新轨道物体跟踪器
for (int i = 0; i < _activeObjectTrackers.Count; i++)
{
var tracker = _activeObjectTrackers[i];
if (!tracker.isActiveAndEnabled) continue;
tracker.ManualTick(songTime);
}
// 4. ParticleTracker更新轨道粒子跟踪器
for (int i = 0; i < _activeParticleTrackers.Count; i++)
{
var tracker = _activeParticleTrackers[i];
if (!tracker.isActiveAndEnabled) continue;
tracker.ManualTick(songTime);
}
// 5. TrackHeadPoint更新轨道头节点
for (int i = 0; i < _activeHeadPoints.Count; i++)
{
var point = _activeHeadPoints[i];
if (!point.isActiveAndEnabled) continue;
point.ManualTick(songTime);
}
// 6. TrackPercentPoint更新轨道百分比节点
for (int i = 0; i < _activePercentPoints.Count; i++)
{
var point = _activePercentPoints[i];
if (!point.isActiveAndEnabled) continue;
point.ManualTick(songTime);
}
}
#endregion
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 3435f487f7c1c654e93adba65c7de915