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>注册一个新 Note(isNewOne = true)</summary>
|
|
|
|
|
|
public void RegisterNote(NoteBase note, float activationTime, float finishTime)
|
2025-12-21 16:41:12 +08:00
|
|
|
|
{
|
2026-03-14 02:30:26 -04:00
|
|
|
|
_pendingNotes.Add(new NoteRecord
|
|
|
|
|
|
{
|
|
|
|
|
|
note = note,
|
|
|
|
|
|
activationTime = activationTime,
|
|
|
|
|
|
finishTime = finishTime
|
|
|
|
|
|
});
|
|
|
|
|
|
_isDirty = true;
|
2025-12-21 16:41:12 +08:00
|
|
|
|
}
|
2026-03-14 02:30:26 -04:00
|
|
|
|
|
|
|
|
|
|
/// <summary>更新已注册 Note 的时间信息(参数改变后重新计算窗口)</summary>
|
2025-12-21 16:41:12 +08:00
|
|
|
|
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);
|
2025-12-21 16:41:12 +08:00
|
|
|
|
if (idx != -1)
|
|
|
|
|
|
{
|
2026-03-14 02:30:26 -04:00
|
|
|
|
_pendingNotes[idx] = new NoteRecord
|
|
|
|
|
|
{
|
|
|
|
|
|
note = note,
|
|
|
|
|
|
activationTime = activationTime,
|
|
|
|
|
|
finishTime = finishTime
|
|
|
|
|
|
};
|
|
|
|
|
|
_isDirty = true;
|
2025-12-21 16:41:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
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();
|
2025-10-19 14:42:05 +08:00
|
|
|
|
|
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)
|
2025-12-21 01:00:57 +08:00
|
|
|
|
{
|
2026-03-14 02:30:26 -04:00
|
|
|
|
int mid = (left + right) >> 1;
|
|
|
|
|
|
if (_pendingNotes[mid].activationTime <= songTime)
|
|
|
|
|
|
{
|
|
|
|
|
|
firstInWindow = mid;
|
|
|
|
|
|
left = mid + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
2025-12-21 01:00:57 +08:00
|
|
|
|
{
|
2026-03-14 02:30:26 -04:00
|
|
|
|
right = mid - 1;
|
2025-12-21 01:00:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-14 02:30:26 -04:00
|
|
|
|
// firstInWindow 此时指向最后一个 activationTime <= songTime 的 Note,
|
|
|
|
|
|
// 从 0 向左扫描(也就是从头扫描到 firstInWindow)都可能在窗口内
|
|
|
|
|
|
// 实际需要:activationTime <= songTime && finishTime >= songTime
|
2025-12-21 01:00:57 +08:00
|
|
|
|
|
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;
|
2025-10-05 11:45:32 +08:00
|
|
|
|
|
2026-03-14 02:30:26 -04:00
|
|
|
|
bool shouldBeActive = record.activationTime <= songTime && record.finishTime >= songTime;
|
|
|
|
|
|
bool isActive = record.note.gameObject.activeSelf;
|
2025-12-21 01:00:57 +08:00
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-19 14:42:05 +08: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
|
|
|
|
}
|
|
|
|
|
|
}
|