273 lines
14 KiB
C#
273 lines
14 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using MagnetismHeroes.Core;
|
|
using MagnetismHeroes.UnityRuntime.Bootstrap;
|
|
using MagnetismHeroes.UnityRuntime.Presentation;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.InputSystem;
|
|
using UnityEngine.InputSystem.EnhancedTouch;
|
|
|
|
namespace MagnetismHeroes.UnityRuntime.Interaction
|
|
{
|
|
public sealed class BoardInput : MonoBehaviour
|
|
{
|
|
[SerializeField] private GameApp _app;
|
|
[SerializeField] private BoardPresenter _presenter;
|
|
[SerializeField] private OrbitCamera _orbit;
|
|
[SerializeField] private float _dragThreshold = 14;
|
|
private enum Gesture { None, Blocked, Friend, Orbit, Pinch }
|
|
private Gesture _gesture;
|
|
private Vector2 _start, _last;
|
|
private string _ballId, _groupId;
|
|
private bool _dragged;
|
|
private string _selectedGroup;
|
|
private readonly HashSet<string> _selected = new HashSet<string>();
|
|
private bool _splitting, _splitDrag;
|
|
public bool IsSplitting => _splitting;
|
|
public int SelectedCount => _selected.Count;
|
|
public string SelectedGroup => _selectedGroup;
|
|
public IReadOnlyCollection<string> SelectedIds => _selected;
|
|
private Plane _dragPlane;
|
|
private Vector3 _grabPoint, _previewDelta;
|
|
private float _pinchDistance;
|
|
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 BeginSplit()
|
|
{
|
|
Cancel();
|
|
var group = _app.Session?.Board?.Groups.FirstOrDefault(value => value.id == _selectedGroup);
|
|
if (group == null || group.ballIds.Length < 2) return;
|
|
_splitting = true; _selected.Clear(); _app.RefreshSelection();
|
|
}
|
|
public void CancelSplit()
|
|
{ Cancel(); if (_splitting) _app.Voice.Clear(); _splitting = false; _selected.Clear(); _selectedGroup = null; _presenter.ShowSelection(_selected); _app.RefreshSelection(); }
|
|
|
|
public void Begin(Vector2 point, bool uiBlocked)
|
|
{
|
|
Cancel();
|
|
if (!AcceptInput) return;
|
|
_start = _last = point;
|
|
if (uiBlocked) { _gesture = Gesture.Blocked; return; }
|
|
Physics.SyncTransforms();
|
|
var ray = _orbit.Camera.ScreenPointToRay(point);
|
|
if (Physics.Raycast(ray, out var hit, 100) && hit.collider.TryGetComponent<BallView>(out var ball))
|
|
{
|
|
_gesture = Gesture.Friend; _ballId = ball.BallId; _groupId = ball.GroupId;
|
|
if (_splitting && _selectedGroup != _groupId) { _selected.Clear(); _selectedGroup = _groupId; _app.RefreshSelection(); }
|
|
_splitDrag = _splitting && _selected.Contains(_ballId);
|
|
_dragPlane = new Plane(_orbit.Camera.transform.forward, hit.point);
|
|
_grabPoint = hit.point;
|
|
}
|
|
else _gesture = Gesture.Orbit;
|
|
}
|
|
|
|
public void Move(Vector2 point)
|
|
{
|
|
if (_gesture == Gesture.None || _gesture == Gesture.Blocked || _gesture == Gesture.Pinch) return;
|
|
_dragged |= (point - _start).sqrMagnitude >= _dragThreshold * _dragThreshold;
|
|
if (_dragged && _gesture == Gesture.Friend)
|
|
{
|
|
var ray = _orbit.Camera.ScreenPointToRay(point);
|
|
if (_dragPlane.Raycast(ray, out var distance))
|
|
{
|
|
_previewDelta = ray.GetPoint(distance) - _grabPoint;
|
|
if (_splitting) { if (_splitDrag) _presenter.PreviewSplit(_groupId, _selected.ToArray(), _previewDelta); }
|
|
else _presenter.PreviewGroup(_groupId, _previewDelta);
|
|
}
|
|
}
|
|
else if (_dragged && _gesture == Gesture.Orbit) _orbit.Orbit(point - _last);
|
|
_last = point;
|
|
}
|
|
|
|
public void End(Vector2 point, bool canceled = false)
|
|
{
|
|
if (canceled || !IsInside(point)) { if (_splitting) CancelSplit(); else Cancel(); return; }
|
|
if (_gesture == Gesture.Friend)
|
|
{
|
|
if (_dragged)
|
|
{
|
|
if (!_presenter.CanDropGroup(_groupId)) { if (_splitting) CancelSplit(); else Cancel(); return; }
|
|
if (_splitting)
|
|
{
|
|
if (_splitDrag && _app.CommitSplit(_groupId, _selected.ToArray(), BoardPresenter.ToPosition(_previewDelta)))
|
|
{ _splitting = false; _selected.Clear(); _selectedGroup = null; }
|
|
else _presenter.CancelPreview();
|
|
_app.RefreshSelection();
|
|
}
|
|
else _app.CommitMove(_groupId, BoardPresenter.ToPosition(_presenter.DropDelta), _presenter.MergeCandidate);
|
|
}
|
|
else if (_splitting)
|
|
{
|
|
if (!_selected.Add(_ballId)) _selected.Remove(_ballId);
|
|
_app.RefreshSelection();
|
|
}
|
|
else { _selectedGroup = _groupId; _app.CountBall(_ballId); _app.RefreshSelection(); }
|
|
}
|
|
else if (_gesture == Gesture.Orbit && _dragged) _app.Session.CommitCamera(_orbit.Snapshot());
|
|
else if (_gesture == Gesture.Pinch) _app.Session.CommitCamera(_orbit.Snapshot());
|
|
_gesture = Gesture.None; _dragged = false; _previewDelta = Vector3.zero; _fingerId = -1;
|
|
}
|
|
|
|
public void BeginPinch(float distance, bool uiBlocked)
|
|
{
|
|
if (_splitting) CancelSplit(); else Cancel();
|
|
_gesture = uiBlocked || !AcceptInput ? Gesture.Blocked : Gesture.Pinch;
|
|
_pinchDistance = distance;
|
|
}
|
|
public void Pinch(float distance)
|
|
{
|
|
if (_gesture != Gesture.Pinch || distance <= 0 || _pinchDistance <= 0) return;
|
|
_orbit.Zoom(distance / _pinchDistance); _pinchDistance = distance;
|
|
}
|
|
public void Cancel()
|
|
{
|
|
if (_gesture == Gesture.Friend) _presenter.CancelPreview();
|
|
if ((_gesture == Gesture.Orbit || _gesture == Gesture.Pinch) && _app.Session != null) _orbit.Apply(_app.Session.Camera);
|
|
_gesture = Gesture.None; _dragged = false; _previewDelta = Vector3.zero; _fingerId = -1;
|
|
_waitForAllTouchesReleased = false; _pinchFirstId = _pinchSecondId = -1;
|
|
}
|
|
|
|
private bool IsUI(Vector2 point)
|
|
{
|
|
if (EventSystem.current == null) return false;
|
|
var hits = new List<RaycastResult>(); EventSystem.current.RaycastAll(new PointerEventData(EventSystem.current) { position = point }, hits);
|
|
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 TouchContact? FindTouch(int id) => _touches.TryGetValue(id, out var touch) ? touch : (TouchContact?)null;
|
|
|
|
private void FinishSingleTouch()
|
|
{
|
|
var touch = FindTouch(_fingerId);
|
|
if (!touch.HasValue || touch.Value.Phase != UnityEngine.InputSystem.TouchPhase.Ended) { if (_splitting) CancelSplit(); else Cancel(); return; }
|
|
var point = touch.Value.Position; Move(point); End(point);
|
|
}
|
|
|
|
private void RecordTouch(Finger finger)
|
|
{
|
|
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) { _touchEvents.Clear(); _touches.Clear(); return; }
|
|
bool hadTouches = _touchEvents.Count > 0, suppressRestOfFrame = false;
|
|
while (_touchEvents.Count > 0)
|
|
{
|
|
var touch = _touchEvents.Dequeue(); _touches[touch.Id] = touch;
|
|
if (!suppressRestOfFrame)
|
|
{
|
|
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();
|
|
if (mouse.leftButton.wasPressedThisFrame) Begin(position, IsUI(position));
|
|
if (mouse.leftButton.isPressed) Move(position);
|
|
if (mouse.leftButton.wasReleasedThisFrame) End(position);
|
|
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 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();
|
|
}
|
|
}
|
|
}
|