From e37b6af9844f74e2eeb46350b48e31177aae48bc Mon Sep 17 00:00:00 2001 From: NINI Date: Mon, 5 Oct 2026 03:54:53 +0900 Subject: [PATCH] Preserve short touch events through official EnhancedTouch history --- .../Tests/PlayMode/FirstPuzzlePlayTests.cs | 73 ++++++++ .../UnityRuntime/Interaction/BoardInput.cs | 169 +++++++++++------- 2 files changed, 181 insertions(+), 61 deletions(-) diff --git a/Assets/MagnetismHeroes/Tests/PlayMode/FirstPuzzlePlayTests.cs b/Assets/MagnetismHeroes/Tests/PlayMode/FirstPuzzlePlayTests.cs index 5da96fa..f6854da 100644 --- a/Assets/MagnetismHeroes/Tests/PlayMode/FirstPuzzlePlayTests.cs +++ b/Assets/MagnetismHeroes/Tests/PlayMode/FirstPuzzlePlayTests.cs @@ -60,6 +60,79 @@ namespace MagnetismHeroes.Tests } private void Drag(Vector2 start, Vector2 end) { _app.Input.Begin(start, false); _app.Input.Move(end); _app.Input.End(end); } private void Tap(Vector2 point) { _app.Input.Begin(point, false); _app.Input.End(point); } + private void QueueTouch(int id, UnityEngine.InputSystem.TouchPhase phase, Vector2 position) => + InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = id, phase = phase, position = position }); + + [UnityTest] public IEnumerator SameUpdateShortTapRespondsBeforeCollectionAndCountsOnceAfterCollection() + { + _touchscreen = InputSystem.AddDevice(); _app.StartStage(1); yield return null; Physics.SyncTransforms(); + var point = BallPoint(); QueueTouch(101, UnityEngine.InputSystem.TouchPhase.Began, point); QueueTouch(101, UnityEngine.InputSystem.TouchPhase.Ended, point); + yield return null; yield return null; + Assert.That(_app.Instruction, Does.Contain("먼저")); Assert.That(_app.Session.Board.CollectedBallIds, Is.Empty); + Drag(point, point + new Vector2(70, 20)); yield return null; Physics.SyncTransforms(); point = BallPoint(); + QueueTouch(102, UnityEngine.InputSystem.TouchPhase.Began, point); QueueTouch(102, UnityEngine.InputSystem.TouchPhase.Ended, point); + yield return null; yield return null; + Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay)); Assert.That(_store.Saved.completedStageIds, Is.EqualTo(new[] { 1 })); + Assert.That(_app.Voice.PendingCount, Is.EqualTo(1)); + } + + [UnityTest] public IEnumerator SameUpdateFastDragMovesWithoutCountingAnAlreadyCollectedFriend() + { + _touchscreen = InputSystem.AddDevice(); _app.StartStage(1); yield return null; Physics.SyncTransforms(); + var point = BallPoint(); Drag(point, point + new Vector2(50, 15)); yield return null; Physics.SyncTransforms(); point = BallPoint(); + var original = _app.Session.Board.Balls.Single().position; + QueueTouch(111, UnityEngine.InputSystem.TouchPhase.Began, point); QueueTouch(111, UnityEngine.InputSystem.TouchPhase.Moved, point + new Vector2(40, 15)); + QueueTouch(111, UnityEngine.InputSystem.TouchPhase.Ended, point + new Vector2(40, 15)); yield return null; yield return null; + Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.Puzzle)); Assert.That(_app.Session.Board.CountedCount, Is.Zero); + Assert.That(_app.Session.Board.Balls.Single().position, Is.Not.EqualTo(original)); + } + + [UnityTest] public IEnumerator SameUpdateCanceledAndOutsideDragsPreserveCommittedState() + { + _touchscreen = InputSystem.AddDevice(); _app.StartStage(1); yield return null; Physics.SyncTransforms(); _app.Session.SaveCurrent(); + var point = BallPoint(); var original = _app.Session.Board.Balls.Single().position; var saved = JsonUtility.ToJson(_store.Saved); + foreach (var phase in new[] { UnityEngine.InputSystem.TouchPhase.Canceled, UnityEngine.InputSystem.TouchPhase.Ended }) + { + QueueTouch(121, UnityEngine.InputSystem.TouchPhase.Began, point); QueueTouch(121, UnityEngine.InputSystem.TouchPhase.Moved, point + new Vector2(50, 10)); + QueueTouch(121, phase, phase == UnityEngine.InputSystem.TouchPhase.Ended ? new Vector2(-1, point.y) : point + new Vector2(50, 10)); + yield return null; yield return null; + Assert.That(_app.Session.Board.Balls.Single().position, Is.EqualTo(original)); Assert.That(_app.Session.Board.CollectedBallIds, Is.Empty); + Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved)); + } + } + + [UnityTest] public IEnumerator SameUpdateTouchBeginningOnUiNeverCountsTheFriendAtRelease() + { + _touchscreen = InputSystem.AddDevice(); _app.StartStage(1); yield return null; Physics.SyncTransforms(); + var point = BallPoint(); Drag(point, point + new Vector2(50, 15)); yield return null; Physics.SyncTransforms(); point = BallPoint(); + var button = _app.transform.Find("Interface/SafeArea/Play/HomeButton").GetComponent(); + Vector2 uiPoint = button.TransformPoint(button.rect.center); + QueueTouch(131, UnityEngine.InputSystem.TouchPhase.Began, uiPoint); QueueTouch(131, UnityEngine.InputSystem.TouchPhase.Ended, point); + yield return null; yield return null; + Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.Puzzle)); Assert.That(_app.Session.Board.CountedCount, Is.Zero); + } + + [UnityTest] public IEnumerator SameUpdateSequentialShortTapsCompleteOnlyOnce() + { + _touchscreen = InputSystem.AddDevice(); _app.StartStage(1); yield return null; Physics.SyncTransforms(); + var point = BallPoint(); Drag(point, point + new Vector2(50, 15)); yield return null; Physics.SyncTransforms(); point = BallPoint(); + QueueTouch(141, UnityEngine.InputSystem.TouchPhase.Began, point); QueueTouch(141, UnityEngine.InputSystem.TouchPhase.Ended, point); + QueueTouch(142, UnityEngine.InputSystem.TouchPhase.Began, point); QueueTouch(142, UnityEngine.InputSystem.TouchPhase.Ended, point); + yield return null; yield return null; + Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.FreePlay)); Assert.That(_store.Saved.completedStageIds, Is.EqualTo(new[] { 1 })); + Assert.That(_app.Voice.PendingCount, Is.EqualTo(1)); + } + + [UnityTest] public IEnumerator SameUpdateConcurrentShortPinchNeverCountsTheFriend() + { + _touchscreen = InputSystem.AddDevice(); _app.StartStage(1); yield return null; Physics.SyncTransforms(); + var point = BallPoint(); Drag(point, point + new Vector2(50, 15)); yield return null; Physics.SyncTransforms(); point = BallPoint(); + QueueTouch(151, UnityEngine.InputSystem.TouchPhase.Began, point); QueueTouch(152, UnityEngine.InputSystem.TouchPhase.Began, point + new Vector2(80, 0)); + QueueTouch(152, UnityEngine.InputSystem.TouchPhase.Moved, point + new Vector2(160, 0)); QueueTouch(151, UnityEngine.InputSystem.TouchPhase.Ended, point); + QueueTouch(152, UnityEngine.InputSystem.TouchPhase.Ended, point + new Vector2(160, 0)); yield return null; yield return null; + Assert.That(_app.Session.Screen, Is.EqualTo(SessionScreen.Puzzle)); Assert.That(_app.Session.Board.CountedCount, Is.Zero); + Assert.That(_app.Session.Camera.distance, Is.LessThan(8)); + } private void DragTo(Vector3 destination) { Physics.SyncTransforms(); var start = BallPoint(); diff --git a/Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs b/Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs index 7db4790..fb2b8da 100644 --- a/Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs +++ b/Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs @@ -5,6 +5,7 @@ using MagnetismHeroes.UnityRuntime.Presentation; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.InputSystem; +using UnityEngine.InputSystem.EnhancedTouch; namespace MagnetismHeroes.UnityRuntime.Interaction { @@ -25,6 +26,17 @@ namespace MagnetismHeroes.UnityRuntime.Interaction private int _fingerId = -1; private bool _waitForAllTouchesReleased; private int _pinchFirstId = -1, _pinchSecondId = -1; + private readonly struct TouchContact + { + public readonly int Id; + public readonly UnityEngine.InputSystem.TouchPhase Phase; + public readonly Vector2 Position; + public TouchContact(int id, UnityEngine.InputSystem.TouchPhase phase, Vector2 position) + { Id = id; Phase = phase; Position = position; } + public bool InProgress => Phase == UnityEngine.InputSystem.TouchPhase.Began || Phase == UnityEngine.InputSystem.TouchPhase.Moved || Phase == UnityEngine.InputSystem.TouchPhase.Stationary; + } + private readonly Queue _touchEvents = new Queue(); + private readonly Dictionary _touches = new Dictionary(); public bool AcceptInput => _app.Session != null && _app.Session.Board != null; public void Begin(Vector2 point, bool uiBlocked) @@ -102,77 +114,99 @@ namespace MagnetismHeroes.UnityRuntime.Interaction private static bool IsInside(Vector2 point) => point.x >= 0 && point.x < Screen.width && point.y >= 0 && point.y < Screen.height; - private static UnityEngine.InputSystem.Controls.TouchControl FindTouch(Touchscreen screen, int id) + private TouchContact? FindTouch(int id) => _touches.TryGetValue(id, out var touch) ? touch : (TouchContact?)null; + + private void FinishSingleTouch() { - foreach (var touch in screen.touches) if (touch.touchId.ReadValue() == id) return touch; - return null; + var touch = FindTouch(_fingerId); + if (!touch.HasValue || touch.Value.Phase != UnityEngine.InputSystem.TouchPhase.Ended) { Cancel(); return; } + var point = touch.Value.Position; Move(point); End(point); } - private void FinishSingleTouch(Touchscreen screen) + private void RecordTouch(Finger finger) { - var touch = FindTouch(screen, _fingerId); - if (touch == null || touch.phase.ReadValue() != UnityEngine.InputSystem.TouchPhase.Ended) { Cancel(); return; } - var point = touch.position.ReadValue(); Move(point); End(point); + if (!AcceptInput || finger.screen != Touchscreen.current) return; + // Copy each official history callback before later events overwrite the native touch slot. + var touch = finger.lastTouch; + _touchEvents.Enqueue(new TouchContact(touch.touchId, touch.phase, touch.screenPosition)); + } + + private void ProcessTouches() + { + var active = new List(); + foreach (var touch in _touches.Values) if (touch.InProgress) active.Add(touch); + if (_fingerId != -1 && !active.Exists(touch => touch.Id == _fingerId)) + { + // Finish the original pointer before any new pointer can inherit its preview. + FinishSingleTouch(); _waitForAllTouchesReleased = active.Count > 0; return; + } + if (_gesture == Gesture.Pinch) + { + Vector2 release = _last; bool ended = false, canceled = false; + foreach (int id in new[] { _pinchFirstId, _pinchSecondId }) + { + var touch = FindTouch(id); + if (!touch.HasValue) { canceled = true; continue; } + var phase = touch.Value.Phase; + if (phase == UnityEngine.InputSystem.TouchPhase.Canceled) canceled = true; + if (phase == UnityEngine.InputSystem.TouchPhase.Ended) + { release = touch.Value.Position; ended = true; canceled |= !IsInside(release); } + } + if (canceled || ended) + { End(release, canceled); _waitForAllTouchesReleased = active.Count > 0; return; } + } + // A canceled/finished pinch cannot restart when a third finger joins a remaining finger. + if (_waitForAllTouchesReleased && _gesture == Gesture.None && active.Count > 0) return; + if (active.Count >= 2) + { + var a = active[0].Position; var b = active[1].Position; + if (_gesture != Gesture.Pinch && _gesture != Gesture.Blocked) + { + BeginPinch(Vector2.Distance(a, b), IsUI(a) || IsUI(b)); + _pinchFirstId = active[0].Id; _pinchSecondId = active[1].Id; + } + else Pinch(Vector2.Distance(a, b)); + _waitForAllTouchesReleased = true; + return; + } + if (_waitForAllTouchesReleased) + { + if (active.Count == 0) { _waitForAllTouchesReleased = false; _gesture = Gesture.None; _fingerId = -1; } + return; + } + if (_gesture == Gesture.Pinch || (active.Count == 0 && _gesture == Gesture.Blocked)) + { End(_last); _fingerId = -1; return; } + if (active.Count == 1) + { + var touch = active[0]; var point = touch.Position; + if (_fingerId == -1) + { + if (touch.Phase != UnityEngine.InputSystem.TouchPhase.Began) { _waitForAllTouchesReleased = true; return; } + Begin(point, IsUI(point)); _fingerId = touch.Id; + } + else Move(point); + return; + } } private void Update() { - if (!AcceptInput) return; - var touchscreen = Touchscreen.current; - if (touchscreen != null) + if (!AcceptInput) { _touchEvents.Clear(); _touches.Clear(); return; } + bool hadTouches = _touchEvents.Count > 0, suppressRestOfFrame = false; + while (_touchEvents.Count > 0) { - var active = new List(); - foreach (var touch in touchscreen.touches) if (touch.press.isPressed) active.Add(touch); - if (_fingerId != -1 && !active.Exists(touch => touch.touchId.ReadValue() == _fingerId)) + var touch = _touchEvents.Dequeue(); _touches[touch.Id] = touch; + if (!suppressRestOfFrame) { - // Finish the original pointer before any new pointer can inherit its preview. - FinishSingleTouch(touchscreen); _waitForAllTouchesReleased = active.Count > 0; return; - } - if (_gesture == Gesture.Pinch) - { - Vector2 release = _last; bool ended = false, canceled = false; - foreach (int id in new[] { _pinchFirstId, _pinchSecondId }) - { - var touch = FindTouch(touchscreen, id); - if (touch == null) { canceled = true; continue; } - var phase = touch.phase.ReadValue(); - if (phase == UnityEngine.InputSystem.TouchPhase.Canceled) canceled = true; - if (phase == UnityEngine.InputSystem.TouchPhase.Ended) - { release = touch.position.ReadValue(); ended = true; canceled |= !IsInside(release); } - } - if (canceled || ended) - { End(release, canceled); _waitForAllTouchesReleased = active.Count > 0; return; } - } - // A canceled/finished pinch cannot restart when a third finger joins a remaining finger. - if (_waitForAllTouchesReleased && _gesture == Gesture.None && active.Count > 0) return; - if (active.Count >= 2) - { - var a = active[0].position.ReadValue(); var b = active[1].position.ReadValue(); - if (_gesture != Gesture.Pinch && _gesture != Gesture.Blocked) - { - BeginPinch(Vector2.Distance(a, b), IsUI(a) || IsUI(b)); - _pinchFirstId = active[0].touchId.ReadValue(); _pinchSecondId = active[1].touchId.ReadValue(); - } - else Pinch(Vector2.Distance(a, b)); - _waitForAllTouchesReleased = true; - return; - } - if (_waitForAllTouchesReleased) - { - if (active.Count == 0) { _waitForAllTouchesReleased = false; _gesture = Gesture.None; _fingerId = -1; } - return; - } - if (_gesture == Gesture.Pinch || (active.Count == 0 && _gesture == Gesture.Blocked)) - { End(_last); _fingerId = -1; return; } - if (active.Count == 1) - { - var touch = active[0]; var point = touch.position.ReadValue(); - if (_fingerId == -1) - { Begin(point, IsUI(point)); _fingerId = touch.touchId.ReadValue(); } - else Move(point); - return; + bool ownsGesture = touch.Id == _fingerId || (_gesture == Gesture.Pinch && (touch.Id == _pinchFirstId || touch.Id == _pinchSecondId)); + bool canceled = touch.Phase == UnityEngine.InputSystem.TouchPhase.Canceled || + (touch.Phase == UnityEngine.InputSystem.TouchPhase.Ended && !IsInside(touch.Position)); + ProcessTouches(); suppressRestOfFrame = ownsGesture && canceled; } + if (!touch.InProgress) _touches.Remove(touch.Id); } + if (suppressRestOfFrame) _waitForAllTouchesReleased = _touches.Count > 0; + if (hadTouches || _touches.Count > 0) return; var mouse = Mouse.current; if (mouse == null) return; var position = mouse.position.ReadValue(); @@ -182,7 +216,20 @@ namespace MagnetismHeroes.UnityRuntime.Interaction if (mouse.scroll.ReadValue().y != 0 && !IsUI(position)) { _orbit.Zoom(Mathf.Exp(mouse.scroll.ReadValue().y * 0.001f)); _app.Session.CommitCamera(_orbit.Snapshot()); } } - private void OnApplicationPause(bool paused) { if (paused) Cancel(); } - private void OnDisable() => Cancel(); + private void OnEnable() + { + EnhancedTouchSupport.Enable(); + UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown += RecordTouch; + UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove += RecordTouch; + UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp += RecordTouch; + } + private void OnApplicationPause(bool paused) { if (paused) { Cancel(); _touchEvents.Clear(); _touches.Clear(); } } + private void OnDisable() + { + UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown -= RecordTouch; + UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove -= RecordTouch; + UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp -= RecordTouch; + EnhancedTouchSupport.Disable(); Cancel(); _touchEvents.Clear(); _touches.Clear(); + } } }