Files
ichni_Official/Assets/Scripts/Game/GameElements/Notes/NoteObjects/NoteBase.cs
SoulliesOfficial 7580c4d87c 大更
2026-03-14 03:13:10 -04:00

484 lines
19 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 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
}