484 lines
19 KiB
C#
484 lines
19 KiB
C#
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<NoteBase>
|
||
{
|
||
#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<EffectBase> generateEffects;
|
||
protected List<EffectBase> generalJudgeEffects;
|
||
protected List<EffectBase> perfectEffects;
|
||
protected List<EffectBase> goodEffects;
|
||
protected List<EffectBase> badEffects;
|
||
protected List<EffectBase> missEffects;
|
||
protected List<EffectBase> 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<EffectBase> effects, float time)
|
||
{
|
||
if (effects == null) return;
|
||
for (int i = 0; i < effects.Count; i++)
|
||
{
|
||
effects[i].UpdateEffect(time);
|
||
}
|
||
}
|
||
|
||
protected List<EffectBase> 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
|
||
}
|