using System.Collections; using System.Collections.Generic; using Ichni.RhythmGame; using SLSUtilities.General; using UnityEngine; namespace Ichni.RhythmGame { /// /// 编辑器 NoteManager:集中管理场上所有 Note 的激活/隐藏与逐帧更新。 /// /// 与游戏本体的关键区别——时间可逆性: /// 游戏本体时间单向流动,_nextNoteIndex 只递增。 /// 编辑器时间可随意跳转 / 倒退(拖动 Slider、按数字键), /// 因此本管理器采用"每帧重新扫描激活窗口"的策略: /// - 时间区间内的 Note → 激活并更新 /// - 时间区间外的 Note → 隐藏 /// 保证无论时间如何跳转,Note 的可见性和状态始终与 songTime 一致。 /// public class NoteManager : Singleton { #region [单例别名] Singleton Alias public new static NoteManager instance => Instance; #endregion #region [数据结构] Note Record /// Note 条目:存储 Note 本身及其激活/消失的时间阈值 private struct NoteRecord { public NoteBase note; public float activationTime; // Note 应当进入可见状态的时间点 public float finishTime; // Note 应当退出可见状态的时间点 } /// /// 所有已注册的 Note,按 activationTime 升序排列。 /// 排序后可用二分查找高效定位当前窗口。 /// private List _pendingNotes = new List(128); /// 当前帧内激活的 Note(缓存,减少 GC) private List _currentlyActive = new List(64); private bool _isDirty = false; // 注册/更新后需要重排序 #endregion #region [注册与管理] Registration /// 注册一个新 Note(isNewOne = true) public void RegisterNote(NoteBase note, float activationTime, float finishTime) { _pendingNotes.Add(new NoteRecord { note = note, activationTime = activationTime, finishTime = finishTime }); _isDirty = true; } /// 更新已注册 Note 的时间信息(参数改变后重新计算窗口) public void ChangeNoteInfo(NoteBase note, float activationTime, float finishTime) { int idx = _pendingNotes.FindIndex(r => r.note == note); if (idx != -1) { _pendingNotes[idx] = new NoteRecord { note = note, activationTime = activationTime, finishTime = finishTime }; _isDirty = true; } } /// 手动触发排序(若需要在注册大批 Note 后一次性完成) public void AllNotesRegistered() { SortIfDirty(); } private void SortIfDirty() { if (!_isDirty) return; // 移除已销毁的 Note _pendingNotes.RemoveAll(r => r.note == null); _pendingNotes.Sort((a, b) => a.activationTime.CompareTo(b.activationTime)); _isDirty = false; } #endregion #region [中央集权主循环] Manager-Driven Tick /// /// 由 EditorManager.Update 统一调度。 /// /// /// 编辑器时间可逆策略:每帧通过二分查找定位当前 songTime 覆盖的激活窗口, /// 对窗口内的 Note 激活并调用 Update(),对窗口外的 Note 隐藏。 /// 无需维护 _nextNoteIndex 指针,天然支持时间任意跳转与倒退。 /// /// public void ManualTick(float songTime) { SortIfDirty(); // 1. 清空上一帧的激活记录 _currentlyActive.Clear(); // 2. 二分查找第一个 activationTime <= songTime 的 Note 起始位置 int count = _pendingNotes.Count; if (count == 0) return; int left = 0, right = count - 1, firstInWindow = count; while (left <= right) { int mid = (left + right) >> 1; if (_pendingNotes[mid].activationTime <= songTime) { firstInWindow = mid; left = mid + 1; } else { right = mid - 1; } } // firstInWindow 此时指向最后一个 activationTime <= songTime 的 Note, // 从 0 向左扫描(也就是从头扫描到 firstInWindow)都可能在窗口内 // 实际需要:activationTime <= songTime && finishTime >= songTime // 3. 遍历所有 Note,判断是否在当前窗口内 for (int i = 0; i < count; i++) { var record = _pendingNotes[i]; if (record.note == null) continue; bool shouldBeActive = record.activationTime <= songTime && record.finishTime >= songTime; bool isActive = record.note.gameObject.activeSelf; if (shouldBeActive) { if (!isActive) record.note.gameObject.SetActive(true); _currentlyActive.Add(record.note); } else { if (isActive) record.note.gameObject.SetActive(false); } } // 4. 集中驱动当前帧内所有激活 Note 的逐帧更新 for (int i = _currentlyActive.Count - 1; i >= 0; i--) { _currentlyActive[i].ManualUpdate(songTime); } } #endregion } }