Files
ichni_Official/Assets/Scripts/Game/GameElements/Notes/NoteObjects/NoteBase.cs
SoulliesOfficial 96619b1bb5 谱面替换
2026-07-30 09:28:43 -04:00

629 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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);
}
}
/// <summary>
/// 当 NoteVisual 被动态挂载或替换后,重新缓存效果列表并刷新 NoteManager 注册。
/// </summary>
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);
}
/// <summary>
/// 判断输入的真实发生时间是否位于该 Note 的起始判定窗口内。
/// checking 列表使用本帧 SongTime 维护,而输入时间可能比本帧时间早几毫秒;
/// 因此必须单独过滤窗口外输入,不能把它当成可 fallback 的 Miss 候选。
/// </summary>
public bool IsWithinStartJudgeWindow(float inputSongTime)
{
float timeDifference = inputSongTime - exactJudgeTime;
return timeDifference >= judgeIntervals.beforeMiss.intervalStart &&
timeDifference <= judgeIntervals.afterMiss;
}
/// <summary>
/// 读取最终判定发生当帧缓存的 JudgeEffect 世界姿态。
/// 特效应使用该姿态,而不是在延迟执行时重新读取 SongTime 或已经切换后的轨道/摄像机状态。
/// </summary>
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<EffectBase> 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<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
}