Files
ichni_Official/Assets/Scripts/Manager/NoteJudgeManager.cs
SoulliesOfficial 96619b1bb5 谱面替换
2026-07-30 09:28:43 -04:00

458 lines
17 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.Collections.Generic;
using Ichni.RhythmGame;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
namespace Ichni
{
public partial class NoteJudgeManager : SerializedMonoBehaviour
{
public List<Tap> checkingTapList;
public List<Stay> checkingStayList;
public List<Hold> checkingHoldList;
public List<Flick> checkingFlickList;
public List<InputUnitTap> inputUnitTapList;
public List<InputUnitTouch> inputUnitTouchList;
public List<InputUnitSwipe> inputUnitSwipeList;
private readonly List<NoteBase> _startCandidatesBuffer = new List<NoteBase>(40);
private readonly NoteCandidateComparer _candidateComparer = new NoteCandidateComparer();
private List<Hold> _availableHoldsBuffer = new List<Hold>(10);
private List<Stay> _availableStaysBuffer = new List<Stay>(30);
private readonly Stack<InputUnitTap> _tapInputPool = new Stack<InputUnitTap>(10);
private readonly Stack<InputUnitTouch> _touchInputPool = new Stack<InputUnitTouch>(10);
private readonly Stack<InputUnitSwipe> _swipeInputPool = new Stack<InputUnitSwipe>(10);
private readonly InputUnitSwipe _assumedSwipeInput = new InputUnitSwipe();
private void Start()
{
checkingTapList = new List<Tap>();
checkingStayList = new List<Stay>();
checkingHoldList = new List<Hold>();
checkingFlickList = new List<Flick>();
inputUnitTapList = new List<InputUnitTap>(10);
inputUnitTouchList = new List<InputUnitTouch>(10);
inputUnitSwipeList = new List<InputUnitSwipe>(10);
}
public void ProcessPendingInputs(float songTime)
{
if (!GameManager.Instance.songPlayer.isPlaying)
{
ClearInputUnits();
return;
}
double inputTime = InputState.currentTime;
// GameInputManager 在 Update 中只入队;这里位于 LateUpdate 的 Note 空间准备之后,
// 使用同一帧已经更新好的判定区域消费 Swipe。
foreach (var swipe in inputUnitSwipeList)
{
InputJudgeContext inputContext = CreateInputJudgeContext(swipe, songTime, inputTime);
_startCandidatesBuffer.Clear();
foreach (Flick flick in checkingFlickList)
{
if (flick.CheckJudgeAvailability(inputContext))
{
_startCandidatesBuffer.Add(flick);
}
}
ExecuteSwipeCandidates(swipe, inputContext);
}
foreach (InputUnitTap inputUnitTap in inputUnitTapList)
{
InputJudgeContext inputContext = CreateInputJudgeContext(inputUnitTap, songTime, inputTime);
_startCandidatesBuffer.Clear();
foreach (Tap tap in checkingTapList)
{
if (tap.CheckJudgeAvailability(inputContext)) _startCandidatesBuffer.Add(tap);
}
foreach (Hold hold in checkingHoldList)
{
if (hold.CheckJudgeAvailability(inputContext)) _startCandidatesBuffer.Add(hold);
}
NoteBase selectedCandidate = SelectStartCandidate(_startCandidatesBuffer, inputContext,
fallbackToFirst: true);
if (selectedCandidate == null) continue;
_assumedSwipeInput.Set(inputUnitTap.fingerId, inputUnitTap.inputPosition,
true, true, Vector2.zero, inputUnitTap.inputEventTime);
InputJudgeContext assumedSwipeContext = inputContext.WithInputUnit(_assumedSwipeInput);
_startCandidatesBuffer.Clear();
foreach (Flick flick in checkingFlickList)
{
if (flick.CheckJudgeAvailability(assumedSwipeContext))
{
_startCandidatesBuffer.Add(flick);
}
}
Flick blockingFlick = SelectStartCandidate(_startCandidatesBuffer, assumedSwipeContext,
fallbackToFirst: false) as Flick;
if (blockingFlick != null &&
blockingFlick.exactJudgeTime < selectedCandidate.exactJudgeTime - 0.05f) continue;
selectedCandidate.ExecuteStartJudge(inputContext.InputSongTime);
}
foreach (InputUnitTouch inputUnitTouch in inputUnitTouchList)
{
InputJudgeContext inputContext = CreateInputJudgeContext(inputUnitTouch, songTime, inputTime);
_availableStaysBuffer.Clear();
foreach (Stay stay in checkingStayList)
{
if (stay.CheckJudgeAvailability(inputContext))
{
_availableStaysBuffer.Add(stay);
}
}
_availableHoldsBuffer.Clear();
foreach (Hold hold in checkingHoldList)
{
if (hold.CheckJudgeAvailability(inputContext))
{
_availableHoldsBuffer.Add(hold);
}
}
foreach (Stay stay in _availableStaysBuffer)
{
stay.ExecuteStartJudge(inputContext.InputSongTime);
}
foreach (Hold hold in _availableHoldsBuffer)
{
hold.ExecuteProcessJudge();
}
}
ClearInputUnits();
}
public void DiscardPendingInputs()
{
ClearInputUnits();
}
private void ClearInputUnits()
{
if (inputUnitTapList != null)
{
for (int i = 0; i < inputUnitTapList.Count; i++) _tapInputPool.Push(inputUnitTapList[i]);
inputUnitTapList.Clear();
}
if (inputUnitTouchList != null)
{
for (int i = 0; i < inputUnitTouchList.Count; i++) _touchInputPool.Push(inputUnitTouchList[i]);
inputUnitTouchList.Clear();
}
if (inputUnitSwipeList != null)
{
for (int i = 0; i < inputUnitSwipeList.Count; i++) _swipeInputPool.Push(inputUnitSwipeList[i]);
inputUnitSwipeList.Clear();
}
}
private static float ResolveInputSongTime(InputUnit inputUnit, float currentSongTime, double currentInputTime)
{
// Input System 时间戳与 currentTime 使用同一时钟。用队列年龄回推 SongTime
// 避免 LateUpdate 消费时把输入时刻错误替换为更晚的当前歌曲时间。
double inputAge = currentInputTime - inputUnit.inputEventTime;
if (double.IsNaN(inputAge) || double.IsInfinity(inputAge) || inputAge < 0d)
{
inputAge = 0d;
}
return currentSongTime - (float)inputAge;
}
private static InputJudgeContext CreateInputJudgeContext(InputUnit inputUnit, float currentSongTime,
double currentInputTime)
{
return new InputJudgeContext(inputUnit,
ResolveInputSongTime(inputUnit, currentSongTime, currentInputTime));
}
public void SetNewInputUnitTap(int fingerId, Vector2 inputPosition)
{
SetNewInputUnitTap(fingerId, inputPosition, InputState.currentTime);
}
public void SetNewInputUnitTap(int fingerId, Vector2 inputPosition, double inputEventTime)
{
if (ContainsFinger(inputUnitTapList, fingerId)) return;
InputUnitTap inputUnitTap = _tapInputPool.Count > 0 ? _tapInputPool.Pop() : new InputUnitTap();
inputUnitTap.Set(fingerId, inputPosition, inputEventTime);
inputUnitTapList.Add(inputUnitTap);
}
public void SetNewInputUnitTouch(int fingerId, Vector2 inputPosition)
{
SetNewInputUnitTouch(fingerId, inputPosition, InputState.currentTime);
}
public void SetNewInputUnitTouch(int fingerId, Vector2 inputPosition, double inputEventTime)
{
if (ContainsFinger(inputUnitTouchList, fingerId)) return;
InputUnitTouch inputUnitTouch = _touchInputPool.Count > 0 ? _touchInputPool.Pop() : new InputUnitTouch();
inputUnitTouch.Set(fingerId, inputPosition, inputEventTime);
inputUnitTouchList.Add(inputUnitTouch);
}
public void SetNewInputUnitSwipe(int fingerId, Vector2 inputPosition, bool isGeneric, bool isFirst, Vector2 delta)
{
SetNewInputUnitSwipe(fingerId, inputPosition, isGeneric, isFirst, delta, InputState.currentTime);
}
public void SetNewInputUnitSwipe(int fingerId, Vector2 inputPosition, bool isGeneric, bool isFirst, Vector2 delta, double inputEventTime)
{
// 每根手指每帧只保留一个 Swipe 样本;下一帧队列清空后,同一条长划动仍可继续触发 Flick。
if (ContainsFinger(inputUnitSwipeList, fingerId)) return;
InputUnitSwipe inputUnitSwipe = _swipeInputPool.Count > 0 ? _swipeInputPool.Pop() : new InputUnitSwipe();
inputUnitSwipe.Set(fingerId, inputPosition, isGeneric, isFirst, delta, inputEventTime);
inputUnitSwipeList.Add(inputUnitSwipe);
}
private static bool ContainsFinger<TInput>(List<TInput> inputs, int fingerId) where TInput : InputUnit
{
for (int i = 0; i < inputs.Count; i++)
{
if (inputs[i].fingerId == fingerId) return true;
}
return false;
}
}
public partial class NoteJudgeManager
{
private void ExecuteSwipeCandidates(InputUnitSwipe swipe, InputJudgeContext context)
{
// 单个 Swipe 样本只选择一个候选;长划动会在后续帧继续产生样本,
// 因而可以依次触发后续 Flick而不会让同一输入广播命中全部重叠 Note。
Flick selectedFlick = SelectStartCandidate(_startCandidatesBuffer, context,
fallbackToFirst: true) as Flick;
if (selectedFlick == null) return;
ExecuteSwipeJudge(selectedFlick, swipe, context.InputSongTime);
}
private void ExecuteSwipeJudge(Flick flick, InputUnitSwipe swipe, float inputSongTime)
{
if (swipe.isFirst && CountCandidatesAtJudgeTime(flick.exactJudgeTime) > 1)
{
flick.ExecuteTapJudge(inputSongTime);
}
else
{
flick.ExecuteStartJudge(inputSongTime);
}
}
private NoteBase SelectStartCandidate(List<NoteBase> candidates, InputJudgeContext context,
bool fallbackToFirst)
{
if (candidates.Count == 0) return null;
_candidateComparer.SetContext(context);
candidates.Sort(_candidateComparer);
for (int i = 0; i < candidates.Count; i++)
{
// 这里只预览判定等级,不修改 Combo、音频、特效或 Note 生命周期。
NoteBase.NoteJudgeType previewType = candidates[i].PreviewStartJudgeType(context.InputSongTime);
if (previewType is NoteBase.NoteJudgeType.Perfect or NoteBase.NoteJudgeType.Good)
{
return candidates[i];
}
}
return fallbackToFirst ? candidates[0] : null;
}
private int CountCandidatesAtJudgeTime(float exactJudgeTime)
{
int count = 0;
for (int i = 0; i < _startCandidatesBuffer.Count; i++)
{
if (Mathf.Approximately(_startCandidatesBuffer[i].exactJudgeTime, exactJudgeTime))
{
count++;
}
}
return count;
}
private sealed class NoteCandidateComparer : IComparer<NoteBase>
{
private Vector2 _inputPosition;
private float _inputSongTime;
public void SetContext(InputJudgeContext context)
{
_inputPosition = context.InputPosition;
_inputSongTime = context.InputSongTime;
}
public int Compare(NoteBase left, NoteBase right)
{
if (ReferenceEquals(left, right)) return 0;
if (left == null) return 1;
if (right == null) return -1;
int timeComparison = left.exactJudgeTime.CompareTo(right.exactJudgeTime);
if (timeComparison != 0) return timeComparison;
float leftDistance = (GetCandidateScreenPosition(left, _inputSongTime) - _inputPosition).sqrMagnitude;
float rightDistance = (GetCandidateScreenPosition(right, _inputSongTime) - _inputPosition).sqrMagnitude;
int distanceComparison = leftDistance.CompareTo(rightDistance);
if (distanceComparison != 0) return distanceComparison;
return left.elementGuid.CompareTo(right.elementGuid);
}
private static Vector2 GetCandidateScreenPosition(NoteBase note, float inputSongTime)
{
if (note.NoteJudgeSubmodule?.judgeUnitList != null)
{
for (int i = 0; i < note.NoteJudgeSubmodule.judgeUnitList.Count; i++)
{
if (note.NoteJudgeSubmodule.judgeUnitList[i] is TouchAreaJudgeUnit touchArea)
{
return touchArea.GetJudgeCenter(inputSongTime);
}
}
}
return note.noteScreenPosition;
}
}
}
public readonly struct InputJudgeContext
{
// InputUnit 保存设备事件InputSongTime 保存该事件映射到谱面时间轴后的发生时刻。
// JudgeUnit 必须使用本上下文,不能在判定途中重新读取当前 SongTime。
public InputUnit InputUnit { get; }
public float InputSongTime { get; }
public int FingerId => InputUnit.fingerId;
public Vector2 InputPosition => InputUnit.inputPosition;
public double InputEventTime => InputUnit.inputEventTime;
public InputJudgeContext(InputUnit inputUnit, float inputSongTime)
{
InputUnit = inputUnit ?? throw new System.ArgumentNullException(nameof(inputUnit));
InputSongTime = inputSongTime;
}
public InputJudgeContext WithInputUnit(InputUnit inputUnit)
{
return new InputJudgeContext(inputUnit, InputSongTime);
}
}
public class InputUnit
{
public int fingerId;
public Vector2 inputPosition;
public double inputEventTime;
}
public class InputUnitTap : InputUnit
{
public InputUnitTap()
{
}
public InputUnitTap(int fingerId, Vector2 inputPosition)
: this(fingerId, inputPosition, InputState.currentTime)
{
}
public InputUnitTap(int fingerId, Vector2 inputPosition, double inputEventTime)
{
Set(fingerId, inputPosition, inputEventTime);
}
public void Set(int fingerId, Vector2 inputPosition, double inputEventTime)
{
this.fingerId = fingerId;
this.inputPosition = inputPosition;
this.inputEventTime = inputEventTime;
}
}
public class InputUnitTouch : InputUnit
{
public InputUnitTouch()
{
}
public InputUnitTouch(int fingerId, Vector2 inputPosition)
: this(fingerId, inputPosition, InputState.currentTime)
{
}
public InputUnitTouch(int fingerId, Vector2 inputPosition, double inputEventTime)
{
Set(fingerId, inputPosition, inputEventTime);
}
public void Set(int fingerId, Vector2 inputPosition, double inputEventTime)
{
this.fingerId = fingerId;
this.inputPosition = inputPosition;
this.inputEventTime = inputEventTime;
}
}
public class InputUnitSwipe : InputUnit
{
public Vector2 swipeDirection;
public bool isGeneric;
public bool isFirst;
public InputUnitSwipe()
{
}
public InputUnitSwipe(int fingerId, Vector2 inputPosition, bool isGeneric, bool isFirst, Vector2 swipeDirection)
: this(fingerId, inputPosition, isGeneric, isFirst, swipeDirection, InputState.currentTime)
{
}
public InputUnitSwipe(int fingerId, Vector2 inputPosition, bool isGeneric, bool isFirst, Vector2 swipeDirection,
double inputEventTime)
{
Set(fingerId, inputPosition, isGeneric, isFirst, swipeDirection, inputEventTime);
}
public void Set(int fingerId, Vector2 inputPosition, bool isGeneric, bool isFirst, Vector2 swipeDirection,
double inputEventTime)
{
this.fingerId = fingerId;
this.inputPosition = inputPosition;
this.inputEventTime = inputEventTime;
this.isGeneric = isGeneric;
this.isFirst = isFirst;
this.swipeDirection = swipeDirection.normalized;
}
}
}