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 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"); perfectNoteScreenPosition = -Vector2.one; lastValidScreenPosition = -Vector2.one; // 初始化 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); } } #endregion #region [轮询更新] Main Update & Visual Calculate public virtual bool ManualUpdate(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; // 1. 轨迹更新 UpdateNoteInTrack(currentSongTime); // 2. 屏幕坐标系计算缓存 if (perfectNoteScreenPosition == -Vector2.one) { if (isDuringJudging) { noteScreenPosition = GetScreenPosition(); } if (exactJudgeTime <= currentSongTime) // 代替 GameManager.Instance.songTime { perfectNoteScreenPosition = noteScreenPosition; } } SetJudgeArea(); 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.gameSettings.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 virtual void SetJudgeArea() { if (!SettingsManager.instance.gameSettings.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); } protected virtual void ExecuteJudge(NoteJudgeType judgeType, float triggerTime) { isDuringJudging = false; judgedTriggerTime = triggerTime; judgedType = judgeType; 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 }