254 lines
10 KiB
C#
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
|
|
|
|
}
|
|
|
|
}
|