Preserve the first touch when the active input device changes

This commit is contained in:
NINI
2026-10-10 07:30:25 +09:00
parent 4184023769
commit 72638ab091
2 changed files with 129 additions and 10 deletions
@@ -31,9 +31,15 @@ namespace MagnetismHeroes.Tests
Saved = Copy(save); return true; Saved = Copy(save); return true;
} }
} }
private GameApp _app; private MemoryStore _store; private Touchscreen _touchscreen; private GameApp _app; private MemoryStore _store; private Touchscreen _touchscreen, _otherTouchscreen;
private InputSettings _originalInputSettings, _testInputSettings; private InputSettings _originalInputSettings, _testInputSettings;
private HideFlags _originalSettingsFlags; private HideFlags _originalSettingsFlags;
private readonly System.Collections.Generic.List<string> _touchTrace = new System.Collections.Generic.List<string>();
private void TraceTouch(UnityEngine.InputSystem.EnhancedTouch.Finger finger)
{
var touch = finger.lastTouch;
_touchTrace.Add($"{Time.frameCount},{finger.screen.deviceId},{Touchscreen.current?.deviceId},{touch.touchId},{touch.phase},{touch.screenPosition.x:R},{touch.screenPosition.y:R}");
}
[UnitySetUp] public IEnumerator SetUp() [UnitySetUp] public IEnumerator SetUp()
{ {
// Batch tests have no focused Game View; route synthetic pointers into the actual player loop. // Batch tests have no focused Game View; route synthetic pointers into the actual player loop.
@@ -44,6 +50,11 @@ namespace MagnetismHeroes.Tests
_testInputSettings.editorInputBehaviorInPlayMode = InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView; _testInputSettings.editorInputBehaviorInPlayMode = InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView;
_testInputSettings.backgroundBehavior = InputSettings.BackgroundBehavior.IgnoreFocus; _testInputSettings.backgroundBehavior = InputSettings.BackgroundBehavior.IgnoreFocus;
InputSystem.settings = _testInputSettings; InputSystem.settings = _testInputSettings;
UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.Enable();
_touchTrace.Clear(); _touchTrace.Add("frame,sourceDevice,currentDevice,touchId,phase,pointerX,pointerY");
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown += TraceTouch;
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove += TraceTouch;
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp += TraceTouch;
foreach (var listener in Object.FindObjectsByType<AudioListener>(FindObjectsSortMode.None)) listener.enabled = false; foreach (var listener in Object.FindObjectsByType<AudioListener>(FindObjectsSortMode.None)) listener.enabled = false;
foreach (var camera in Object.FindObjectsByType<Camera>(FindObjectsSortMode.None)) camera.enabled = false; foreach (var camera in Object.FindObjectsByType<Camera>(FindObjectsSortMode.None)) camera.enabled = false;
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab"); var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab");
@@ -52,7 +63,18 @@ namespace MagnetismHeroes.Tests
} }
[UnityTearDown] public IEnumerator TearDown() [UnityTearDown] public IEnumerator TearDown()
{ {
if (_touchscreen != null) InputSystem.RemoveDevice(_touchscreen); Object.Destroy(_app.gameObject); UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown -= TraceTouch;
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove -= TraceTouch;
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp -= TraceTouch;
UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.Disable();
var evidence = System.Environment.GetEnvironmentVariable("MAGNETISM_TOUCH_EVIDENCE");
if (!string.IsNullOrEmpty(evidence) && _touchTrace.Count > 1)
{
System.IO.Directory.CreateDirectory(evidence);
System.IO.File.WriteAllLines(System.IO.Path.Combine(evidence, TestContext.CurrentContext.Test.Name + ".csv"), _touchTrace);
}
if (_touchscreen != null) InputSystem.RemoveDevice(_touchscreen);
if (_otherTouchscreen != null) InputSystem.RemoveDevice(_otherTouchscreen); Object.Destroy(_app.gameObject);
InputSystem.settings = _originalInputSettings; _originalInputSettings.hideFlags = _originalSettingsFlags; InputSystem.settings = _originalInputSettings; _originalInputSettings.hideFlags = _originalSettingsFlags;
Object.Destroy(_testInputSettings); yield return null; Object.Destroy(_testInputSettings); yield return null;
} }
@@ -71,6 +93,91 @@ namespace MagnetismHeroes.Tests
private void QueueTouch(int id, UnityEngine.InputSystem.TouchPhase phase, Vector2 position) => private void QueueTouch(int id, UnityEngine.InputSystem.TouchPhase phase, Vector2 position) =>
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = id, phase = phase, position = position }); InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = id, phase = phase, position = position });
[UnityTest] public IEnumerator FirstAndSecondDragAfterAnotherTouchscreenBothUseTheActualPointerDelta()
{
_touchscreen = InputSystem.AddDevice<Touchscreen>(); _otherTouchscreen = InputSystem.AddDevice<Touchscreen>();
_app.StartStage(1); yield return null; Physics.SyncTransforms();
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 201, phase = UnityEngine.InputSystem.TouchPhase.Began, position = new Vector2(5, 5) });
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 201, phase = UnityEngine.InputSystem.TouchPhase.Ended, position = new Vector2(5, 5) });
yield return null; yield return null;
Assert.That(Touchscreen.current, Is.SameAs(_otherTouchscreen));
var camera = JsonUtility.ToJson(_app.Session.Camera); var id = _app.Session.Board.Balls.Single().id;
for (int attempt = 0; attempt < 2; attempt++)
{
Physics.SyncTransforms(); var start = BallPoint(); var end = start + new Vector2(-40, 0);
var before = BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position);
var ray = _app.Presenter.Camera.ScreenPointToRay(start); Physics.Raycast(ray, out var hit, 100);
var plane = new Plane(_app.Presenter.Camera.transform.forward, hit.point);
Assert.That(plane.Raycast(ray, out var from), Is.True);
var endRay = _app.Presenter.Camera.ScreenPointToRay(end); Assert.That(plane.Raycast(endRay, out var to), Is.True);
var expected = before + endRay.GetPoint(to) - ray.GetPoint(from);
QueueTouch(202 + attempt, UnityEngine.InputSystem.TouchPhase.Began, start);
QueueTouch(202 + attempt, UnityEngine.InputSystem.TouchPhase.Moved, end);
QueueTouch(202 + attempt, UnityEngine.InputSystem.TouchPhase.Ended, end);
yield return null; yield return null;
Assert.That(_app.Session.Board.Balls.Single().id, Is.EqualTo(id));
Assert.That(Vector3.Distance(BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position), expected), Is.LessThan(.001f), "Both the first and second real queued drag must retain Began and the pointer direction.");
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(camera)); Assert.That(_app.Session.Board.CountedCount, Is.Zero);
yield return new WaitForSecondsRealtime(1.5f);
}
}
[UnityTest] public IEnumerator SwitchedTouchscreenBoundaryDragAndHeldReleaseCommitTheSameLastSafePosition()
{
_touchscreen = InputSystem.AddDevice<Touchscreen>(); _otherTouchscreen = InputSystem.AddDevice<Touchscreen>();
_store.Saved.completedStageIds = new[] { 1 };
for (int attempt = 0; attempt < 2; attempt++)
{
_store.Saved.latestFreePlay = new FreePlayState { sourceStageId = 1,
balls = new[] { new BallState { id = "native-drag-0", position = new Position3(-6.8f, 4, 0) } },
groups = new[] { new GroupState { id = "native-drag-group", ballIds = new[] { "native-drag-0" } } },
camera = new CameraState { target = new Position3(-6.8f, 3.35f, 0), rotation = new Rotation4(0, 0, 0, 1), distance = 14 } };
_app.Initialize(_store); _app.ContinueFreePlay(); yield return null; Physics.SyncTransforms();
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 1, phase = UnityEngine.InputSystem.TouchPhase.Began, position = new Vector2(5, 5) });
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 1, phase = UnityEngine.InputSystem.TouchPhase.Ended, position = new Vector2(5, 5) });
yield return null; yield return null;
var saved = JsonUtility.ToJson(_store.Saved); var camera = JsonUtility.ToJson(_app.Session.Camera);
var start = BallPoint(); var end = start - new Vector2(Screen.width * 193f / 2400f, 0);
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Began, start);
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Moved, end);
yield return null; yield return null;
var preview = _app.Presenter.Balls.Single().Value.transform.position;
Assert.That(preview.x, Is.EqualTo(-8).Within(.004f)); Assert.That(preview.y, Is.EqualTo(4).Within(.004f)); Assert.That(preview.z, Is.EqualTo(0).Within(.004f));
yield return new WaitForSecondsRealtime(.7f);
Assert.That(_app.Presenter.Balls.Single().Value.transform.position, Is.EqualTo(preview));
Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved), "Camera assistance and a held pointer cannot save a preview.");
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Ended, end); yield return null; yield return null;
Assert.That(_app.Session.Board.Balls.Single().id, Is.EqualTo("native-drag-0"));
Assert.That(_app.Session.Board.Groups.Single().ballIds, Is.EqualTo(new[] { "native-drag-0" }));
Assert.That(Vector3.Distance(BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position), preview), Is.LessThan(.001f));
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(camera));
Assert.That(_store.Saved.completedStageIds, Is.EqualTo(new[] { 1 })); Assert.That(_store.Saved.puzzleStates, Is.Empty);
yield return new WaitForSecondsRealtime(1.5f);
}
}
[UnityTest] public IEnumerator AnotherTouchscreenWithTheSameIdCannotMovePinchOrEndTheHeldFriend()
{
_touchscreen = InputSystem.AddDevice<Touchscreen>(); _otherTouchscreen = InputSystem.AddDevice<Touchscreen>();
_app.StartStage(1); yield return null; Physics.SyncTransforms();
var start = BallPoint(); var saved = JsonUtility.ToJson(_store.Saved); var camera = JsonUtility.ToJson(_app.Session.Camera);
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Began, start); yield return null; yield return null;
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Moved, start + new Vector2(-40, 0)); yield return null; yield return null;
var preview = _app.Presenter.Balls.Values.Single().transform.position;
foreach (var phase in new[] { UnityEngine.InputSystem.TouchPhase.Began, UnityEngine.InputSystem.TouchPhase.Moved, UnityEngine.InputSystem.TouchPhase.Ended })
{
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 1, phase = phase, position = start + new Vector2(100, 30) });
InputSystem.QueueStateEvent(_otherTouchscreen, new TouchState { touchId = 2, phase = phase, position = start + new Vector2(200, 30) });
yield return null; yield return null;
Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(preview));
Assert.That(_app.Orbit.IsAssisting, Is.True); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved));
}
QueueTouch(1, UnityEngine.InputSystem.TouchPhase.Canceled, start + new Vector2(-40, 0)); yield return null; yield return null;
Assert.That(_app.Session.Board.CollectedBallIds, Is.Empty); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved));
Assert.That(JsonUtility.ToJson(_app.Session.Camera), Is.EqualTo(camera));
Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(BoardPresenter.ToVector(_app.Session.Board.Balls.Single().position)));
}
[UnityTest] public IEnumerator FailedMoveCameraAndPauseSavesShowAVisibleNoticeAndRecoveryClearsIt() [UnityTest] public IEnumerator FailedMoveCameraAndPauseSavesShowAVisibleNoticeAndRecoveryClearsIt()
{ {
_app.StartStage(1); yield return null; _app.StartStage(1); yield return null;
@@ -33,13 +33,15 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
private int _fingerId = -1; private int _fingerId = -1;
private bool _waitForAllTouchesReleased; private bool _waitForAllTouchesReleased;
private int _pinchFirstId = -1, _pinchSecondId = -1; private int _pinchFirstId = -1, _pinchSecondId = -1;
private Touchscreen _touchscreen;
private readonly struct TouchContact private readonly struct TouchContact
{ {
public readonly Touchscreen Screen;
public readonly int Id; public readonly int Id;
public readonly UnityEngine.InputSystem.TouchPhase Phase; public readonly UnityEngine.InputSystem.TouchPhase Phase;
public readonly Vector2 Position; public readonly Vector2 Position;
public TouchContact(int id, UnityEngine.InputSystem.TouchPhase phase, Vector2 position) public TouchContact(Touchscreen screen, int id, UnityEngine.InputSystem.TouchPhase phase, Vector2 position)
{ Id = id; Phase = phase; Position = position; } { Screen = screen; Id = id; Phase = phase; Position = position; }
public bool InProgress => Phase == UnityEngine.InputSystem.TouchPhase.Began || Phase == UnityEngine.InputSystem.TouchPhase.Moved || Phase == UnityEngine.InputSystem.TouchPhase.Stationary; 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 Queue<TouchContact> _touchEvents = new Queue<TouchContact>();
@@ -186,10 +188,11 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
private void RecordTouch(Finger finger) private void RecordTouch(Finger finger)
{ {
if (!AcceptInput || finger.screen != Touchscreen.current) return; if (!AcceptInput) return;
// Copy each official history callback before later events overwrite the native touch slot. // Copy each official history callback before later events overwrite the native touch slot.
// Input System makes this device current AFTER its EnhancedTouch callbacks.
var touch = finger.lastTouch; var touch = finger.lastTouch;
_touchEvents.Enqueue(new TouchContact(touch.touchId, touch.phase, touch.screenPosition)); _touchEvents.Enqueue(new TouchContact(finger.screen, touch.touchId, touch.phase, touch.screenPosition));
} }
private void ProcessTouches() private void ProcessTouches()
@@ -252,11 +255,19 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
private void Update() private void Update()
{ {
if (!AcceptInput) { _touchEvents.Clear(); _touches.Clear(); return; } if (!AcceptInput) { _touchEvents.Clear(); _touches.Clear(); _touchscreen = null; return; }
bool hadTouches = _touchEvents.Count > 0, suppressRestOfFrame = false; bool hadTouches = _touchEvents.Count > 0, suppressRestOfFrame = false;
while (_touchEvents.Count > 0) while (_touchEvents.Count > 0)
{ {
var touch = _touchEvents.Dequeue(); _touches[touch.Id] = touch; var touch = _touchEvents.Dequeue();
if (_touchscreen == null)
{
if (touch.Phase != UnityEngine.InputSystem.TouchPhase.Began) continue;
_touchscreen = touch.Screen;
}
// Touch IDs are local to a device; another device cannot replace a held finger.
if (touch.Screen != _touchscreen) continue;
_touches[touch.Id] = touch;
if (!suppressRestOfFrame) if (!suppressRestOfFrame)
{ {
bool ownsGesture = touch.Id == _fingerId || (_gesture == Gesture.Pinch && (touch.Id == _pinchFirstId || touch.Id == _pinchSecondId)); bool ownsGesture = touch.Id == _fingerId || (_gesture == Gesture.Pinch && (touch.Id == _pinchFirstId || touch.Id == _pinchSecondId));
@@ -265,6 +276,7 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
ProcessTouches(); suppressRestOfFrame = ownsGesture && canceled; ProcessTouches(); suppressRestOfFrame = ownsGesture && canceled;
} }
if (!touch.InProgress) _touches.Remove(touch.Id); if (!touch.InProgress) _touches.Remove(touch.Id);
if (_touches.Count == 0) _touchscreen = null;
} }
if (suppressRestOfFrame) _waitForAllTouchesReleased = _touches.Count > 0; if (suppressRestOfFrame) _waitForAllTouchesReleased = _touches.Count > 0;
if (hadTouches || _touches.Count > 0) return; if (hadTouches || _touches.Count > 0) return;
@@ -284,13 +296,13 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove += RecordTouch; UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove += RecordTouch;
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp += RecordTouch; UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp += RecordTouch;
} }
private void OnApplicationPause(bool paused) { if (paused) { Cancel(); _touchEvents.Clear(); _touches.Clear(); } } private void OnApplicationPause(bool paused) { if (paused) { Cancel(); _touchEvents.Clear(); _touches.Clear(); _touchscreen = null; } }
private void OnDisable() private void OnDisable()
{ {
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown -= RecordTouch; UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown -= RecordTouch;
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove -= RecordTouch; UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerMove -= RecordTouch;
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp -= RecordTouch; UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerUp -= RecordTouch;
EnhancedTouchSupport.Disable(); Cancel(); _touchEvents.Clear(); _touches.Clear(); EnhancedTouchSupport.Disable(); Cancel(); _touchEvents.Clear(); _touches.Clear(); _touchscreen = null;
} }
} }
} }