159 lines
5.9 KiB
C#
159 lines
5.9 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using Ichni.RhythmGame;
|
||
using SLSUtilities.General;
|
||
using UnityEngine;
|
||
|
||
namespace Ichni.RhythmGame
|
||
{
|
||
/// <summary>
|
||
/// 编辑器 NoteManager:集中管理场上所有 Note 的激活/隐藏与逐帧更新。
|
||
///
|
||
/// 与游戏本体的关键区别——时间可逆性:
|
||
/// 游戏本体时间单向流动,_nextNoteIndex 只递增。
|
||
/// 编辑器时间可随意跳转 / 倒退(拖动 Slider、按数字键),
|
||
/// 因此本管理器采用"每帧重新扫描激活窗口"的策略:
|
||
/// - 时间区间内的 Note → 激活并更新
|
||
/// - 时间区间外的 Note → 隐藏
|
||
/// 保证无论时间如何跳转,Note 的可见性和状态始终与 songTime 一致。
|
||
/// </summary>
|
||
public class NoteManager : Singleton<NoteManager>
|
||
{
|
||
#region [单例别名] Singleton Alias
|
||
public new static NoteManager instance => Instance;
|
||
#endregion
|
||
|
||
#region [数据结构] Note Record
|
||
/// <summary>Note 条目:存储 Note 本身及其激活/消失的时间阈值</summary>
|
||
private struct NoteRecord
|
||
{
|
||
public NoteBase note;
|
||
public float activationTime; // Note 应当进入可见状态的时间点
|
||
public float finishTime; // Note 应当退出可见状态的时间点
|
||
}
|
||
|
||
/// <summary>
|
||
/// 所有已注册的 Note,按 activationTime 升序排列。
|
||
/// 排序后可用二分查找高效定位当前窗口。
|
||
/// </summary>
|
||
private List<NoteRecord> _pendingNotes = new List<NoteRecord>(128);
|
||
|
||
/// <summary>当前帧内激活的 Note(缓存,减少 GC)</summary>
|
||
private List<NoteBase> _currentlyActive = new List<NoteBase>(64);
|
||
|
||
private bool _isDirty = false; // 注册/更新后需要重排序
|
||
#endregion
|
||
|
||
#region [注册与管理] Registration
|
||
/// <summary>注册一个新 Note(isNewOne = true)</summary>
|
||
public void RegisterNote(NoteBase note, float activationTime, float finishTime)
|
||
{
|
||
_pendingNotes.Add(new NoteRecord
|
||
{
|
||
note = note,
|
||
activationTime = activationTime,
|
||
finishTime = finishTime
|
||
});
|
||
_isDirty = true;
|
||
}
|
||
|
||
/// <summary>更新已注册 Note 的时间信息(参数改变后重新计算窗口)</summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>手动触发排序(若需要在注册大批 Note 后一次性完成)</summary>
|
||
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
|
||
/// <summary>
|
||
/// 由 EditorManager.Update 统一调度。
|
||
///
|
||
/// <para>
|
||
/// 编辑器时间可逆策略:每帧通过二分查找定位当前 songTime 覆盖的激活窗口,
|
||
/// 对窗口内的 Note 激活并调用 Update(),对窗口外的 Note 隐藏。
|
||
/// 无需维护 _nextNoteIndex 指针,天然支持时间任意跳转与倒退。
|
||
/// </para>
|
||
/// </summary>
|
||
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
|
||
}
|
||
} |