using System; using System.Collections.Generic; using Dreamteck.Splines; using Sirenix.OdinInspector; using TMPro; using UnityEngine; namespace Ichni.RhythmGame { public abstract partial class NoteBase : GameElement, IHaveTimeDurationSubmodule, IComparable { #region [暴露属性字段] Basic & Info [Title("Basic Info")] public float exactJudgeTime; public NoteJudgeIntervals judgeIntervals; [Title("Track Info")] public bool isOnTrack; public Track track; public SplinePositioner trackPositioner; [Title("NoteVisual")] public NoteVisualBase noteVisual; [Title("Submodules")] public TimeDurationSubmodule timeDurationSubmodule { get; set; } public NoteJudgeSubmodule NoteJudgeSubmodule { get; set; } public NoteAudioSubmodule NoteAudioSubmodule { get; set; } [Title("In-Game Info")] public bool isDuringJudging; public Vector2 noteScreenPosition; public Vector2 perfectNoteScreenPosition; public bool isFirstJudged; public bool isFinalJudged; public override int HierarchyPriority => -10; [Title("Debug")] public TMP_Text judgeRankHint; #endregion #region [运行时缓存变量] GC-Free 核心状态层 protected List generateEffects; protected List generalJudgeEffects; protected List perfectEffects; protected List goodEffects; protected List badEffects; protected List missEffects; protected List afterJudgeEffects; protected float judgedTriggerTime; protected NoteJudgeType judgedType; protected bool isJudgedAndDestroying; protected float destroyTimer; protected Pose judgedEffectPose; protected bool hasJudgedEffectPose; // 用于记录是否离开屏幕的最后一帧有效坐标 protected Vector2 lastValidScreenPosition = -Vector2.one; #endregion #region [生命周期] Initialization public override void SetDefaultSubmodules() { timeDurationSubmodule = new TimeDurationSubmodule(this); NoteJudgeSubmodule = new NoteJudgeSubmodule(this); } public override void AfterInitialize() { // 安全读取字典防报错 generateEffects = GetEffectListSafe("Generate"); generalJudgeEffects = GetEffectListSafe("GeneralJudge"); perfectEffects = GetEffectListSafe("Perfect"); goodEffects = GetEffectListSafe("Good"); badEffects = GetEffectListSafe("Bad"); missEffects = GetEffectListSafe("Miss"); afterJudgeEffects = GetEffectListSafe("AfterJudge"); noteScreenPosition = Vector2.zero; perfectNoteScreenPosition = -Vector2.one; lastValidScreenPosition = -Vector2.one; // 初始化 judgedEffectPose = default; hasJudgedEffectPose = false; if (NoteJudgeSubmodule?.judgeUnitList != null) { for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++) { NoteJudgeSubmodule.judgeUnitList[i].ResetSpatialState(); } } float beyondTime = 0f; // 【Review修改】:使用无 GC 预存列表操作替代 String 调用 if (generateEffects != null) { for (int i = 0; i < generateEffects.Count; i++) { EffectBase effectBase = generateEffects[i]; if (effectBase is NoteGenerateEffect ge) { ge.Recover(); beyondTime = Mathf.Max(beyondTime, ge.generateTime); } else { effectBase.Recover(); } } } if (exactJudgeTime - beyondTime - 0.5f > -GameManager.Instance.songInformation.delay) { gameObject.SetActive(false); GameManager.Instance.noteManager.RegisterNote(this, exactJudgeTime - beyondTime - 0.5f); } } /// /// 当 NoteVisual 被动态挂载或替换后,重新缓存效果列表并刷新 NoteManager 注册。 /// public void RefreshNoteVisualCaches() { generateEffects = GetEffectListSafe("Generate"); generalJudgeEffects = GetEffectListSafe("GeneralJudge"); perfectEffects = GetEffectListSafe("Perfect"); goodEffects = GetEffectListSafe("Good"); badEffects = GetEffectListSafe("Bad"); missEffects = GetEffectListSafe("Miss"); afterJudgeEffects = GetEffectListSafe("AfterJudge"); float beyondTime = 0f; if (generateEffects != null) { for (int i = 0; i < generateEffects.Count; i++) { EffectBase effectBase = generateEffects[i]; if (effectBase is NoteGenerateEffect ge) { ge.nowEffectState = EffectBase.EffectState.Before; ge.Recover(); beyondTime = Mathf.Max(beyondTime, ge.generateTime); } else { effectBase.nowEffectState = EffectBase.EffectState.Before; effectBase.Recover(); } } } if (exactJudgeTime - beyondTime - 0.5f > -GameManager.Instance.songInformation.delay) { gameObject.SetActive(false); GameManager.Instance.noteManager.RegisterNote(this, exactJudgeTime - beyondTime - 0.5f); } } #endregion #region [轮询更新] Main Update & Visual Calculate public virtual void ManualPrepareForJudge(float currentSongTime) { if (isJudgedAndDestroying || isFinalJudged) return; // 判定前必须先完成本帧的轨迹、屏幕坐标与判定区域采样。 UpdateNoteInTrack(currentSongTime); UpdateSpatialStateBeforeJudgeScreenSample(currentSongTime); UpdateJudgeSpatialState(currentSongTime); SetJudgeArea(); } protected virtual void UpdateSpatialStateBeforeJudgeScreenSample(float currentSongTime) { } protected virtual void UpdateJudgeSpatialState(float currentSongTime) { if (perfectNoteScreenPosition != -Vector2.one) return; if (isDuringJudging) { UpdateJudgeScreenPosition(currentSongTime); } if (exactJudgeTime <= currentSongTime) { perfectNoteScreenPosition = noteScreenPosition; } } public virtual bool ManualResolveAfterJudge(float currentSongTime) { // 若被 Judge 判定击中进入到特效摧毁倒计时 if (isJudgedAndDestroying) { UpdatePostJudgeEffects(); destroyTimer -= Time.deltaTime; if (destroyTimer <= 0) { isJudgedAndDestroying = false; // 【池化核心】调用 NoteManager 向 LeanPool 归还自己,向外抛出 false 要求将自己从更新名单剔除 GameManager.Instance.noteManager.DespawnNote(this); return false; } return true; } if (isFinalJudged) return true; if (generateEffects != null) { for (int i = 0; i < generateEffects.Count; i++) { generateEffects[i].UpdateEffect(exactJudgeTime); } } // 自然 Miss 判定 if (!isFirstJudged && currentSongTime > exactJudgeTime + judgeIntervals.afterMiss) { Miss(exactJudgeTime + judgeIntervals.afterMiss); GameManager.Instance.playingRecorder.resultData.Add(judgeIntervals.afterMiss); isFirstJudged = true; isFinalJudged = true; RemoveFromCheckingList(); } // AutoPlay if (SettingsManager.instance.settingsSaveData.autoPlay && !isFirstJudged && currentSongTime >= exactJudgeTime) { ExecuteStartJudge(currentSongTime); } return true; // 仍然存活 } public Vector2 GetScreenPosition() { Camera cam = GameManager.Instance.cameraManager.gameCamera.cam; Vector3 screenPoint = cam.WorldToScreenPoint(noteVisual.noteVisualPosition); // 判断是否在屏幕内并且在相机前方 (z>0) bool isInsideScreen = screenPoint.z > 0 && screenPoint.x >= 0 && screenPoint.x <= Screen.width && screenPoint.y >= 0 && screenPoint.y <= Screen.height; if (isInsideScreen) { // 如果在视野中,则缓存坐标并返回 lastValidScreenPosition = new Vector2(screenPoint.x, screenPoint.y); return lastValidScreenPosition; } else { // 如果已经离开屏幕,但是曾经存在缓存,就返回离开前最后一帧坐标 if (lastValidScreenPosition != -Vector2.one) { return lastValidScreenPosition; } // 极端情况(生成时直接在屏幕外),直接返回换算的外界坐标兜底 return new Vector2(screenPoint.x, screenPoint.y); } } protected void UpdateJudgeScreenPosition(float songTime) { noteScreenPosition = GetScreenPosition(); if (NoteJudgeSubmodule?.judgeUnitList == null) return; for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++) { NoteJudgeSubmodule.judgeUnitList[i].UpdateSpatialState(songTime, noteScreenPosition); } } protected virtual void SetJudgeArea() { if (!SettingsManager.instance.settingsSaveData.debugMode || NoteJudgeSubmodule?.judgeUnitList == null) return; if (isDuringJudging && !isFirstJudged) { for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++) { var unit = NoteJudgeSubmodule.judgeUnitList[i]; if (!unit.isShowingJudge) unit.SetShowingJudge(true); } } for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++) { var unit = NoteJudgeSubmodule.judgeUnitList[i]; if (unit.isShowingJudge) unit.UpdateJudge(); } if (!isDuringJudging && isFinalJudged) { for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++) { var unit = NoteJudgeSubmodule.judgeUnitList[i]; if (unit.isShowingJudge) unit.SetShowingJudge(false); } } } #endregion #region [轨道运动] Track Logic public void UpdateNoteInTrack(float songTime) { if (!isOnTrack || track.trackTimeSubmodule == null) return; if (track.trackTimeSubmodule is TrackTimeSubmoduleMovable) { UpdateNoteInMovableTrack(songTime); } else if (track.trackTimeSubmodule is TrackTimeSubmoduleStatic) { UpdateNoteInStaticTrack(songTime); } } public virtual void UpdateNoteInMovableTrack(float songTime) { TrackTimeSubmoduleMovable trackTimeSubmoduleMovable = track.trackTimeSubmodule as TrackTimeSubmoduleMovable; trackPositioner.SetPercent(trackTimeSubmoduleMovable.GetTrackPercent(exactJudgeTime)); } public virtual void UpdateNoteInStaticTrack(float songTime) { TrackTimeSubmoduleStatic trackTimeSubmoduleStatic = track.trackTimeSubmodule as TrackTimeSubmoduleStatic; float startMove = exactJudgeTime - trackTimeSubmoduleStatic.trackTotalTime; float percent = AnimationCurveEvaluator.Evaluate(trackTimeSubmoduleStatic.animationCurveType, (songTime - startMove) / trackTimeSubmoduleStatic.trackTotalTime); percent = Mathf.Clamp01(percent); // 替代 Max 和 Min 系统调用 trackPositioner.SetPercent(1 - percent); } public virtual void SetPerfectPosition() { if (isOnTrack && track.trackTimeSubmodule is TrackTimeSubmoduleMovable movable) { float notePercent = movable.GetTrackPercent(CoreServices.TimeProvider.SongTime); trackPositioner.SetPercent(notePercent); } } protected virtual void SlowOffsetAfterExactJudgeTime() { if (isOnTrack && track.trackTimeSubmodule is TrackTimeSubmoduleMovable movable) { float slowedTime = (CoreServices.TimeProvider.SongTime - exactJudgeTime) * 0.8f; float notePercent = movable.GetTrackPercent(exactJudgeTime + slowedTime); trackPositioner.SetPercent(notePercent); } } #endregion #region [打击判定分发] Judge Dispatcher public virtual void Perfect(float triggerTime) { ExecuteJudge(NoteJudgeType.Perfect, triggerTime); } public virtual void Good(float triggerTime) { ExecuteJudge(NoteJudgeType.Good, triggerTime); } public virtual void Bad(float triggerTime){ ExecuteJudge(NoteJudgeType.Bad, triggerTime); } public virtual void Miss(float triggerTime) { ExecuteJudge(NoteJudgeType.Miss, triggerTime); } public virtual void ExecuteStartJudge(float triggerTime) {} protected virtual void RemoveFromCheckingList() => throw new NotImplementedException(); protected virtual NoteJudgeType GetStartJudgeType(float timeDifference) { return judgeIntervals.GetNoteJudgeType(timeDifference); } public NoteJudgeType PreviewStartJudgeType(float triggerTime) { // Keep candidate resolution pure. The actual Note state changes only after one candidate is selected. return GetStartJudgeType(triggerTime - exactJudgeTime); } /// /// 判断输入的真实发生时间是否位于该 Note 的起始判定窗口内。 /// checking 列表使用本帧 SongTime 维护,而输入时间可能比本帧时间早几毫秒; /// 因此必须单独过滤窗口外输入,不能把它当成可 fallback 的 Miss 候选。 /// public bool IsWithinStartJudgeWindow(float inputSongTime) { float timeDifference = inputSongTime - exactJudgeTime; return timeDifference >= judgeIntervals.beforeMiss.intervalStart && timeDifference <= judgeIntervals.afterMiss; } /// /// 读取最终判定发生当帧缓存的 JudgeEffect 世界姿态。 /// 特效应使用该姿态,而不是在延迟执行时重新读取 SongTime 或已经切换后的轨道/摄像机状态。 /// public bool TryGetJudgedEffectPose(out Pose pose) { pose = judgedEffectPose; return hasJudgedEffectPose; } private void CaptureJudgedEffectPose() { hasJudgedEffectPose = false; Transform judgeEffectTransform = noteVisual?.judgeEffect?.transform; if (judgeEffectTransform == null) return; judgedEffectPose = new Pose(judgeEffectTransform.position, judgeEffectTransform.rotation); hasJudgedEffectPose = true; } private void PrepareJudgedEffectPose(NoteJudgeType judgeType) { List judgedEffects = judgeType switch { NoteJudgeType.Perfect => perfectEffects, NoteJudgeType.Good => goodEffects, NoteJudgeType.Bad => badEffects, NoteJudgeType.Miss => missEffects, _ => null }; if (judgedEffects == null) return; for (int i = 0; i < judgedEffects.Count; i++) { if (judgedEffects[i] is NoteEffectBase noteEffect) { noteEffect.PrepareJudgedEffectPose(); } } } protected virtual void ExecuteJudge(NoteJudgeType judgeType, float triggerTime) { isDuringJudging = false; isFirstJudged = true; judgedTriggerTime = triggerTime; judgedType = judgeType; // 判定之后轨道、摄像机或 Note 层级都可能立即变化,必须先冻结本次打击特效的世界姿态。 PrepareJudgedEffectPose(judgeType); CaptureJudgedEffectPose(); switch (judgeType) { case NoteJudgeType.Perfect: GameManager.Instance.playingRecorder.AddPerfect(); break; case NoteJudgeType.Good: GameManager.Instance.playingRecorder.AddGood(); break; case NoteJudgeType.Bad: GameManager.Instance.playingRecorder.AddBad(); break; case NoteJudgeType.Miss: GameManager.Instance.playingRecorder.AddMiss(); break; } NoteAudioSubmodule.PlayNoteJudgeAudios(judgeType); if (isOnTrack) track.childElementList.Remove(this); if (NoteJudgeSubmodule?.judgeUnitList != null) { for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++) { var unit = NoteJudgeSubmodule.judgeUnitList[i]; if (unit.isShowingJudge) { unit.SetShowingJudge(false); } } } // 启动销毁计时器接管 isJudgedAndDestroying = true; destroyTimer = 1.2f; } protected virtual void UpdatePostJudgeEffects() { UpdateEffectListInternal(generalJudgeEffects, judgedTriggerTime); switch (judgedType) { case NoteJudgeType.Perfect: UpdateEffectListInternal(perfectEffects, judgedTriggerTime); break; case NoteJudgeType.Good: UpdateEffectListInternal(goodEffects, judgedTriggerTime); break; case NoteJudgeType.Bad: UpdateEffectListInternal(badEffects, judgedTriggerTime); break; case NoteJudgeType.Miss: UpdateEffectListInternal(missEffects, judgedTriggerTime); break; } UpdateEffectListInternal(afterJudgeEffects, exactJudgeTime); } // 【Review修改】:改用强制 for 循环,彻底禁用带局部参数传递的 Lambda 方法与扩展 private void UpdateEffectListInternal(List effects, float time) { if (effects == null) return; for (int i = 0; i < effects.Count; i++) { effects[i].UpdateEffect(time); } } protected List GetEffectListSafe(string key) { if (noteVisual?.effectSubmodule?.effectCollection != null && noteVisual.effectSubmodule.effectCollection.TryGetValue(key, out var list)) { return list; } return null; } public int CompareTo(NoteBase other) { return exactJudgeTime.CompareTo(other.exactJudgeTime); } #endregion } #region [类附属依赖系统] Judge Type & Intervals public abstract partial class NoteBase { public enum NoteJudgeType { Perfect = 0, Good = 1, Bad = 2, Miss = 3, NotJudged = -999 } public static NoteJudgeType GetLowerType(NoteJudgeType typeA, NoteJudgeType typeB) { if (typeA == NoteJudgeType.NotJudged) return typeB; if (typeB == NoteJudgeType.NotJudged) return typeA; return typeA > typeB ? typeA : typeB; } public class NoteJudgeIntervals { public TimeInterval beforeMiss, beforeBad, beforeGood, perfect, afterGood, afterBad; public float afterMiss; public static readonly NoteJudgeIntervals TapDefault = new NoteJudgeIntervals( new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, -0.125f), new TimeInterval(-0.125f, -0.1f), new TimeInterval(-0.1f, 0.1f), new TimeInterval(0.1f, 0.125f), new TimeInterval(0.125f, 0.15f), 0.15f); public static readonly NoteJudgeIntervals StayDefault = new NoteJudgeIntervals( new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, 0.15f), new TimeInterval(0.15f, 0.15f), new TimeInterval(0.15f, 0.15f), 0.15f); public static readonly NoteJudgeIntervals HoldDefault = new NoteJudgeIntervals( new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, -0.125f), new TimeInterval(-0.125f, -0.1f), new TimeInterval(-0.1f, 0.1f), new TimeInterval(0.1f, 0.125f), new TimeInterval(0.125f, 0.15f), 0.15f); public static readonly NoteJudgeIntervals FlickDefault = new NoteJudgeIntervals( new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, 0.15f), new TimeInterval(0.15f, 0.15f), new TimeInterval(0.15f, 0.15f), 0.15f); public NoteJudgeIntervals(TimeInterval beforeMiss, TimeInterval beforeBad, TimeInterval beforeGood, TimeInterval perfect, TimeInterval afterGood, TimeInterval afterBad, float afterMiss) { this.beforeMiss = beforeMiss; this.beforeBad = beforeBad; this.beforeGood = beforeGood; this.perfect = perfect; this.afterGood = afterGood; this.afterBad = afterBad; this.afterMiss = afterMiss; } public NoteJudgeType GetNoteJudgeType(float timeDifference) { if (beforeMiss.IsInInterval(timeDifference)) return NoteJudgeType.Miss; if (beforeBad.IsInInterval(timeDifference)) return NoteJudgeType.Bad; if (beforeGood.IsInInterval(timeDifference)) return NoteJudgeType.Good; if (perfect.IsInInterval(timeDifference)) return NoteJudgeType.Perfect; if (afterGood.IsInInterval(timeDifference)) return NoteJudgeType.Good; if (afterBad.IsInInterval(timeDifference)) return NoteJudgeType.Bad; return NoteJudgeType.Miss; } } public class TimeInterval { public float intervalStart; public float intervalEnd; public TimeInterval(float start, float end) { intervalStart = start; intervalEnd = end; } public bool IsInInterval(float time) { if (Mathf.Approximately(intervalStart, intervalEnd)) return false; return time >= intervalStart && time <= intervalEnd; } } public static string GetNoteTypeName(NoteBase note) { return note switch { Tap => "Tap", Stay => "Stay", Hold => "Hold", Flick => "Flick", _ => throw new NotImplementedException("Note type not recognized") }; } } #endregion }