谱面替换
This commit is contained in:
@@ -213,7 +213,7 @@ Material:
|
||||
- _Dst: 10
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EdgeValue: 0.9624716
|
||||
- _EdgeValue: 0.59140563
|
||||
- _EnvironmentReflections: 1
|
||||
- _FNLfanxiangkaiguan: 0
|
||||
- _Face: 1
|
||||
@@ -258,7 +258,7 @@ Material:
|
||||
- _Mask_scale: 1
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Opacity: 0.037528396
|
||||
- _Opacity: 0.40859437
|
||||
- _Parallax: 0.005
|
||||
- _Pass: 0
|
||||
- _QueueOffset: 0
|
||||
|
||||
8
Assets/AddressableAssetsData/iOS.meta
Normal file
8
Assets/AddressableAssetsData/iOS.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee86eb680edff0441812416293403139
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/AddressableAssetsData/iOS/addressables_content_state.bin
Normal file
BIN
Assets/AddressableAssetsData/iOS/addressables_content_state.bin
Normal file
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a792caa2a59b77543b25793ed326a5bc
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c82452eec95d2140ac081f8c18042f3
|
||||
guid: 0955e9cb35e5f834abe8a52400d9f81a
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace Dreamteck.Splines
|
||||
public enum EmitPoint { Beginning, Ending, Random, Ordered }
|
||||
public enum MotionType { None, UseParticleSystem, FollowForward, FollowBackward, ByNormal, ByNormalRandomized }
|
||||
public enum Wrap { Default, Loop }
|
||||
public enum PathNodeSizeMode { LegacyOffset, ApplyPathNodeSize }
|
||||
|
||||
public bool is3D;
|
||||
public float width = 10f;
|
||||
@@ -51,6 +52,8 @@ namespace Dreamteck.Splines
|
||||
public float minCycles = 1f;
|
||||
[HideInInspector]
|
||||
public float maxCycles = 1f;
|
||||
[HideInInspector]
|
||||
public PathNodeSizeMode pathNodeSizeMode = PathNodeSizeMode.LegacyOffset;
|
||||
|
||||
private Dictionary<uint, Particle> _particleDataMap = new Dictionary<uint, Particle>();
|
||||
private ParticleSystem.Particle[] _particles = new ParticleSystem.Particle[0];
|
||||
|
||||
BIN
Assets/FR2_Cache.asset
LFS
BIN
Assets/FR2_Cache.asset
LFS
Binary file not shown.
@@ -27,7 +27,7 @@ RectTransform:
|
||||
m_GameObject: {fileID: 7671065637303904002}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 2, y: 2, z: 2}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
|
||||
Binary file not shown.
@@ -1 +1,2 @@
|
||||
é虎h¸DAÉ a¼ª–{„Pg…ÇßP‘@×w<C397>ÇÚY2héH<C3A9>ØU<15>^DÑmw£roV¶r<C2B6>'ƒU'(høB˜íú¼€þ4à‘ä—-øhR”‰ a'rY*®E¼À1ÇXð\¶qi´By4?±²FÔUÝyšÎôæ'°KlVGR§PkeO –û‹Ö´¬«"¢êßxIT~…vv•ÿç_H$þ:2âMH¯'òƒÑÓ™#pÔp<C394>Ð@‘
|
||||
€96×ySö(Ë¥šÜ`Ãñ™YôýôèQÖÛù$HÆ/¬-„u»›PûÁ'8nG_¼©ú07 ¹ø<I“c>ï7¯n<>j¿'S;‹Ð±/¾ÃÔˆgPE4¸Y[æ™Vö=EL5¨‘#rfëpW¾5`”ª]<5D>‘:}r1Æ´€
|
||||
<rôúý!J. Õ<>ëRyÅÕ]JRéàs¬‘áP6€ÁL\ýÝ7 <37>GºKÉ¢üÔc‡_0L²/ÄEmMKœ
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +1 @@
|
||||
7ã”Þ¨ñÂKî<4B>mI2!_YÀøFÂòô¨=²XmAÚøÊ<C3B8>.Þ;,<2C>––7S’ö[aMß7ÚOÂÔ-GP^ùsäH€>¨VÛå¾þîP$Ó{¹3Tæ<54>¬ú)ní÷æ4´V0bOy@7MÆ<4D>ä&H×ñùÁÈ}ÅKJ¤I|ÁÑ6 qîΆÎ-|ÿ;ç&X<>Á
|
||||
¤½ä2ÄnÒ.÷±t3íIÇ«]Îö•Ò~9Ň>ÑUÂ"¨2Pp¹vX~€³ÌÍ"¯ñjœâžæÅôÚß+ÝÎ=õZ°Ô'yÂýfø
|
||||
…Ús—´…@¼²OÝÏî<C38F>[”VAj§OôÚï€àdܰìnøf''l |QJæ¦K<0B>^£(e#À⻀<C2BB>zm°JIÁÞk ñ‘Ò0eÝž
|
||||
BIN
Assets/Resources/Chapter0.asset
LFS
BIN
Assets/Resources/Chapter0.asset
LFS
Binary file not shown.
@@ -11790,7 +11790,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 99971dd6462223c4596d435e8acdcfb8, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
minSwipeDistance: 100
|
||||
minSwipeDistance: 50
|
||||
swipeAngleThreshold: 1
|
||||
playerInput: {fileID: 1332204029}
|
||||
holdingTouch0: 0
|
||||
|
||||
@@ -11875,7 +11875,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 99971dd6462223c4596d435e8acdcfb8, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
minSwipeDistance: 100
|
||||
minSwipeDistance: 25
|
||||
swipeAngleThreshold: 1
|
||||
playerInput: {fileID: 1332204029}
|
||||
holdingTouch0: 0
|
||||
|
||||
@@ -25327,7 +25327,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1589396086
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -25358,9 +25358,9 @@ CanvasGroup:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1589396085}
|
||||
m_Enabled: 1
|
||||
m_Alpha: 1
|
||||
m_Interactable: 1
|
||||
m_BlocksRaycasts: 1
|
||||
m_Alpha: 0
|
||||
m_Interactable: 0
|
||||
m_BlocksRaycasts: 0
|
||||
m_IgnoreParentGroups: 0
|
||||
--- !u!1001 &1609000271
|
||||
PrefabInstance:
|
||||
@@ -30066,7 +30066,7 @@ ParticleSystem:
|
||||
ringBufferLoopRange: {x: 0, y: 1}
|
||||
emitterVelocityMode: 1
|
||||
looping: 1
|
||||
prewarm: 1
|
||||
prewarm: 0
|
||||
playOnAwake: 1
|
||||
useUnscaledTime: 0
|
||||
autoRandomSeed: 1
|
||||
@@ -34825,9 +34825,9 @@ CanvasGroup:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2019519601}
|
||||
m_Enabled: 1
|
||||
m_Alpha: 0
|
||||
m_Interactable: 0
|
||||
m_BlocksRaycasts: 0
|
||||
m_Alpha: 1
|
||||
m_Interactable: 1
|
||||
m_BlocksRaycasts: 1
|
||||
m_IgnoreParentGroups: 0
|
||||
--- !u!114 &2019519604
|
||||
MonoBehaviour:
|
||||
@@ -34861,7 +34861,7 @@ MonoBehaviour:
|
||||
m_UiScaleMode: 1
|
||||
m_ReferencePixelsPerUnit: 100
|
||||
m_ScaleFactor: 1
|
||||
m_ReferenceResolution: {x: 1920, y: 1080}
|
||||
m_ReferenceResolution: {x: 2560, y: 1440}
|
||||
m_ScreenMatchMode: 0
|
||||
m_MatchWidthOrHeight: 0
|
||||
m_PhysicalUnit: 3
|
||||
|
||||
@@ -58,9 +58,18 @@ namespace Ichni.RhythmGame
|
||||
#region [基础逻辑] Main Update Logic
|
||||
public virtual void UpdateEffect(float judgeTime)
|
||||
{
|
||||
EffectState state = CheckEffectState(judgeTime);
|
||||
float songTime = CoreServices.TimeProvider.SongTime;
|
||||
|
||||
// 零时长特效必须在到达触发时间的当帧执行。
|
||||
// 若继续进入 Middle 分支,不仅会把 Adjust() 延迟到下一帧,还会产生 0 / 0 的 NaN 进度。
|
||||
if (isInstantEffect)
|
||||
{
|
||||
UpdateInstantEffect(judgeTime, songTime);
|
||||
return;
|
||||
}
|
||||
|
||||
EffectState state = CheckEffectState(judgeTime);
|
||||
|
||||
if (state == EffectState.Before && nowEffectState != EffectState.Before)
|
||||
{
|
||||
nowEffectState = EffectState.Before;
|
||||
@@ -86,6 +95,27 @@ namespace Ichni.RhythmGame
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateInstantEffect(float triggerTime, float songTime)
|
||||
{
|
||||
if (songTime < triggerTime)
|
||||
{
|
||||
if (nowEffectState != EffectState.Before)
|
||||
{
|
||||
nowEffectState = EffectState.Before;
|
||||
effectProgressPercent = 0;
|
||||
Recover();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (nowEffectState == EffectState.After) return;
|
||||
|
||||
nowEffectState = EffectState.After;
|
||||
effectProgressPercent = 1;
|
||||
Adjust();
|
||||
}
|
||||
|
||||
protected virtual EffectState CheckEffectState(float triggerTime)
|
||||
{
|
||||
float songTime = CoreServices.TimeProvider.SongTime;
|
||||
|
||||
@@ -27,8 +27,9 @@ namespace Ichni.RhythmGame
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CheckJudgeAvailability(InputUnit inputUnit)
|
||||
public override bool CheckJudgeAvailability(InputJudgeContext context)
|
||||
{
|
||||
InputUnit inputUnit = context.InputUnit;
|
||||
if (inputUnit is InputUnitSwipe swipe && note is Flick flick)
|
||||
{
|
||||
return flick.CheckSwipeDirection(swipe);
|
||||
|
||||
@@ -44,7 +44,15 @@ namespace Ichni.RhythmGame
|
||||
|
||||
}
|
||||
|
||||
public virtual bool CheckJudgeAvailability(InputUnit inputUnit)
|
||||
public virtual void UpdateSpatialState(float songTime, Vector2 screenPosition)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void ResetSpatialState()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual bool CheckJudgeAvailability(InputJudgeContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System.Collections;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -7,11 +5,42 @@ namespace Ichni.RhythmGame
|
||||
public class TouchAreaJudgeUnit : NoteJudgeUnit
|
||||
{
|
||||
#region [暴露属性字段] Properties
|
||||
public float ellipseXMultiplier = 1.25f;
|
||||
public float ellipseYMultiplier = 1.0f;
|
||||
// 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 static float CurrentScreenRatio => Screen.height / 1080f; // 以1080p为基准的屏幕缩放比例
|
||||
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
|
||||
@@ -29,26 +58,97 @@ namespace Ichni.RhythmGame
|
||||
|
||||
public override void UpdateJudge()
|
||||
{
|
||||
if ((note is not Hold && note.isFirstJudged)||(note is Hold && note.isFinalJudged)) return;
|
||||
Vector2 noteScreenPosition = note.noteScreenPosition;
|
||||
RectTransform canvasRect = GameManager.Instance.judgeHintCanvas.GetComponent<RectTransform>();
|
||||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, noteScreenPosition, null, out Vector2 uiPosition))
|
||||
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(areaRadius * 2 * ellipseXMultiplier, areaRadius * 2 * ellipseYMultiplier) * CurrentScreenRatio;
|
||||
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 bool CheckJudgeAvailability(InputUnit inputUnit)
|
||||
public override void ResetSpatialState()
|
||||
{
|
||||
Vector2 inputScreenPosition = inputUnit.inputPosition;
|
||||
Vector2 noteScreenPosition = note.noteScreenPosition != Vector2.zero ? note.noteScreenPosition : note.GetScreenPosition();
|
||||
float scaledBaseRadius = areaRadius * CurrentScreenRatio;
|
||||
_previousSample = default;
|
||||
_currentSample = default;
|
||||
_hasPreviousSample = false;
|
||||
_hasCurrentSample = false;
|
||||
_lastStableDirection = Vector2.zero;
|
||||
_lastStableDirectionSongTime = 0f;
|
||||
_hasStableDirection = false;
|
||||
}
|
||||
|
||||
float dx = Mathf.Abs(inputScreenPosition.x - noteScreenPosition.x) / ellipseXMultiplier;
|
||||
float dy = Mathf.Abs(inputScreenPosition.y - noteScreenPosition.y) / ellipseYMultiplier;
|
||||
public override void UpdateSpatialState(float songTime, Vector2 screenPosition)
|
||||
{
|
||||
if (!IsFinite(songTime) || !IsFinite(screenPosition)) return;
|
||||
|
||||
if ((dx * dx) + (dy * dy) <= (scaledBaseRadius * scaledBaseRadius))
|
||||
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)
|
||||
{
|
||||
@@ -59,6 +159,93 @@ namespace Ichni.RhythmGame
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@ namespace Ichni.RhythmGame
|
||||
public NoteBase note;
|
||||
public NoteVisualBase noteVisual;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 在 Note 缓存最终判定姿态之前执行。
|
||||
/// 只有确实需要调整打击位置的主题特效才应重写此方法,避免改变其他主题的 Note 表现。
|
||||
/// </summary>
|
||||
public virtual void PrepareJudgedEffectPose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -51,8 +51,10 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [主循环阶段] Main Update
|
||||
public override bool ManualUpdate(float currentSongTime)
|
||||
public override void ManualPrepareForJudge(float currentSongTime)
|
||||
{
|
||||
// 这里按“本帧 SongTime”把 Flick 放入可检查列表;真正消费 Swipe 时,
|
||||
// 还必须用 InputJudgeContext.InputSongTime 再检查一次输入发生时间。
|
||||
if (!isFirstJudged && !isDuringJudging &&
|
||||
currentSongTime >= exactJudgeTime + judgeIntervals.beforeMiss.intervalStart &&
|
||||
!GameManager.Instance.noteJudgeManager.checkingFlickList.Contains(this))
|
||||
@@ -61,8 +63,13 @@ namespace Ichni.RhythmGame
|
||||
GameManager.Instance.noteJudgeManager.checkingFlickList.Add(this);
|
||||
}
|
||||
|
||||
base.ManualPrepareForJudge(currentSongTime);
|
||||
}
|
||||
|
||||
public override bool ManualResolveAfterJudge(float currentSongTime)
|
||||
{
|
||||
ExecuteFinalJudge(currentSongTime);
|
||||
return base.ManualUpdate(currentSongTime);
|
||||
return base.ManualResolveAfterJudge(currentSongTime);
|
||||
}
|
||||
|
||||
protected override void RemoveFromCheckingList()
|
||||
@@ -73,11 +80,16 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [核心判定逻辑] Judgement Logic
|
||||
public bool CheckJudgeAvailability(InputUnitSwipe inputUnitSwipe)
|
||||
public bool CheckJudgeAvailability(InputJudgeContext context)
|
||||
{
|
||||
if (context.InputUnit is not InputUnitSwipe) return false;
|
||||
|
||||
// 不能让窗口外的早期 Swipe 抢先把 Flick 以 Miss 状态移出 checkingFlickList。
|
||||
if (!IsWithinStartJudgeWindow(context.InputSongTime)) return false;
|
||||
|
||||
foreach (var judgeUnit in NoteJudgeSubmodule.judgeUnitList)
|
||||
{
|
||||
if (!judgeUnit.CheckJudgeAvailability(inputUnitSwipe)) return false;
|
||||
if (!judgeUnit.CheckJudgeAvailability(context)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -150,9 +162,6 @@ namespace Ichni.RhythmGame
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [谱面功能] Beatmap Method
|
||||
#endregion
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -73,10 +73,8 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [主循环阶段] Main Update
|
||||
public override bool ManualUpdate(float currentSongTime)
|
||||
public override void ManualPrepareForJudge(float currentSongTime)
|
||||
{
|
||||
if (!base.ManualUpdate(currentSongTime)) return false;
|
||||
|
||||
if (!isFirstJudged && !isDuringJudging &&
|
||||
currentSongTime >= exactJudgeTime + judgeIntervals.beforeMiss.intervalStart &&
|
||||
!GameManager.Instance.noteJudgeManager.checkingHoldList.Contains(this))
|
||||
@@ -85,6 +83,43 @@ namespace Ichni.RhythmGame
|
||||
GameManager.Instance.noteJudgeManager.checkingHoldList.Add(this);
|
||||
}
|
||||
|
||||
// isHolding 会在上一帧末尾重置。已经开始的 Hold 必须保留上一帧收缩后的
|
||||
// Visual,供本帧 Touch 判定使用;否则 UpdateNoteInTrack 会先把头部展开回起点。
|
||||
if (isFirstJudged && !isFinalJudged)
|
||||
{
|
||||
UpdateJudgeSpatialState(currentSongTime);
|
||||
SetJudgeArea();
|
||||
return;
|
||||
}
|
||||
|
||||
base.ManualPrepareForJudge(currentSongTime);
|
||||
}
|
||||
|
||||
protected override void UpdateSpatialStateBeforeJudgeScreenSample(float currentSongTime)
|
||||
{
|
||||
if (!isFirstJudged && exactJudgeTime < currentSongTime)
|
||||
{
|
||||
SlowOffsetAfterExactJudgeTime();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateJudgeSpatialState(float currentSongTime)
|
||||
{
|
||||
if (isDuringJudging)
|
||||
{
|
||||
UpdateJudgeScreenPosition(currentSongTime);
|
||||
}
|
||||
|
||||
if (perfectNoteScreenPosition == -Vector2.one && exactJudgeTime <= currentSongTime)
|
||||
{
|
||||
perfectNoteScreenPosition = noteScreenPosition;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ManualResolveAfterJudge(float currentSongTime)
|
||||
{
|
||||
if (!base.ManualResolveAfterJudge(currentSongTime)) return false;
|
||||
|
||||
if (!GameManager.Instance.songPlayer.isUpdating || isFinalJudged) return true;
|
||||
|
||||
if (SettingsManager.instance.settingsSaveData.autoPlay && isFirstJudged) ExecuteProcessJudge();
|
||||
@@ -99,21 +134,14 @@ namespace Ichni.RhythmGame
|
||||
bufferTimer -= Time.deltaTime;
|
||||
}
|
||||
|
||||
// Touch 判定已经在本帧设置 isHolding。现在使用最新 holdingTime 收缩 Hold,
|
||||
// 再采样实际头部位置,保证下一帧判定区域与 Visual 保持一致。
|
||||
if (isOnTrack) UpdateNoteInTrack(currentSongTime);
|
||||
|
||||
if (isDuringJudging) noteScreenPosition = GetScreenPosition();
|
||||
|
||||
if (isDuringJudging) UpdateJudgeScreenPosition(currentSongTime);
|
||||
SetJudgeArea();
|
||||
|
||||
if (!isFirstJudged && exactJudgeTime < currentSongTime) SlowOffsetAfterExactJudgeTime();
|
||||
|
||||
UpdateHoldEffects();
|
||||
|
||||
if (SettingsManager.instance.settingsSaveData.autoPlay && !isFirstJudged && currentSongTime >= exactJudgeTime)
|
||||
{
|
||||
ExecuteStartJudge(currentSongTime);
|
||||
}
|
||||
|
||||
CheckHoldLifeCycle(currentSongTime);
|
||||
|
||||
return true;
|
||||
@@ -154,22 +182,18 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [核心判定逻辑] Judgement Logic
|
||||
public bool CheckJudgeAvailability(InputUnitTap inputUnitTap)
|
||||
public bool CheckJudgeAvailability(InputJudgeContext context)
|
||||
{
|
||||
if (isFirstJudged) return false;
|
||||
foreach (var judgeUnit in NoteJudgeSubmodule.judgeUnitList)
|
||||
{
|
||||
if (!judgeUnit.CheckJudgeAvailability(inputUnitTap)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (context.InputUnit is InputUnitTap && isFirstJudged) return false;
|
||||
if (context.InputUnit is InputUnitTouch && !isFirstJudged) return false;
|
||||
if (context.InputUnit is not InputUnitTap && context.InputUnit is not InputUnitTouch) return false;
|
||||
|
||||
// 只限制 Hold 头部的 Tap;起判成功后的 Touch 需要在整段 Hold 生命周期持续生效。
|
||||
if (context.InputUnit is InputUnitTap && !IsWithinStartJudgeWindow(context.InputSongTime)) return false;
|
||||
|
||||
public bool CheckJudgeAvailability(InputUnitTouch inputUnitTouch)
|
||||
{
|
||||
if (!isFirstJudged) return false;
|
||||
foreach (var judgeUnit in NoteJudgeSubmodule.judgeUnitList)
|
||||
{
|
||||
if (!judgeUnit.CheckJudgeAvailability(inputUnitTouch)) return false;
|
||||
if (!judgeUnit.CheckJudgeAvailability(context)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ namespace Ichni.RhythmGame
|
||||
protected NoteJudgeType judgedType;
|
||||
protected bool isJudgedAndDestroying;
|
||||
protected float destroyTimer;
|
||||
protected Pose judgedEffectPose;
|
||||
protected bool hasJudgedEffectPose;
|
||||
|
||||
// 用于记录是否离开屏幕的最后一帧有效坐标
|
||||
protected Vector2 lastValidScreenPosition = -Vector2.one;
|
||||
@@ -75,8 +77,19 @@ namespace Ichni.RhythmGame
|
||||
missEffects = GetEffectListSafe("Miss");
|
||||
afterJudgeEffects = GetEffectListSafe("AfterJudge");
|
||||
|
||||
noteScreenPosition = Vector2.zero;
|
||||
perfectNoteScreenPosition = -Vector2.one;
|
||||
lastValidScreenPosition = -Vector2.one; // 初始化
|
||||
judgedEffectPose = default;
|
||||
hasJudgedEffectPose = false;
|
||||
|
||||
if (NoteJudgeSubmodule?.judgeUnitList != null)
|
||||
{
|
||||
for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++)
|
||||
{
|
||||
NoteJudgeSubmodule.judgeUnitList[i].ResetSpatialState();
|
||||
}
|
||||
}
|
||||
|
||||
float beyondTime = 0f;
|
||||
|
||||
@@ -147,7 +160,37 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [轮询更新] Main Update & Visual Calculate
|
||||
public virtual bool ManualUpdate(float currentSongTime)
|
||||
public virtual void ManualPrepareForJudge(float currentSongTime)
|
||||
{
|
||||
if (isJudgedAndDestroying || isFinalJudged) return;
|
||||
|
||||
// 判定前必须先完成本帧的轨迹、屏幕坐标与判定区域采样。
|
||||
UpdateNoteInTrack(currentSongTime);
|
||||
UpdateSpatialStateBeforeJudgeScreenSample(currentSongTime);
|
||||
UpdateJudgeSpatialState(currentSongTime);
|
||||
SetJudgeArea();
|
||||
}
|
||||
|
||||
protected virtual void UpdateSpatialStateBeforeJudgeScreenSample(float currentSongTime)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void UpdateJudgeSpatialState(float currentSongTime)
|
||||
{
|
||||
if (perfectNoteScreenPosition != -Vector2.one) return;
|
||||
|
||||
if (isDuringJudging)
|
||||
{
|
||||
UpdateJudgeScreenPosition(currentSongTime);
|
||||
}
|
||||
|
||||
if (exactJudgeTime <= currentSongTime)
|
||||
{
|
||||
perfectNoteScreenPosition = noteScreenPosition;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool ManualResolveAfterJudge(float currentSongTime)
|
||||
{
|
||||
// 若被 Judge 判定击中进入到特效摧毁倒计时
|
||||
if (isJudgedAndDestroying)
|
||||
@@ -168,21 +211,6 @@ namespace Ichni.RhythmGame
|
||||
|
||||
if (isFinalJudged) return true;
|
||||
|
||||
// 1. 轨迹更新
|
||||
UpdateNoteInTrack(currentSongTime);
|
||||
// 2. 屏幕坐标系计算缓存
|
||||
if (perfectNoteScreenPosition == -Vector2.one)
|
||||
{
|
||||
if (isDuringJudging)
|
||||
{
|
||||
noteScreenPosition = GetScreenPosition();
|
||||
}
|
||||
if (exactJudgeTime <= currentSongTime) // 代替 GameManager.Instance.songTime
|
||||
{
|
||||
perfectNoteScreenPosition = noteScreenPosition;
|
||||
}
|
||||
}
|
||||
SetJudgeArea();
|
||||
if (generateEffects != null)
|
||||
{
|
||||
for (int i = 0; i < generateEffects.Count; i++)
|
||||
@@ -237,6 +265,17 @@ namespace Ichni.RhythmGame
|
||||
}
|
||||
}
|
||||
|
||||
protected void UpdateJudgeScreenPosition(float songTime)
|
||||
{
|
||||
noteScreenPosition = GetScreenPosition();
|
||||
if (NoteJudgeSubmodule?.judgeUnitList == null) return;
|
||||
|
||||
for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++)
|
||||
{
|
||||
NoteJudgeSubmodule.judgeUnitList[i].UpdateSpatialState(songTime, noteScreenPosition);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void SetJudgeArea()
|
||||
{
|
||||
if (!SettingsManager.instance.settingsSaveData.debugMode || NoteJudgeSubmodule?.judgeUnitList == null) return;
|
||||
@@ -332,6 +371,67 @@ namespace Ichni.RhythmGame
|
||||
return judgeIntervals.GetNoteJudgeType(timeDifference);
|
||||
}
|
||||
|
||||
public NoteJudgeType PreviewStartJudgeType(float triggerTime)
|
||||
{
|
||||
// Keep candidate resolution pure. The actual Note state changes only after one candidate is selected.
|
||||
return GetStartJudgeType(triggerTime - exactJudgeTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断输入的真实发生时间是否位于该 Note 的起始判定窗口内。
|
||||
/// checking 列表使用本帧 SongTime 维护,而输入时间可能比本帧时间早几毫秒;
|
||||
/// 因此必须单独过滤窗口外输入,不能把它当成可 fallback 的 Miss 候选。
|
||||
/// </summary>
|
||||
public bool IsWithinStartJudgeWindow(float inputSongTime)
|
||||
{
|
||||
float timeDifference = inputSongTime - exactJudgeTime;
|
||||
return timeDifference >= judgeIntervals.beforeMiss.intervalStart &&
|
||||
timeDifference <= judgeIntervals.afterMiss;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取最终判定发生当帧缓存的 JudgeEffect 世界姿态。
|
||||
/// 特效应使用该姿态,而不是在延迟执行时重新读取 SongTime 或已经切换后的轨道/摄像机状态。
|
||||
/// </summary>
|
||||
public bool TryGetJudgedEffectPose(out Pose pose)
|
||||
{
|
||||
pose = judgedEffectPose;
|
||||
return hasJudgedEffectPose;
|
||||
}
|
||||
|
||||
private void CaptureJudgedEffectPose()
|
||||
{
|
||||
hasJudgedEffectPose = false;
|
||||
|
||||
Transform judgeEffectTransform = noteVisual?.judgeEffect?.transform;
|
||||
if (judgeEffectTransform == null) return;
|
||||
|
||||
judgedEffectPose = new Pose(judgeEffectTransform.position, judgeEffectTransform.rotation);
|
||||
hasJudgedEffectPose = true;
|
||||
}
|
||||
|
||||
private void PrepareJudgedEffectPose(NoteJudgeType judgeType)
|
||||
{
|
||||
List<EffectBase> judgedEffects = judgeType switch
|
||||
{
|
||||
NoteJudgeType.Perfect => perfectEffects,
|
||||
NoteJudgeType.Good => goodEffects,
|
||||
NoteJudgeType.Bad => badEffects,
|
||||
NoteJudgeType.Miss => missEffects,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (judgedEffects == null) return;
|
||||
|
||||
for (int i = 0; i < judgedEffects.Count; i++)
|
||||
{
|
||||
if (judgedEffects[i] is NoteEffectBase noteEffect)
|
||||
{
|
||||
noteEffect.PrepareJudgedEffectPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void ExecuteJudge(NoteJudgeType judgeType, float triggerTime)
|
||||
{
|
||||
isDuringJudging = false;
|
||||
@@ -339,6 +439,10 @@ namespace Ichni.RhythmGame
|
||||
judgedTriggerTime = triggerTime;
|
||||
judgedType = judgeType;
|
||||
|
||||
// 判定之后轨道、摄像机或 Note 层级都可能立即变化,必须先冻结本次打击特效的世界姿态。
|
||||
PrepareJudgedEffectPose(judgeType);
|
||||
CaptureJudgedEffectPose();
|
||||
|
||||
switch (judgeType)
|
||||
{
|
||||
case NoteJudgeType.Perfect:
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [主循环阶段] Main Update
|
||||
public override bool ManualUpdate(float currentSongTime)
|
||||
public override void ManualPrepareForJudge(float currentSongTime)
|
||||
{
|
||||
if (!isFirstJudged && !isDuringJudging &&
|
||||
currentSongTime >= exactJudgeTime + judgeIntervals.beforeMiss.intervalStart &&
|
||||
@@ -58,8 +58,13 @@ namespace Ichni.RhythmGame
|
||||
GameManager.Instance.noteJudgeManager.checkingStayList.Add(this);
|
||||
}
|
||||
|
||||
base.ManualPrepareForJudge(currentSongTime);
|
||||
}
|
||||
|
||||
public override bool ManualResolveAfterJudge(float currentSongTime)
|
||||
{
|
||||
ExecuteFinalJudge(currentSongTime);
|
||||
return base.ManualUpdate(currentSongTime);
|
||||
return base.ManualResolveAfterJudge(currentSongTime);
|
||||
}
|
||||
|
||||
protected override void RemoveFromCheckingList()
|
||||
@@ -70,11 +75,14 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [核心判定逻辑] Judgement Logic
|
||||
public bool CheckJudgeAvailability(InputUnitTouch inputUnitTouch)
|
||||
public bool CheckJudgeAvailability(InputJudgeContext context)
|
||||
{
|
||||
if (context.InputUnit is not InputUnitTouch) return false;
|
||||
if (!IsWithinStartJudgeWindow(context.InputSongTime)) return false;
|
||||
|
||||
foreach (var judgeUnit in NoteJudgeSubmodule.judgeUnitList)
|
||||
{
|
||||
if (!judgeUnit.CheckJudgeAvailability(inputUnitTouch)) return false;
|
||||
if (!judgeUnit.CheckJudgeAvailability(context)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [主循环阶段] Main Update
|
||||
public override bool ManualUpdate(float currentSongTime)
|
||||
public override void ManualPrepareForJudge(float currentSongTime)
|
||||
{
|
||||
if (!isFirstJudged && !isDuringJudging &&
|
||||
currentSongTime >= exactJudgeTime + judgeIntervals.beforeMiss.intervalStart &&
|
||||
@@ -52,7 +52,8 @@ namespace Ichni.RhythmGame
|
||||
isDuringJudging = true;
|
||||
GameManager.Instance.noteJudgeManager.checkingTapList.Add(this);
|
||||
}
|
||||
return base.ManualUpdate(currentSongTime);
|
||||
|
||||
base.ManualPrepareForJudge(currentSongTime);
|
||||
}
|
||||
|
||||
protected override void RemoveFromCheckingList()
|
||||
@@ -63,11 +64,14 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [核心判定逻辑] Judgement Logic
|
||||
public bool CheckJudgeAvailability(InputUnitTap inputUnitTap)
|
||||
public bool CheckJudgeAvailability(InputJudgeContext context)
|
||||
{
|
||||
if (context.InputUnit is not InputUnitTap) return false;
|
||||
if (!IsWithinStartJudgeWindow(context.InputSongTime)) return false;
|
||||
|
||||
foreach (var judgeUnit in NoteJudgeSubmodule.judgeUnitList)
|
||||
{
|
||||
if (!judgeUnit.CheckJudgeAvailability(inputUnitTap)) return false;
|
||||
if (!judgeUnit.CheckJudgeAvailability(context)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Ichni
|
||||
/// Phase 4 TrackCore — 更新轨道时间、裁剪区间(Track 通过 IScheduledElement 注册)
|
||||
/// Phase 5 TrackFollower — CrossTrackPoint / HeadPoint / PercentPoint
|
||||
/// [TrackManager.ManualLateUpdate — 清除 refreshedThisFrame 标记]
|
||||
/// Phase 6 Note — NoteManager 内部驱动(激活 + 更新 + 重置)
|
||||
/// Phase 6 Note — Note Prepare → Input Judge → Note Resolve → Hold Reset
|
||||
/// Phase 7 Effect — ParticleEmitter / TimeEffectsCollection / ParticleTracker / DTM 元素 / LookAt 旋转覆盖
|
||||
/// Phase 8 Misc — SkyboxSubsetter / JudgeTrigger / LowPriorityActions
|
||||
///
|
||||
@@ -198,7 +198,11 @@ namespace Ichni
|
||||
public void TickEarly(float songTime)
|
||||
{
|
||||
_isUpdating = GameManager.Instance.songPlayer.isUpdating;
|
||||
if (!_isUpdating) return;
|
||||
if (!_isUpdating)
|
||||
{
|
||||
GameManager.Instance.noteJudgeManager.DiscardPendingInputs();
|
||||
return;
|
||||
}
|
||||
|
||||
_cachedSongTime = songTime;
|
||||
|
||||
@@ -257,11 +261,13 @@ namespace Ichni
|
||||
GameManager.Instance.trackManager.ManualLateUpdate(songTime);
|
||||
}
|
||||
|
||||
// ─── Phase 6: Note(由 NoteManager 内部驱动)─────────────────────────
|
||||
// ─── Phase 6: Note(空间准备 → 输入判定 → 生命周期结算 → Hold 重置)──
|
||||
using (PhaseMarkers[PhaseIndex(UpdatePhase.Note)].Auto())
|
||||
{
|
||||
TickPhaseElements(UpdatePhase.Note, songTime);
|
||||
GameManager.Instance.noteManager.ManualUpdate(songTime);
|
||||
GameManager.Instance.noteManager.ManualPrepareForJudge(songTime);
|
||||
GameManager.Instance.noteJudgeManager.ProcessPendingInputs(songTime);
|
||||
GameManager.Instance.noteManager.ManualResolveAfterJudge(songTime);
|
||||
GameManager.Instance.noteManager.ManualLateUpdate(songTime);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Sirenix.OdinInspector;
|
||||
using TMPro;
|
||||
using UnityEngine.InputSystem.Controls;
|
||||
using UnityEngine.InputSystem.EnhancedTouch;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
using UnityEngine.UI;
|
||||
using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
|
||||
using TouchPhase = UnityEngine.InputSystem.TouchPhase;
|
||||
@@ -27,7 +28,7 @@ namespace Ichni.RhythmGame
|
||||
// =====================================================================
|
||||
|
||||
[Header("划动设置 (Swipe Settings)")] [Tooltip("识别为划动的最小移动距离(像素)")] [SerializeField]
|
||||
private float minSwipeDistance = 100f;
|
||||
private float minSwipeDistance = 50f;
|
||||
|
||||
[SerializeField] private float swipeAngleThreshold = 1f;
|
||||
|
||||
@@ -42,12 +43,13 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
public int TouchId;
|
||||
public Vector2 StartPosition;
|
||||
public float StartTime;
|
||||
public Vector2 LastPosition;
|
||||
public Vector2 LastSwipeDirection = Vector2.zero;
|
||||
public bool isFirstSwipe = true;
|
||||
}
|
||||
|
||||
private readonly Dictionary<int, TouchState> _activeTouches = new Dictionary<int, TouchState>();
|
||||
private readonly Dictionary<int, TouchState> _activeTouches = new Dictionary<int, TouchState>(10);
|
||||
private readonly Stack<TouchState> _touchStatePool = new Stack<TouchState>(10);
|
||||
|
||||
// 为鼠标模拟专门设置一个固定的Touch ID
|
||||
private const int MOUSE_TOUCH_ID = 999;
|
||||
@@ -74,6 +76,19 @@ namespace Ichni.RhythmGame
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// GameInputManager owns its EnhancedTouch dependency instead of relying on CwInput initialization.
|
||||
if (!EnhancedTouchSupport.enabled)
|
||||
{
|
||||
EnhancedTouchSupport.Enable();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR || UNITY_STANDALONE
|
||||
SetDesktopInputActionsEnabled(Touchscreen.current == null);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!GameManager.Instance.songPlayer.isUpdating)
|
||||
@@ -83,7 +98,18 @@ namespace Ichni.RhythmGame
|
||||
|
||||
// 使用预处理指令区分平台
|
||||
#if UNITY_EDITOR || UNITY_STANDALONE
|
||||
// Device Simulator and physical desktop touchscreens expose a Touchscreen device.
|
||||
// In that case use the same EnhancedTouch path as mobile builds.
|
||||
bool useTouchInput = Touchscreen.current != null;
|
||||
SetDesktopInputActionsEnabled(!useTouchInput);
|
||||
if (useTouchInput)
|
||||
{
|
||||
ProcessRealTouchInput();
|
||||
}
|
||||
else
|
||||
{
|
||||
HandleHolding();
|
||||
}
|
||||
#else
|
||||
ProcessRealTouchInput();
|
||||
#endif
|
||||
@@ -91,36 +117,45 @@ namespace Ichni.RhythmGame
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
#if UNITY_EDITOR || UNITY_STANDALONE
|
||||
foreach (TouchState state in _activeTouches.Values)
|
||||
{
|
||||
RecycleTouchState(state);
|
||||
}
|
||||
_activeTouches.Clear();
|
||||
|
||||
#if UNITY_EDITOR || UNITY_STANDALONE
|
||||
SetDesktopInputActionsEnabled(false);
|
||||
#else
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnTap(int id, Vector2 position)
|
||||
private void OnTap(int id, Vector2 position, double inputEventTime)
|
||||
{
|
||||
if (SettingsManager.instance.settingsSaveData.debugMode)
|
||||
{
|
||||
GenerateTapMark(id, position);
|
||||
}
|
||||
|
||||
GameManager.Instance.noteJudgeManager.SetNewInputUnitTap(id, position);
|
||||
GameManager.Instance.noteJudgeManager.SetNewInputUnitTap(id, position, inputEventTime);
|
||||
}
|
||||
|
||||
private void OnTouch(int id, Vector2 position)
|
||||
private void OnTouch(int id, Vector2 position, double inputEventTime)
|
||||
{
|
||||
if (SettingsManager.instance.settingsSaveData.debugMode)
|
||||
{
|
||||
GenerateTouchMark(id, position);
|
||||
}
|
||||
|
||||
GameManager.Instance.noteJudgeManager.SetNewInputUnitTouch(id, position);
|
||||
GameManager.Instance.noteJudgeManager.SetNewInputUnitTouch(id, position, inputEventTime);
|
||||
}
|
||||
|
||||
private void OnSwipe(int id, Vector2 position, bool isGeneric, bool isFirst, Vector2 direction)
|
||||
private void OnSwipe(int id, Vector2 position, bool isGeneric, bool isFirst, Vector2 direction,
|
||||
double inputEventTime, bool showDebugFeedback = true)
|
||||
{
|
||||
if (SettingsManager.instance.settingsSaveData.debugMode)
|
||||
// 此处只负责把已经识别出的 Swipe 记录为输入样本。白色/红色箭头是采集层反馈,
|
||||
// 是否命中 Flick 要等 LateUpdate 中 Note 完成空间采样后再由 NoteJudgeManager 决定。
|
||||
if (showDebugFeedback && SettingsManager.instance.settingsSaveData.debugMode)
|
||||
{
|
||||
GenerateSwipeMark(id, position, isGeneric, isFirst, direction);
|
||||
|
||||
@@ -128,7 +163,7 @@ namespace Ichni.RhythmGame
|
||||
else Debug.Log($"划动更新 - ID: {id}, 位置: {position}, 方向: {direction}");
|
||||
}
|
||||
|
||||
GameManager.Instance.noteJudgeManager.SetNewInputUnitSwipe(id, position, isGeneric, isFirst, direction);
|
||||
GameManager.Instance.noteJudgeManager.SetNewInputUnitSwipe(id, position, isGeneric, isFirst, direction, inputEventTime);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR || UNITY_STANDALONE
|
||||
@@ -140,47 +175,48 @@ namespace Ichni.RhythmGame
|
||||
if (Mouse.current == null) return;
|
||||
|
||||
Vector2 position = Mouse.current.position.ReadValue();
|
||||
double inputEventTime = InputState.currentTime;
|
||||
|
||||
if (Mouse.current.leftButton.wasPressedThisFrame)
|
||||
{
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Began, position);
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Began, position, inputEventTime);
|
||||
}
|
||||
else if (Mouse.current.leftButton.isPressed)
|
||||
{
|
||||
// 如果鼠标位置有变化,则为Moved,否则为Stationary
|
||||
if (Mouse.current.delta.ReadValue().sqrMagnitude > 0.1f)
|
||||
{
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Moved, position);
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Moved, position, inputEventTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Stationary, position);
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Stationary, position, inputEventTime);
|
||||
}
|
||||
}
|
||||
else if (Mouse.current.leftButton.wasReleasedThisFrame)
|
||||
{
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Ended, position);
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Ended, position, inputEventTime);
|
||||
}
|
||||
|
||||
if (Mouse.current.rightButton.wasPressedThisFrame)
|
||||
{
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Began, position);
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Began, position, inputEventTime);
|
||||
}
|
||||
else if (Mouse.current.rightButton.isPressed)
|
||||
{
|
||||
// 如果鼠标位置有变化,则为Moved,否则为Stationary
|
||||
if (Mouse.current.delta.ReadValue().sqrMagnitude > 0.1f)
|
||||
{
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Moved, position);
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Moved, position, inputEventTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Stationary, position);
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Stationary, position, inputEventTime);
|
||||
}
|
||||
}
|
||||
else if (Mouse.current.rightButton.wasReleasedThisFrame)
|
||||
{
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Ended, position);
|
||||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Ended, position, inputEventTime);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -201,7 +237,7 @@ namespace Ichni.RhythmGame
|
||||
if (ctx.performed)
|
||||
{
|
||||
Vector2 inputPosition = new Vector2(-600 + Screen.width * 0.5f, 200f);
|
||||
OnTap(0, inputPosition);
|
||||
OnTap(0, inputPosition, ctx.time);
|
||||
holdingTouch0 = true;
|
||||
}
|
||||
};
|
||||
@@ -211,7 +247,7 @@ namespace Ichni.RhythmGame
|
||||
if (ctx.performed)
|
||||
{
|
||||
Vector2 inputPosition = new Vector2(-200 + Screen.width * 0.5f, 200f);
|
||||
OnTap(1, inputPosition);
|
||||
OnTap(1, inputPosition, ctx.time);
|
||||
holdingTouch1 = true;
|
||||
}
|
||||
};
|
||||
@@ -221,7 +257,7 @@ namespace Ichni.RhythmGame
|
||||
if (ctx.performed)
|
||||
{
|
||||
Vector2 inputPosition = new Vector2(200 + Screen.width * 0.5f, 200f);
|
||||
OnTap(2, inputPosition);
|
||||
OnTap(2, inputPosition, ctx.time);
|
||||
holdingTouch2 = true;
|
||||
}
|
||||
};
|
||||
@@ -231,7 +267,7 @@ namespace Ichni.RhythmGame
|
||||
if (ctx.performed)
|
||||
{
|
||||
Vector2 inputPosition = new Vector2(600 + Screen.width * 0.5f, 200f);
|
||||
OnTap(3, inputPosition);
|
||||
OnTap(3, inputPosition, ctx.time);
|
||||
holdingTouch3 = true;
|
||||
}
|
||||
};
|
||||
@@ -285,36 +321,52 @@ namespace Ichni.RhythmGame
|
||||
};
|
||||
}
|
||||
|
||||
private void SetDesktopInputActionsEnabled(bool shouldEnable)
|
||||
{
|
||||
if (gameInput == null || gameInput.Game.enabled == shouldEnable) return;
|
||||
|
||||
if (shouldEnable)
|
||||
{
|
||||
gameInput.Game.Enable();
|
||||
}
|
||||
else
|
||||
{
|
||||
gameInput.Game.Disable();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleHolding()
|
||||
{
|
||||
double inputEventTime = InputState.currentTime;
|
||||
|
||||
if (holdingTouch0)
|
||||
{
|
||||
Vector2 inputPosition = new Vector2(-600 + Screen.width * 0.5f, 200f);
|
||||
OnTouch(0, inputPosition);
|
||||
OnTouch(0, inputPosition, inputEventTime);
|
||||
}
|
||||
|
||||
if (holdingTouch1)
|
||||
{
|
||||
Vector2 inputPosition = new Vector2(-200 + Screen.width * 0.5f, 200f);
|
||||
OnTouch(1, inputPosition);
|
||||
OnTouch(1, inputPosition, inputEventTime);
|
||||
}
|
||||
|
||||
if (holdingTouch2)
|
||||
{
|
||||
Vector2 inputPosition = new Vector2(200 + Screen.width * 0.5f, 200f);
|
||||
OnTouch(2, inputPosition);
|
||||
OnTouch(2, inputPosition, inputEventTime);
|
||||
}
|
||||
|
||||
if (holdingTouch3)
|
||||
{
|
||||
Vector2 inputPosition = new Vector2(600 + Screen.width * 0.5f, 200f);
|
||||
OnTouch(3, inputPosition);
|
||||
OnTouch(3, inputPosition, inputEventTime);
|
||||
}
|
||||
|
||||
if (holdingSwipe0)
|
||||
{
|
||||
Vector2 inputPosition = new Vector2(Screen.width * 0.5f, 200f);
|
||||
OnSwipe(0, inputPosition, true, false, Vector2.zero);
|
||||
OnSwipe(0, inputPosition, true, false, Vector2.zero, inputEventTime);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -328,10 +380,14 @@ namespace Ichni.RhythmGame
|
||||
|
||||
foreach (Touch touch in Touch.activeTouches)
|
||||
{
|
||||
double inputEventTime = touch.phase == TouchPhase.Stationary
|
||||
? InputState.currentTime
|
||||
: touch.time;
|
||||
ProcessInputEvent(
|
||||
touch.touchId,
|
||||
touch.phase,
|
||||
touch.screenPosition
|
||||
touch.screenPosition,
|
||||
inputEventTime
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -339,29 +395,31 @@ namespace Ichni.RhythmGame
|
||||
/// <summary>
|
||||
/// 所有输入事件的核心处理函数,无论是真实触摸还是鼠标模拟都会调用它。
|
||||
/// </summary>
|
||||
private void ProcessInputEvent(int touchId, TouchPhase phase, Vector2 position)
|
||||
private void ProcessInputEvent(int touchId, TouchPhase phase, Vector2 position, double inputEventTime)
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case TouchPhase.Began:
|
||||
var newState = new TouchState
|
||||
if (_activeTouches.TryGetValue(touchId, out TouchState existingState))
|
||||
{
|
||||
TouchId = touchId,
|
||||
StartPosition = position,
|
||||
StartTime = CoreServices.TimeProvider.SongTime,
|
||||
LastSwipeDirection = Vector2.zero,
|
||||
isFirstSwipe = true
|
||||
};
|
||||
RecycleTouchState(existingState);
|
||||
}
|
||||
|
||||
TouchState newState = GetTouchState(touchId, position);
|
||||
_activeTouches[touchId] = newState;
|
||||
OnTap(touchId, position);
|
||||
OnTouch(touchId, position);
|
||||
OnTap(touchId, position, inputEventTime);
|
||||
OnTouch(touchId, position, inputEventTime);
|
||||
break;
|
||||
|
||||
case TouchPhase.Moved:
|
||||
if (_activeTouches.TryGetValue(touchId, out TouchState movedState))
|
||||
{
|
||||
OnTouch(touchId, position);
|
||||
DetectSwipe(movedState, position);
|
||||
OnTouch(touchId, position, inputEventTime);
|
||||
bool emittedNewSwipeSegment = DetectSwipe(movedState, position, inputEventTime);
|
||||
if (!emittedNewSwipeSegment && !movedState.isFirstSwipe)
|
||||
{
|
||||
EmitContinuingSwipe(movedState, position, inputEventTime, true);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -369,15 +427,21 @@ namespace Ichni.RhythmGame
|
||||
case TouchPhase.Stationary:
|
||||
if (_activeTouches.TryGetValue(touchId, out TouchState stationaryState))
|
||||
{
|
||||
OnTouch(touchId, position);
|
||||
OnTouch(touchId, position, inputEventTime);
|
||||
if (!stationaryState.isFirstSwipe)
|
||||
{
|
||||
// Stationary keeps the Swipe available for judgment, but it is not a new movement sample.
|
||||
EmitContinuingSwipe(stationaryState, position, inputEventTime, false);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TouchPhase.Ended:
|
||||
if (_activeTouches.ContainsKey(touchId))
|
||||
if (_activeTouches.TryGetValue(touchId, out TouchState endedState))
|
||||
{
|
||||
_activeTouches.Remove(touchId);
|
||||
RecycleTouchState(endedState);
|
||||
if (SettingsManager.instance.settingsSaveData.debugMode)
|
||||
{
|
||||
GenerateEndMark(position);
|
||||
@@ -387,9 +451,10 @@ namespace Ichni.RhythmGame
|
||||
break;
|
||||
|
||||
case TouchPhase.Canceled:
|
||||
if (_activeTouches.ContainsKey(touchId))
|
||||
if (_activeTouches.TryGetValue(touchId, out TouchState canceledState))
|
||||
{
|
||||
_activeTouches.Remove(touchId);
|
||||
RecycleTouchState(canceledState);
|
||||
if (SettingsManager.instance.settingsSaveData.debugMode)
|
||||
{
|
||||
GenerateCanceledMark(position);
|
||||
@@ -401,26 +466,69 @@ namespace Ichni.RhythmGame
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测划动逻辑 (无需修改)
|
||||
/// 检测新的划动段。首次超过阈值后,该触点会保持 Swipe 状态直到 Ended/Canceled。
|
||||
/// </summary>
|
||||
private void DetectSwipe(TouchState state, Vector2 currentPosition)
|
||||
private bool DetectSwipe(TouchState state, Vector2 currentPosition, double inputEventTime)
|
||||
{
|
||||
Vector2 swipeVector = currentPosition - state.StartPosition;
|
||||
if (swipeVector.magnitude < minSwipeDistance) return;
|
||||
Vector2 movementDelta = currentPosition - state.LastPosition;
|
||||
state.LastPosition = currentPosition;
|
||||
|
||||
Vector2 direction = swipeVector.normalized;
|
||||
if (state.isFirstSwipe)
|
||||
{
|
||||
Vector2 initialSwipeVector = currentPosition - state.StartPosition;
|
||||
if (initialSwipeVector.sqrMagnitude < minSwipeDistance * minSwipeDistance) return false;
|
||||
|
||||
// 检查是否是新的划动方向
|
||||
Vector2 direction = initialSwipeVector.normalized;
|
||||
OnSwipe(state.TouchId, currentPosition, false, true, direction, inputEventTime);
|
||||
state.LastSwipeDirection = direction;
|
||||
state.isFirstSwipe = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (movementDelta.sqrMagnitude > Mathf.Epsilon)
|
||||
{
|
||||
Vector2 direction = movementDelta.normalized;
|
||||
if (Vector2.Dot(direction, state.LastSwipeDirection) <= swipeAngleThreshold)
|
||||
{
|
||||
OnSwipe(state.TouchId, state.StartPosition, false, state.isFirstSwipe, direction);
|
||||
state.LastSwipeDirection = direction;
|
||||
state.StartPosition = currentPosition;
|
||||
state.StartTime = CoreServices.TimeProvider.SongTime;
|
||||
state.isFirstSwipe = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Swipe 激活后,每个移动采样都使用当前触点位置;不再等待新的距离阈值。
|
||||
EmitContinuingSwipe(state, currentPosition, inputEventTime, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EmitContinuingSwipe(TouchState state, Vector2 currentPosition, double inputEventTime,
|
||||
bool showDebugFeedback)
|
||||
{
|
||||
OnSwipe(state.TouchId, currentPosition, false, false, state.LastSwipeDirection, inputEventTime,
|
||||
showDebugFeedback);
|
||||
}
|
||||
|
||||
private TouchState GetTouchState(int touchId, Vector2 startPosition)
|
||||
{
|
||||
TouchState state = _touchStatePool.Count > 0 ? _touchStatePool.Pop() : new TouchState();
|
||||
state.TouchId = touchId;
|
||||
state.StartPosition = startPosition;
|
||||
state.LastPosition = startPosition;
|
||||
state.LastSwipeDirection = Vector2.zero;
|
||||
state.isFirstSwipe = true;
|
||||
return state;
|
||||
}
|
||||
|
||||
private void RecycleTouchState(TouchState state)
|
||||
{
|
||||
if (state == null) return;
|
||||
|
||||
state.TouchId = 0;
|
||||
state.StartPosition = Vector2.zero;
|
||||
state.LastPosition = Vector2.zero;
|
||||
state.LastSwipeDirection = Vector2.zero;
|
||||
state.isFirstSwipe = true;
|
||||
_touchStatePool.Push(state);
|
||||
}
|
||||
|
||||
private void GenerateTapMark(int id, Vector2 pos)
|
||||
{
|
||||
RectTransform mark = LeanPool.Spawn(GameManager.Instance.basePrefabs.tapInputMark,
|
||||
|
||||
@@ -4,7 +4,7 @@ MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: -13
|
||||
executionOrder: 15
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
|
||||
@@ -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,16 +18,14 @@ 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 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 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()
|
||||
{
|
||||
@@ -35,207 +33,84 @@ namespace Ichni
|
||||
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);
|
||||
_startCandidatesBuffer.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);
|
||||
ExecuteSwipeCandidates(swipe, inputContext);
|
||||
}
|
||||
else // 原 holdSwipeList 逻辑
|
||||
{
|
||||
_availableFlicksBuffer.Sort();
|
||||
_availableFlicksBuffer[0].ExecuteStartJudge(songTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*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,76 +136,235 @@ namespace Ichni
|
||||
}
|
||||
}
|
||||
|
||||
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++)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (note.exactJudgeTime < minTime)
|
||||
{
|
||||
minTime = note.exactJudgeTime;
|
||||
destination.Clear();
|
||||
destination.Add(note);
|
||||
// 单个 Swipe 样本只选择一个候选;长划动会在后续帧继续产生样本,
|
||||
// 因而可以依次触发后续 Flick,而不会让同一输入广播命中全部重叠 Note。
|
||||
Flick selectedFlick = SelectStartCandidate(_startCandidatesBuffer, context,
|
||||
fallbackToFirst: true) as Flick;
|
||||
if (selectedFlick == null) return;
|
||||
|
||||
ExecuteSwipeJudge(selectedFlick, swipe, context.InputSongTime);
|
||||
}
|
||||
else if (Mathf.Approximately(note.exactJudgeTime, minTime))
|
||||
|
||||
private void ExecuteSwipeJudge(Flick flick, InputUnitSwipe swipe, float inputSongTime)
|
||||
{
|
||||
destination.Add(note);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private T GetNearestNote<T>(List<T> notes, Vector2 inputPosition) where T : NoteBase
|
||||
{
|
||||
float minDistance = float.MaxValue;
|
||||
T closestNote = null;
|
||||
foreach (T note in notes)
|
||||
{
|
||||
float distance = Vector2.Distance(inputPosition, note.noteScreenPosition);
|
||||
if (distance < minDistance)
|
||||
{
|
||||
minDistance = distance;
|
||||
closestNote = note;
|
||||
return note.noteScreenPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return closestNote;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,23 +372,54 @@ namespace Ichni
|
||||
{
|
||||
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,10 +428,27 @@ 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;
|
||||
|
||||
@@ -4,7 +4,7 @@ MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: -12
|
||||
executionOrder: 18
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [中央集权主循环] Manager-Driven Tick
|
||||
public void ManualUpdate(float currentSongTime)
|
||||
public void ManualPrepareForJudge(float currentSongTime)
|
||||
{
|
||||
if (!GameManager.Instance.songPlayer.isUpdating) return;
|
||||
|
||||
@@ -42,10 +42,21 @@ namespace Ichni.RhythmGame
|
||||
_nextNoteIndex++;
|
||||
}
|
||||
|
||||
// 2. 集中处理场内活跃 Note (倒序遍历以防越界)
|
||||
// 2. 判定前更新场内 Note 的最终空间状态与候选列表。
|
||||
for (int i = _activeNotes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!_activeNotes[i].ManualUpdate(currentSongTime))
|
||||
_activeNotes[i].ManualPrepareForJudge(currentSongTime);
|
||||
}
|
||||
}
|
||||
|
||||
public void ManualResolveAfterJudge(float currentSongTime)
|
||||
{
|
||||
if (!GameManager.Instance.songPlayer.isUpdating) return;
|
||||
|
||||
// 输入判定完成后再处理 Miss、Hold buffer、特效与销毁。
|
||||
for (int i = _activeNotes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!_activeNotes[i].ManualResolveAfterJudge(currentSongTime))
|
||||
{
|
||||
_activeNotes.RemoveAt(i);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
executionOrder: -14
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
ManifestFileVersion: 0
|
||||
UnityVersion: 6000.3.7f1
|
||||
CRC: 3109658480
|
||||
CRC: 53704479
|
||||
HashAppended: 0
|
||||
AssetBundleManifest:
|
||||
AssetBundleInfos:
|
||||
|
||||
Binary file not shown.
@@ -1,16 +1,16 @@
|
||||
ManifestFileVersion: 0
|
||||
UnityVersion: 6000.3.7f1
|
||||
CRC: 2950436852
|
||||
CRC: 3236759806
|
||||
Hashes:
|
||||
AssetFileHash:
|
||||
serializedVersion: 2
|
||||
Hash: d96a269b1a7c59262f46b605d1f572d6
|
||||
Hash: 83fbc969fd57eaab900477c21478720c
|
||||
TypeTreeHash:
|
||||
serializedVersion: 2
|
||||
Hash: dfed41e3b38758e5f34df30221d719dc
|
||||
IncrementalBuildHash:
|
||||
serializedVersion: 2
|
||||
Hash: debfd94a19fd386044ef29acc180950b
|
||||
Hash: 5415c53667acfa3eee32fdad43ac1d0a
|
||||
HashAppended: 0
|
||||
ClassTypes:
|
||||
- Class: 1
|
||||
|
||||
Binary file not shown.
@@ -1,16 +1,16 @@
|
||||
ManifestFileVersion: 0
|
||||
UnityVersion: 6000.3.7f1
|
||||
CRC: 3768715475
|
||||
CRC: 4192970564
|
||||
Hashes:
|
||||
AssetFileHash:
|
||||
serializedVersion: 2
|
||||
Hash: cee2c3b5a44828ab368863b9490257c3
|
||||
Hash: d290c68c3266302b3a1da71ea21801c4
|
||||
TypeTreeHash:
|
||||
serializedVersion: 2
|
||||
Hash: 8b453097cf816e7e6fe791950bd1564f
|
||||
Hash: 6d9f9b8bc1074349261a0dbbf7f0f9f8
|
||||
IncrementalBuildHash:
|
||||
serializedVersion: 2
|
||||
Hash: 7a3537e33b1ecab07e9a0bb7956f55bd
|
||||
Hash: 98fb70226d12a3b6d725a6203ec4c3bc
|
||||
HashAppended: 0
|
||||
ClassTypes:
|
||||
- Class: 1
|
||||
@@ -37,6 +37,10 @@ ClassTypes:
|
||||
Script: {fileID: 11500000, guid: 0def5156137c6d14082064fa7b5d5247, type: 3}
|
||||
- Class: 114
|
||||
Script: {fileID: 11500000, guid: 77ab80dc0820f9d4a8cd9fddbbc89057, type: 3}
|
||||
- Class: 114
|
||||
Script: {fileID: 11500000, guid: 2cc178b2441bca14eb1321c83761ab6f, type: 3}
|
||||
- Class: 114
|
||||
Script: {fileID: 11500000, guid: 8ba3055fc57abcb41811796e6a0ed3aa, type: 3}
|
||||
- Class: 114
|
||||
Script: {fileID: 11500000, guid: e8bab02f6d349f6418ccfe0674b93013, type: 3}
|
||||
- Class: 114
|
||||
@@ -47,10 +51,14 @@ ClassTypes:
|
||||
Script: {fileID: 11500000, guid: 677671f1d5140b2489dc52c18792b8f5, type: 3}
|
||||
- Class: 114
|
||||
Script: {fileID: 11500000, guid: f853de2eedbaded4489d7b898fef5926, type: 3}
|
||||
- Class: 114
|
||||
Script: {fileID: 11500000, guid: 23b750fa4c234eb995b6be5e65b56dba, type: 3}
|
||||
- Class: 114
|
||||
Script: {fileID: 11500000, guid: 638fbd0e919667f47a93485dd230c71f, type: 3}
|
||||
- Class: 114
|
||||
Script: {fileID: 11500000, guid: eb907071d10d7324995ff3aefbc2f1df, type: 3}
|
||||
- Class: 114
|
||||
Script: {fileID: 11500000, guid: 922e341faf5784c41b3b0466e0468972, type: 3}
|
||||
- Class: 115
|
||||
Script: {instanceID: 0}
|
||||
- Class: 135
|
||||
@@ -81,6 +89,7 @@ SerializeReferenceClassIdentifiers:
|
||||
- AssemblyName: Sirenix.Serialization
|
||||
ClassName: Sirenix.Serialization.SerializationData
|
||||
Assets:
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/Environment/DTM_CircleSpinTrails.prefab
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/DTM_NoteVisualTap.prefab
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Materials/Skybox/DTM_Skybox_Starry.mat
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/Environment/DTM_RandomGridTube.prefab
|
||||
@@ -88,8 +97,10 @@ Assets:
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Textures/Skybox/DTM_SunMask0.png
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/DTM_NoteVisualFlick.prefab
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/DTM_Trail2.prefab
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/Decorations/DTM_ConstellationCube.prefab
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/Environment/DTM_GlobalFog.prefab
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/DTM_Trail.prefab
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/Decorations/Constellation.prefab
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Textures/Skybox/DTM_SunMask1.png
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/Environment/DTM_StarrySkybox.prefab
|
||||
- Assets/ThemeBundles/DepartureToMultiverse/Prefabs/DTM_NoteVisualHold.prefab
|
||||
|
||||
Binary file not shown.
@@ -1,16 +1,16 @@
|
||||
ManifestFileVersion: 0
|
||||
UnityVersion: 6000.3.7f1
|
||||
CRC: 3125059414
|
||||
CRC: 1833553277
|
||||
Hashes:
|
||||
AssetFileHash:
|
||||
serializedVersion: 2
|
||||
Hash: da0193547f358bc1bf0378e49649aa75
|
||||
Hash: e08a9beb2e31cec7d5d5c5f2ed2bcccf
|
||||
TypeTreeHash:
|
||||
serializedVersion: 2
|
||||
Hash: 8ab77e54d824a64c30bc20496c5f5926
|
||||
IncrementalBuildHash:
|
||||
serializedVersion: 2
|
||||
Hash: 284ba86ec2440e4ef2177b0e314946a9
|
||||
Hash: 93d69c94859430eb482017e88174278c
|
||||
HashAppended: 0
|
||||
ClassTypes:
|
||||
- Class: 1
|
||||
|
||||
@@ -10,7 +10,7 @@ Hashes:
|
||||
Hash: bc9f87c7e64ff656997db0f2b2a91703
|
||||
IncrementalBuildHash:
|
||||
serializedVersion: 2
|
||||
Hash: 02555d72e66a23410baf71e006ec9a39
|
||||
Hash: bac15f27dfb157ff22724a110e8702fe
|
||||
HashAppended: 0
|
||||
ClassTypes:
|
||||
- Class: 28
|
||||
|
||||
@@ -28,6 +28,12 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
|
||||
{
|
||||
effectParticle = LeanPool.Spawn(noteVisual.effectPrefabList[2], noteVisual.judgeEffect.transform).GetComponent<ParticleSystem>();
|
||||
effectParticle.transform.SetParent(GameManager.Instance.cameraManager.gameCamera.transform);
|
||||
|
||||
if (note.TryGetJudgedEffectPose(out Pose pose))
|
||||
{
|
||||
effectParticle.transform.SetPositionAndRotation(pose.position, pose.rotation);
|
||||
}
|
||||
|
||||
effectParticle.Play();
|
||||
noteVisual.noteMain.SetActive(false);
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
|
||||
{
|
||||
effectParticle = LeanPool.Spawn(noteVisual.effectPrefabList[1], noteVisual.judgeEffect.transform).GetComponent<ParticleSystem>();
|
||||
effectParticle.transform.SetParent(GameManager.Instance.cameraManager.gameCamera.transform);
|
||||
|
||||
if (note.TryGetJudgedEffectPose(out Pose pose))
|
||||
{
|
||||
effectParticle.transform.SetPositionAndRotation(pose.position, pose.rotation);
|
||||
}
|
||||
|
||||
effectParticle.Play();
|
||||
noteVisual.noteMain.SetActive(false);
|
||||
|
||||
|
||||
@@ -25,12 +25,24 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
|
||||
noteVisual.noteMain.SetActive(true);
|
||||
}
|
||||
|
||||
public override void PrepareJudgedEffectPose()
|
||||
{
|
||||
// DTM 的 Perfect 打击点需要与判定当帧的轨道头对齐,并在移动后立即由 Note 缓存世界姿态。
|
||||
note.SetPerfectPosition();
|
||||
}
|
||||
|
||||
public override void Adjust()
|
||||
{
|
||||
note.SetPerfectPosition();
|
||||
//effectParticle = LeanPool.Spawn(noteVisual.effectPrefabList[0], noteVisual.judgeEffect.transform).GetComponent<ParticleSystem>();
|
||||
effectParticle = LeanPool.Spawn(noteVisual.effectPrefabList[0], noteVisual.judgeEffect.transform).GetComponent<ParticleSystem>();
|
||||
effectParticle.transform.SetParent(GameManager.Instance.cameraManager.gameCamera.transform);
|
||||
|
||||
// 摄像机或 Track 可能与判定发生在同一时间点切换,粒子必须回到判定当帧缓存的位置。
|
||||
if (note.TryGetJudgedEffectPose(out Pose pose))
|
||||
{
|
||||
effectParticle.transform.SetPositionAndRotation(pose.position, pose.rotation);
|
||||
}
|
||||
|
||||
effectParticle.Play();
|
||||
noteVisual.noteMain.SetActive(false);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user