Files
ichni_Creator_Studio/Assets/Scripts/Manager/NoteManager.cs
SoulliesOfficial aee62cd637 大修
2026-03-14 02:30:26 -04:00

159 lines
5.9 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.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>注册一个新 NoteisNewOne = 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
}
}