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 checkingTapList; public List checkingStayList; public List checkingHoldList; public List checkingFlickList; public List inputUnitTapList; public List inputUnitTouchList; public List inputUnitSwipeList; private readonly List _startCandidatesBuffer = new List(40); private readonly NoteCandidateComparer _candidateComparer = new NoteCandidateComparer(); private List _availableHoldsBuffer = new List(10); private List _availableStaysBuffer = new List(30); private readonly Stack _tapInputPool = new Stack(10); private readonly Stack _touchInputPool = new Stack(10); private readonly Stack _swipeInputPool = new Stack(10); private readonly InputUnitSwipe _assumedSwipeInput = new InputUnitSwipe(); private void Start() { checkingTapList = new List(); checkingStayList = new List(); checkingHoldList = new List(); checkingFlickList = new List(); inputUnitTapList = new List(10); inputUnitTouchList = new List(10); inputUnitSwipeList = new List(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(List 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 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 { 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; } } }