Files
ichni_Official/Assets/Scripts/Game/GameElements/Notes/JudgeSubmodules/TouchAreaJudgeUnit.cs
SoulliesOfficial 96619b1bb5 谱面替换
2026-07-30 09:28:43 -04:00

254 lines
10 KiB
C#

using UnityEngine;
namespace Ichni.RhythmGame
{
public class TouchAreaJudgeUnit : NoteJudgeUnit
{
#region [] Properties
// Global tuning values. Speed uses 1080p-reference pixels per second;
// sample distance is normalized by the current screen height.
public static float DirectionalMajorAxisMultiplier = 1.10f;
public static float DirectionalMinorAxisMultiplier = 0.80f;
public static float FallbackCircleMultiplier = 0.94f;
// 面积缩放为 75% 时,长短轴半径需要统一乘以 sqrt(0.75),才能保持原有椭圆比例。
public static float JudgeAreaLinearScale = 0.866f;
public static float MinimumDirectionalSpeed = 30f;
public static float DirectionPersistenceDuration = 0.15f;
public static float MaximumSampleInterval = 0.10f;
public static float MaximumSampleDistanceRatio = 0.25f;
public static float MaximumInputSampleExtrapolation = 0.05f;
public float areaRadius;
private ScreenSample _previousSample;
private ScreenSample _currentSample;
private bool _hasPreviousSample;
private bool _hasCurrentSample;
private Vector2 _lastStableDirection;
private float _lastStableDirectionSongTime;
private bool _hasStableDirection;
private static float CurrentScreenRatio => Mathf.Max(Screen.height / 1080f, 0.0001f);
private struct ScreenSample
{
public float songTime;
public Vector2 position;
public ScreenSample(float songTime, Vector2 position)
{
this.songTime = songTime;
this.position = position;
}
}
#endregion
#region [] Initialization
public TouchAreaJudgeUnit(NoteBase note, float areaRadius) : base(note)
{
this.areaRadius = areaRadius;
}
#endregion
#region [] Judge Overrides
protected override GameObject GetHintImagePrefab()
{
return GameManager.Instance.basePrefabs.areaHint;
}
public override void UpdateJudge()
{
if (judgeHintImage == null ||
(note is not Hold && note.isFirstJudged) ||
(note is Hold && note.isFinalJudged)) return;
GetJudgeGeometry(CoreServices.TimeProvider.SongTime, out Vector2 center, out Vector2 majorDirection,
out float majorRadius, out float minorRadius);
Canvas canvas = GameManager.Instance.judgeHintCanvas;
RectTransform canvasRect = canvas.GetComponent<RectTransform>();
Camera eventCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
Vector2 minorDirection = new Vector2(-majorDirection.y, majorDirection.x);
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, center, eventCamera,
out Vector2 uiPosition) &&
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect,
center + majorDirection * majorRadius, eventCamera, out Vector2 uiMajorPoint) &&
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect,
center + minorDirection * minorRadius, eventCamera, out Vector2 uiMinorPoint))
{
Vector2 uiMajorAxis = uiMajorPoint - uiPosition;
Vector2 uiMinorAxis = uiMinorPoint - uiPosition;
judgeHintImage.anchoredPosition = uiPosition;
judgeHintImage.sizeDelta = new Vector2(uiMajorAxis.magnitude * 2f, uiMinorAxis.magnitude * 2f);
judgeHintImage.localRotation = Quaternion.Euler(0f, 0f,
Mathf.Atan2(uiMajorAxis.y, uiMajorAxis.x) * Mathf.Rad2Deg);
}
}
public override void ResetSpatialState()
{
_previousSample = default;
_currentSample = default;
_hasPreviousSample = false;
_hasCurrentSample = false;
_lastStableDirection = Vector2.zero;
_lastStableDirectionSongTime = 0f;
_hasStableDirection = false;
}
public override void UpdateSpatialState(float songTime, Vector2 screenPosition)
{
if (!IsFinite(songTime) || !IsFinite(screenPosition)) return;
ScreenSample newSample = new ScreenSample(songTime, screenPosition);
if (!_hasCurrentSample)
{
_currentSample = newSample;
_hasCurrentSample = true;
return;
}
if (Mathf.Approximately(songTime, _currentSample.songTime))
{
_currentSample = newSample;
return;
}
if (!IsValidTransition(_currentSample, newSample))
{
_currentSample = newSample;
_hasPreviousSample = false;
_hasStableDirection = false;
return;
}
_previousSample = _currentSample;
_currentSample = newSample;
_hasPreviousSample = true;
Vector2 displacement = _currentSample.position - _previousSample.position;
float duration = _currentSample.songTime - _previousSample.songTime;
float referenceSpeed = displacement.magnitude / CurrentScreenRatio / duration;
if (displacement.sqrMagnitude > Mathf.Epsilon && referenceSpeed >= MinimumDirectionalSpeed)
{
_lastStableDirection = displacement.normalized;
_lastStableDirectionSongTime = _currentSample.songTime;
_hasStableDirection = true;
}
}
public override bool CheckJudgeAvailability(InputJudgeContext context)
{
InputUnit inputUnit = context.InputUnit;
GetJudgeGeometry(context.InputSongTime, out Vector2 center, out Vector2 majorDirection,
out float majorRadius, out float minorRadius);
Vector2 inputOffset = context.InputPosition - center;
Vector2 minorDirection = new Vector2(-majorDirection.y, majorDirection.x);
float along = Vector2.Dot(inputOffset, majorDirection) / Mathf.Max(majorRadius, Mathf.Epsilon);
float across = Vector2.Dot(inputOffset, minorDirection) / Mathf.Max(minorRadius, Mathf.Epsilon);
if ((along * along) + (across * across) <= 1f)
{
if (inputUnit is InputUnitSwipe swipe && note is Flick flick)
{
return flick.CheckSwipeDirection(swipe);
}
return true;
}
return false;
}
public Vector2 GetJudgeCenter(float songTime)
{
return ResolveCenter(songTime);
}
private void GetJudgeGeometry(float songTime, out Vector2 center, out Vector2 majorDirection,
out float majorRadius, out float minorRadius)
{
center = ResolveCenter(songTime);
float scaledBaseRadius = Mathf.Max(areaRadius, 0f) * CurrentScreenRatio * JudgeAreaLinearScale;
if (TryResolveDirection(songTime, out majorDirection))
{
majorRadius = scaledBaseRadius * DirectionalMajorAxisMultiplier;
minorRadius = scaledBaseRadius * DirectionalMinorAxisMultiplier;
return;
}
majorDirection = Vector2.right;
majorRadius = scaledBaseRadius * FallbackCircleMultiplier;
minorRadius = majorRadius;
}
private Vector2 ResolveCenter(float songTime)
{
if (!_hasCurrentSample)
{
return note.noteScreenPosition != Vector2.zero
? note.noteScreenPosition
: note.GetScreenPosition();
}
if (!_hasPreviousSample) return _currentSample.position;
if (songTime <= _previousSample.songTime) return _previousSample.position;
if (songTime >= _currentSample.songTime) return _currentSample.position;
float interpolation = Mathf.InverseLerp(_previousSample.songTime, _currentSample.songTime, songTime);
return Vector2.LerpUnclamped(_previousSample.position, _currentSample.position, interpolation);
}
private bool TryResolveDirection(float songTime, out Vector2 direction)
{
if (_hasPreviousSample &&
songTime >= _previousSample.songTime - MaximumInputSampleExtrapolation &&
songTime <= _currentSample.songTime + MaximumInputSampleExtrapolation)
{
Vector2 displacement = _currentSample.position - _previousSample.position;
float duration = _currentSample.songTime - _previousSample.songTime;
float referenceSpeed = displacement.magnitude / CurrentScreenRatio / duration;
if (displacement.sqrMagnitude > Mathf.Epsilon && referenceSpeed >= MinimumDirectionalSpeed)
{
direction = displacement.normalized;
return true;
}
}
if (_hasStableDirection &&
Mathf.Abs(songTime - _lastStableDirectionSongTime) <= DirectionPersistenceDuration)
{
direction = _lastStableDirection;
return true;
}
direction = Vector2.right;
return false;
}
private static bool IsValidTransition(ScreenSample from, ScreenSample to)
{
float duration = to.songTime - from.songTime;
if (duration <= Mathf.Epsilon || duration > MaximumSampleInterval) return false;
float screenHeight = Mathf.Max(Screen.height, 1f);
float distanceRatio = Vector2.Distance(from.position, to.position) / screenHeight;
return distanceRatio <= MaximumSampleDistanceRatio;
}
private static bool IsFinite(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value);
}
private static bool IsFinite(Vector2 value)
{
return IsFinite(value.x) && IsFinite(value.y);
}
#endregion
}
}