Reject background penetration and preserve canceled touch ownership

This commit is contained in:
NINI
2026-10-05 02:19:34 +09:00
parent e65f970e9a
commit b2eded808e
3 changed files with 114 additions and 23 deletions
@@ -58,6 +58,59 @@ 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 DragTo(Vector3 destination)
{
Physics.SyncTransforms(); var start = BallPoint();
var camera = _app.Presenter.Camera; Physics.Raycast(camera.ScreenPointToRay(start), out var hit, 100);
var offset = hit.point - _app.Presenter.Balls.Values.Single().transform.position;
Drag(start, camera.WorldToScreenPoint(destination + offset));
}
[UnityTest] public IEnumerator PinchReleaseOutsideRestoresCameraAndSuppressesRemainingTouch()
{
_touchscreen = InputSystem.AddDevice<Touchscreen>(); _app.StartStage(1); yield return null; Physics.SyncTransforms();
var point = BallPoint(); var second = point + new Vector2(90, 0); var initial = _app.Session.Camera.distance;
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 71, phase = UnityEngine.InputSystem.TouchPhase.Began, position = point });
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 72, phase = UnityEngine.InputSystem.TouchPhase.Began, position = second }); yield return null; yield return null;
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 72, phase = UnityEngine.InputSystem.TouchPhase.Moved, position = second + new Vector2(80, 0) }); yield return null; yield return null;
Assert.That(_app.Orbit.Snapshot().distance, Is.LessThan(initial)); var saved = JsonUtility.ToJson(_store.Saved);
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 72, phase = UnityEngine.InputSystem.TouchPhase.Ended, position = new Vector2(-1, second.y) }); yield return null; yield return null;
Assert.That(_app.Orbit.Snapshot().distance, Is.EqualTo(initial)); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved));
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 71, phase = UnityEngine.InputSystem.TouchPhase.Moved, position = point + new Vector2(70, 20) }); yield return null; yield return null;
Assert.That(_app.Orbit.Snapshot().rotation, Is.EqualTo(_app.Session.Camera.rotation));
Assert.That(_app.Session.Board.CollectedBallIds, Is.Empty);
}
[UnityTest] public IEnumerator CanceledDragCannotBeInheritedByANewFingerInTheSameFrame()
{
_touchscreen = InputSystem.AddDevice<Touchscreen>(); _app.StartStage(1); yield return null; Physics.SyncTransforms();
var point = BallPoint(); var initial = _app.Session.Board.Balls.Single().position;
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 81, phase = UnityEngine.InputSystem.TouchPhase.Began, position = point }); yield return null; yield return null;
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 81, phase = UnityEngine.InputSystem.TouchPhase.Moved, position = point + new Vector2(70, 20) }); yield return null; yield return null;
var saved = JsonUtility.ToJson(_store.Saved);
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 81, phase = UnityEngine.InputSystem.TouchPhase.Canceled, position = point + new Vector2(70, 20) });
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 82, phase = UnityEngine.InputSystem.TouchPhase.Began, position = point + new Vector2(140, 30) }); yield return null; yield return null;
Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(new Vector3(initial.x, initial.y, initial.z)));
InputSystem.QueueStateEvent(_touchscreen, new TouchState { touchId = 82, phase = UnityEngine.InputSystem.TouchPhase.Ended, position = point + new Vector2(140, 30) }); yield return null; yield return null;
Assert.That(_app.Session.Board.Balls.Single().position, Is.EqualTo(initial)); Assert.That(_app.Session.Board.CollectedBallIds, Is.Empty);
Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved));
}
[UnityTest] public IEnumerator BackgroundInteriorDropIsRejectedAndSurfaceContactRemainsPlayable()
{
_app.StartStage(1); yield return null; DragTo(new Vector3(1.8f, 0.8f, 0)); yield return null;
Assert.That(_app.Session.Board.CollectedBallIds.Length, Is.EqualTo(1));
_app.Orbit.Orbit(new Vector2(360, 0)); _app.Session.CommitCamera(_app.Orbit.Snapshot());
DragTo(new Vector3(1.8f, 0.65f, 1.1f)); yield return null; Physics.SyncTransforms();
var initial = _app.Session.Board.Balls.Single().position;
Assert.That(initial.y, Is.EqualTo(0.65f).Within(0.001)); Assert.That(initial.z, Is.EqualTo(1.1f).Within(0.001)); BallPoint();
var saved = JsonUtility.ToJson(_store.Saved); var camera = _app.Orbit.Snapshot();
DragTo(new Vector3(1.8f, -0.4f, 1.1f)); yield return null;
Assert.That(_app.Session.Board.Balls.Single().position, Is.EqualTo(initial), "The scene interior must not become a committed drop.");
Assert.That(_app.Presenter.Balls.Values.Single().transform.position, Is.EqualTo(new Vector3(initial.x, initial.y, initial.z)));
Assert.That(_app.Orbit.Snapshot().rotation, Is.EqualTo(camera.rotation)); Assert.That(JsonUtility.ToJson(_store.Saved), Is.EqualTo(saved));
Physics.SyncTransforms(); BallPoint();
}
[UnityTest] public IEnumerator InspectorButtonsNavigateAndHomeKeepsACameraAndListener()
{
@@ -59,11 +59,14 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
public void End(Vector2 point, bool canceled = false)
{
bool inside = point.x >= 0 && point.x < Screen.width && point.y >= 0 && point.y < Screen.height;
if (canceled || !inside) { Cancel(); return; }
if (canceled || !IsInside(point)) { Cancel(); return; }
if (_gesture == Gesture.Friend)
{
if (_dragged) _app.CommitMove(_groupId, BoardPresenter.ToPosition(_previewDelta));
if (_dragged)
{
if (!_presenter.CanDropGroup(_groupId)) { Cancel(); return; }
_app.CommitMove(_groupId, BoardPresenter.ToPosition(_previewDelta));
}
else _app.CountBall(_ballId);
}
else if (_gesture == Gesture.Orbit && _dragged) _app.Session.CommitCamera(_orbit.Snapshot());
@@ -97,23 +100,49 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
return hits.Count > 0;
}
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)
{
foreach (var touch in screen.touches) if (touch.touchId.ReadValue() == id) return touch;
return null;
}
private void FinishSingleTouch(Touchscreen screen)
{
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);
}
private void Update()
{
if (!AcceptInput) return;
var touchscreen = Touchscreen.current;
if (touchscreen != null)
{
if (_gesture == Gesture.Pinch)
{
foreach (var touch in touchscreen.touches)
{
var id = touch.touchId.ReadValue();
if ((id == _pinchFirstId || id == _pinchSecondId) && touch.phase.ReadValue() == UnityEngine.InputSystem.TouchPhase.Canceled)
{ Cancel(); _waitForAllTouchesReleased = true; return; }
}
}
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))
{
// 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)
@@ -130,7 +159,6 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
}
if (_waitForAllTouchesReleased)
{
if (_gesture == Gesture.Pinch) End(_last);
if (active.Count == 0) { _waitForAllTouchesReleased = false; _gesture = Gesture.None; _fingerId = -1; }
return;
}
@@ -144,16 +172,6 @@ namespace MagnetismHeroes.UnityRuntime.Interaction
else Move(point);
return;
}
if (_fingerId != -1)
{
bool canceled = false;
Vector2 releasePoint = _last;
foreach (var touch in touchscreen.touches)
if (touch.touchId.ReadValue() == _fingerId)
{ canceled = touch.phase.ReadValue() == UnityEngine.InputSystem.TouchPhase.Canceled; releasePoint = touch.position.ReadValue(); break; }
if (!canceled) Move(releasePoint);
End(releasePoint, canceled); return;
}
}
var mouse = Mouse.current;
if (mouse == null) return;
@@ -50,6 +50,26 @@ namespace MagnetismHeroes.UnityRuntime.Presentation
PositionBadges();
}
public bool CanDropGroup(string groupId)
{
Physics.SyncTransforms();
foreach (var ball in _balls.Values.Where(ball => ball.GroupId == groupId))
{
var sphere = ball.GetComponent<SphereCollider>();
var scale = sphere.transform.lossyScale;
float radius = sphere.radius * Mathf.Max(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z));
foreach (var blocker in Physics.OverlapSphere(sphere.transform.TransformPoint(sphere.center), radius, ~0, QueryTriggerInteraction.Ignore))
{
// Board friends and their face/count decorations are not background obstacles.
if (blocker.transform.IsChildOf(transform) || blocker.attachedRigidbody != null) continue;
if (Physics.ComputePenetration(sphere, sphere.transform.position, sphere.transform.rotation,
blocker, blocker.transform.position, blocker.transform.rotation, out _, out float depth) && depth > 0.001f)
return false;
}
}
return true;
}
private void PositionBadges()
{
if (_board == null) return;