458 lines
17 KiB
C#
458 lines
17 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|