Merge commit 'e37b6af9844f74e2eeb46350b48e31177aae48bc' into dryforge/T3

This commit is contained in:
NINI
2026-10-05 03:56:18 +09:00
2 changed files with 181 additions and 61 deletions
@@ -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<Touchscreen>(); _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<Touchscreen>(); _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<Touchscreen>(); _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<Touchscreen>(); _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<RectTransform>();
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<Touchscreen>(); _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<Touchscreen>(); _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();
@@ -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<TouchContact> _touchEvents = new Queue<TouchContact>();
private readonly Dictionary<int, TouchContact> _touches = new Dictionary<int, TouchContact>();
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<TouchContact>();
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<UnityEngine.InputSystem.Controls.TouchControl>();
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();
}
}
}