谱面替换

This commit is contained in:
SoulliesOfficial
2026-07-30 09:28:43 -04:00
parent 58afd6acc1
commit 96619b1bb5
50 changed files with 1453 additions and 807 deletions

View File

@@ -1,9 +1,9 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Ichni.RhythmGame;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
namespace Ichni
{
@@ -18,224 +18,99 @@ namespace Ichni
public List<InputUnitTouch> inputUnitTouchList;
public List<InputUnitSwipe> inputUnitSwipeList;
private List<Tap> _availableTapsBuffer = new List<Tap>(30);
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<Flick> _availableFlicksBuffer = new List<Flick>(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 List<Tap> _minTapsBuffer = new List<Tap>(5);
private List<Hold> _minHoldsBuffer = new List<Hold>(5);
private List<Flick> _minFlicksBuffer = new List<Flick>(5);
private InputUnitSwipe _cachedAssumedSwipe = new InputUnitSwipe(0, Vector2.zero, true, true, Vector2.zero);
private void Start()
{
checkingTapList = new List<Tap>();
checkingStayList = new List<Stay>();
checkingHoldList = new List<Hold>();
checkingFlickList = new List<Flick>();
inputUnitTapList = new List<InputUnitTap>();
inputUnitTouchList = new List<InputUnitTouch>();
inputUnitSwipeList = new List<InputUnitSwipe>();
inputUnitTapList = new List<InputUnitTap>(10);
inputUnitTouchList = new List<InputUnitTouch>(10);
inputUnitSwipeList = new List<InputUnitSwipe>(10);
}
public void Update()
public void ProcessPendingInputs(float songTime)
{
if (!GameManager.Instance.songPlayer.isPlaying)
{
ClearInputUnits();
return;
}
float songTime = CoreServices.TimeProvider.SongTime;
double inputTime = InputState.currentTime;
// GameInputManager 在 Update 中只入队;这里位于 LateUpdate 的 Note 空间准备之后,
// 使用同一帧已经更新好的判定区域消费 Swipe。
foreach (var swipe in inputUnitSwipeList)
{
_availableFlicksBuffer.Clear();
InputJudgeContext inputContext = CreateInputJudgeContext(swipe, songTime, inputTime);
_startCandidatesBuffer.Clear();
foreach (Flick flick in checkingFlickList)
{
if (flick.CheckJudgeAvailability(swipe))
if (flick.CheckJudgeAvailability(inputContext))
{
_availableFlicksBuffer.Add(flick);
}
}
if (_availableFlicksBuffer.Count > 0)
{
if (swipe.isFirst) // 原 tapSwipeList 逻辑
{
GetAllMinNotes(_availableFlicksBuffer, _minFlicksBuffer); // 提醒:内部也需防 new List
if (_minFlicksBuffer.Count == 1) _minFlicksBuffer[0].ExecuteStartJudge(songTime);
else if (_minFlicksBuffer.Count > 1) GetNearestNote(_minFlicksBuffer, swipe.inputPosition).ExecuteTapJudge(songTime);
}
else // 原 holdSwipeList 逻辑
{
_availableFlicksBuffer.Sort();
_availableFlicksBuffer[0].ExecuteStartJudge(songTime);
_startCandidatesBuffer.Add(flick);
}
}
ExecuteSwipeCandidates(swipe, inputContext);
}
/*foreach (InputUnitSwipe tapSwipe in tapSwipeList)
{
List<Flick> availableFlicks = new List<Flick>();
foreach (Flick flick in checkingFlickList)
{
if (flick.CheckJudgeAvailability(tapSwipe))
{
availableFlicks.Add(flick);
}
}
List<Flick> minFlicks = GetAllMinNotes(availableFlicks);
if (minFlicks.Count == 1)
{
minFlicks[0].ExecuteStartJudge();
}
else if (minFlicks.Count > 1)
{
GetNearestNote(minFlicks, tapSwipe.inputPosition).ExecuteTapJudge();
}
}
foreach (InputUnitSwipe inputUnitSwipe in holdSwipeList)
{
List<Flick> availableFlicks = new List<Flick>();
foreach (Flick flick in checkingFlickList)
{
if (flick.CheckJudgeAvailability(inputUnitSwipe))
{
availableFlicks.Add(flick);
}
}
if (availableFlicks.Count > 0)
{
availableFlicks.Sort();
Flick closestFlick = availableFlicks[0];
closestFlick.ExecuteStartJudge();
}
}*/
foreach (InputUnitTap inputUnitTap in inputUnitTapList)
{
_availableTapsBuffer.Clear();
_availableHoldsBuffer.Clear();
InputJudgeContext inputContext = CreateInputJudgeContext(inputUnitTap, songTime, inputTime);
_startCandidatesBuffer.Clear();
foreach (Tap tap in checkingTapList)
{
if (tap.CheckJudgeAvailability(inputUnitTap)) _availableTapsBuffer.Add(tap);
if (tap.CheckJudgeAvailability(inputContext)) _startCandidatesBuffer.Add(tap);
}
foreach (Hold hold in checkingHoldList)
{
if (hold.CheckJudgeAvailability(inputUnitTap)) _availableHoldsBuffer.Add(hold);
if (hold.CheckJudgeAvailability(inputContext)) _startCandidatesBuffer.Add(hold);
}
bool haveTap = _availableTapsBuffer.Count > 0;
bool haveHold = _availableHoldsBuffer.Count > 0;
NoteBase selectedCandidate = SelectStartCandidate(_startCandidatesBuffer, inputContext,
fallbackToFirst: true);
if (selectedCandidate == null) continue;
Flick closestFlick = null;
InputUnitSwipe assumedSwipe = new InputUnitSwipe(inputUnitTap.fingerId, inputUnitTap.inputPosition, true, true, Vector2.zero);
_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(assumedSwipe))
if (flick.CheckJudgeAvailability(assumedSwipeContext))
{
if (closestFlick == null || flick.exactJudgeTime < closestFlick.exactJudgeTime)
{
closestFlick = flick;
}
_startCandidatesBuffer.Add(flick);
}
}
if (haveHold && haveTap)
{
GetAllMinNotes(_availableHoldsBuffer, _minHoldsBuffer);
GetAllMinNotes(_availableTapsBuffer, _minTapsBuffer);
Hold closestHold = _minHoldsBuffer[0];
Tap closestTap = _minTapsBuffer[0];
bool holdBlockedByFlick = false;
bool tapBlockedByFlick = false;
Flick blockingFlick = SelectStartCandidate(_startCandidatesBuffer, assumedSwipeContext,
fallbackToFirst: false) as Flick;
if (blockingFlick != null &&
blockingFlick.exactJudgeTime < selectedCandidate.exactJudgeTime - 0.05f) continue;
if (closestFlick != null)
{
if (closestFlick.exactJudgeTime < closestHold.exactJudgeTime - 0.05f)
{
holdBlockedByFlick = true;
}
if (closestFlick.exactJudgeTime < closestTap.exactJudgeTime - 0.05f)
{
tapBlockedByFlick = true;
}
}
if (!holdBlockedByFlick && closestHold.exactJudgeTime < closestTap.exactJudgeTime - 0.05f)
{
if (_minHoldsBuffer.Count == 1)
{
_minHoldsBuffer[0].ExecuteStartJudge(songTime);
}
else
{
GetNearestNote(_minHoldsBuffer, inputUnitTap.inputPosition).ExecuteStartJudge(songTime);
}
}
else if (!tapBlockedByFlick)
{
if (_minTapsBuffer.Count == 1)
{
_minTapsBuffer[0].ExecuteStartJudge(songTime);
}
else
{
GetNearestNote(_minTapsBuffer, inputUnitTap.inputPosition).ExecuteStartJudge(songTime);
}
}
}
else if (haveHold)
{
GetAllMinNotes(_availableHoldsBuffer, _minHoldsBuffer);
bool holdBlockedByFlick = closestFlick != null && closestFlick.exactJudgeTime < _minHoldsBuffer[0].exactJudgeTime - 0.05f;
if (!holdBlockedByFlick)
{
if (_minHoldsBuffer.Count == 1)
{
_minHoldsBuffer[0].ExecuteStartJudge(songTime);
}
else
{
GetNearestNote(_minHoldsBuffer, inputUnitTap.inputPosition).ExecuteStartJudge(songTime);
}
}
}
else if (haveTap)
{
GetAllMinNotes(_availableTapsBuffer, _minTapsBuffer);
bool tapBlockedByFlick = closestFlick != null && closestFlick.exactJudgeTime < _minTapsBuffer[0].exactJudgeTime - 0.05f;
if (!tapBlockedByFlick)
{
if (_minTapsBuffer.Count == 1)
{
_minTapsBuffer[0].ExecuteStartJudge(songTime);
}
else
{
GetNearestNote(_minTapsBuffer, inputUnitTap.inputPosition).ExecuteStartJudge(songTime);
}
}
}
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(inputUnitTouch))
if (stay.CheckJudgeAvailability(inputContext))
{
_availableStaysBuffer.Add(stay);
}
@@ -244,7 +119,7 @@ namespace Ichni
_availableHoldsBuffer.Clear();
foreach (Hold hold in checkingHoldList)
{
if (hold.CheckJudgeAvailability(inputUnitTouch))
if (hold.CheckJudgeAvailability(inputContext))
{
_availableHoldsBuffer.Add(hold);
}
@@ -252,7 +127,7 @@ namespace Ichni
foreach (Stay stay in _availableStaysBuffer)
{
stay.ExecuteStartJudge(songTime);
stay.ExecuteStartJudge(inputContext.InputSongTime);
}
foreach (Hold hold in _availableHoldsBuffer)
@@ -261,100 +136,290 @@ namespace Ichni
}
}
inputUnitTapList.Clear();
inputUnitTouchList.Clear();
inputUnitSwipeList.Clear();
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)
{
InputUnitTap inputUnitTap = new InputUnitTap(fingerId, inputPosition);
if(!inputUnitTapList.Exists(x => x.fingerId == fingerId))
{
inputUnitTapList.Add(inputUnitTap);
}
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)
{
InputUnitTouch inputUnitTouch = new InputUnitTouch(fingerId, inputPosition);
if(!inputUnitTouchList.Exists(x => x.fingerId == fingerId))
{
inputUnitTouchList.Add(inputUnitTouch);
}
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)
{
InputUnitSwipe inputUnitSwipe = new InputUnitSwipe(fingerId, inputPosition, isGeneric, isFirst, delta);
if(!inputUnitSwipeList.Exists(x => x.fingerId == fingerId))
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++)
{
inputUnitSwipeList.Add(inputUnitSwipe);
if (inputs[i].fingerId == fingerId) return true;
}
return false;
}
}
public partial class NoteJudgeManager
{
private void GetAllMinNotes<T>(List<T> availableFlicks, List<T> destination) where T : NoteBase
private void ExecuteSwipeCandidates(InputUnitSwipe swipe, InputJudgeContext context)
{
destination.Clear();
float minTime = float.MaxValue;
foreach (T note in availableFlicks)
// 单个 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)
{
if (note.exactJudgeTime < minTime)
{
minTime = note.exactJudgeTime;
destination.Clear();
destination.Add(note);
}
else if (Mathf.Approximately(note.exactJudgeTime, minTime))
{
destination.Add(note);
}
flick.ExecuteTapJudge(inputSongTime);
}
else
{
flick.ExecuteStartJudge(inputSongTime);
}
}
private T GetNearestNote<T>(List<T> notes, Vector2 inputPosition) where T : NoteBase
private NoteBase SelectStartCandidate(List<NoteBase> candidates, InputJudgeContext context,
bool fallbackToFirst)
{
float minDistance = float.MaxValue;
T closestNote = null;
foreach (T note in notes)
if (candidates.Count == 0) return null;
_candidateComparer.SetContext(context);
candidates.Sort(_candidateComparer);
for (int i = 0; i < candidates.Count; i++)
{
float distance = Vector2.Distance(inputPosition, note.noteScreenPosition);
if (distance < minDistance)
// 这里只预览判定等级,不修改 Combo、音频、特效或 Note 生命周期。
NoteBase.NoteJudgeType previewType = candidates[i].PreviewStartJudgeType(context.InputSongTime);
if (previewType is NoteBase.NoteJudgeType.Perfect or NoteBase.NoteJudgeType.Good)
{
minDistance = distance;
closestNote = note;
return candidates[i];
}
}
return closestNote;
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;
}
}
@@ -363,13 +428,30 @@ namespace Ichni
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;
}
}
}
}