Files
ichni_Creator_Studio/Assets/Scripts/Manager/NoteManager.cs

159 lines
5.9 KiB
C#
Raw Normal View History

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