This commit is contained in:
SoulliesOfficial
2026-03-14 02:30:26 -04:00
parent cf86f0ee51
commit aee62cd637
2041 changed files with 246771 additions and 129128 deletions

View File

@@ -1,90 +1,159 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UniRx;
using Ichni.RhythmGame;
using SLSUtilities.General;
using UnityEngine;
namespace Ichni.RhythmGame
{
public class NoteManager : MonoBehaviour
/// <summary>
/// 编辑器 NoteManager集中管理场上所有 Note 的激活/隐藏与逐帧更新。
///
/// 与游戏本体的关键区别——时间可逆性:
/// 游戏本体时间单向流动_nextNoteIndex 只递增。
/// 编辑器时间可随意跳转 / 倒退(拖动 Slider、按数字键
/// 因此本管理器采用"每帧重新扫描激活窗口"的策略:
/// - 时间区间内的 Note → 激活并更新
/// - 时间区间外的 Note → 隐藏
/// 保证无论时间如何跳转Note 的可见性和状态始终与 songTime 一致。
/// </summary>
public class NoteManager : Singleton<NoteManager>
{
public static NoteManager instance;
public List<(NoteBase note, float activationTime, float finishTime)> pendingNotes = new List<(NoteBase, float, float)>();
private List<(NoteBase note1, bool isActive, float activationTime)> ProcessingNotes = new List<(NoteBase, bool, float)>();
#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((note, activationTime, finishTime));
AllNotesRegistered();
print($"Registered note {note.elementName} with activation time {activationTime} and finish time {finishTime}");
}
public void Awake()
{
instance = this;
_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(i => i.note == note);
int idx = _pendingNotes.FindIndex(r => r.note == note);
if (idx != -1)
{
var one = pendingNotes[idx];
one.finishTime = finishTime;
one.activationTime = activationTime;
pendingNotes[idx] = one;
AllNotesRegistered();
_pendingNotes[idx] = new NoteRecord
{
note = note,
activationTime = activationTime,
finishTime = finishTime
};
_isDirty = true;
}
}
// 在所有物体注册完毕后,对列表进行一次排序
/// <summary>手动触发排序(若需要在注册大批 Note 后一次性完成)</summary>
public void AllNotesRegistered()
{
pendingNotes.Sort((a, b) => a.activationTime.CompareTo(b.activationTime));
SortIfDirty();
}
void Update()
private void SortIfDirty()
{
float currentTime = EditorManager.instance.songInformation.songTime;
foreach (var item in ProcessingNotes)
{
var (note, isActive, activationTime) = item;
if (!isActive) note.Update();
note.gameObject.SetActive(isActive);
if (isActive)
{
note.Update();
if (currentTime < activationTime)
{
note.noteVisual?.Recover();
}
else if (note is Hold hold && currentTime >= hold.holdEndTime)
{
hold.SetFinishEffects();
}
}
}
ProcessingNotes.Clear();
// 一次性移除所有 null 项
pendingNotes.RemoveAll(item => item.note == null);
foreach (var item in pendingNotes)
{
var (note, activationTime, finishTime) = item;
bool shouldBeActive = currentTime >= activationTime && currentTime <= finishTime;
bool isActive = note.gameObject.activeSelf;
if (shouldBeActive && !isActive)
{
ProcessingNotes.Add((note, true, activationTime));
}
else if (!shouldBeActive && isActive)
{
ProcessingNotes.Add((note, false, activationTime));
}
}
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
}
}