Files
MagnetismHeroes/Assets/MagnetismHeroes/UnityRuntime/Interaction/BoardInput.cs
T

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();
}
}
}