using UnityEngine; using System; using System.Collections.Generic; using UnityEngine.EventSystems; namespace Nullframes.Intrigues { /// A modular and easily customisable Unity MonoBehaviour for handling swipe and pinch motions on mobile. public class PanAndZoom : MonoBehaviour { /// Called as soon as the player touches the screen. The argument is the screen position. public event Action onStartTouch; /// Called as soon as the player stops touching the screen. The argument is the screen position. public event Action onEndTouch; /// Called if the player completed a quick tap motion. The argument is the screen position. public event Action onTap; /// Called if the player swiped the screen. The argument is the screen movement delta. public event Action onSwipe; /// Called if the player pinched the screen. The arguments are the distance between the fingers before and after. public event Action onPinch; [Header("Tap")] [Tooltip("The maximum movement for a touch motion to be treated as a tap")] public float maxDistanceForTap = 40; [Tooltip("The maximum duration for a touch motion to be treated as a tap")] public float maxDurationForTap = 0.4f; [Header("Desktop debug")] [Tooltip("Use the mouse on desktop?")] public bool useMouse = true; [Tooltip("The simulated pinch speed using the scroll wheel")] public float zoomSpeed = 1.0f; public float minZoom = 5.0f; public float maxZoom = 50.0f; public float smoothFactor = 10.0f; private float targetZoom; [Header("Camera control")] [Tooltip("Does the script control camera movement?")] public bool controlCamera = true; [Tooltip("The controlled camera, ignored of controlCamera=false")] public Camera cam; [Header("UI")] [Tooltip("Are touch motions listened to if they are over UI elements?")] public bool ignoreUI; [Header("Bounds")] [Tooltip("Is the camera bound to an area?")] public bool useBounds; public float boundMinX = -150; public float boundMaxX = 150; public float boundMinY = -150; public float boundMaxY = 150; private Vector3 targetPosition; Vector2 touch0StartPosition; Vector2 touch0LastPosition; float touch0StartTime; bool cameraControlEnabled = true; bool canUseMouse; /// Has the player at least one finger on the screen? private bool isTouching { get; set; } /// The point of contact if it exists in Screen space. public Vector2 touchPosition => touch0LastPosition; void Start() { canUseMouse = Application.platform != RuntimePlatform.Android && Application.platform != RuntimePlatform.IPhonePlayer && Input.mousePresent; targetZoom = cam.orthographicSize; } void Update() { if (useMouse && canUseMouse) { UpdateWithMouse(); } else { UpdateWithTouch(); } } void LateUpdate() { CameraInBounds(); } void UpdateWithMouse() { if (Input.GetMouseButtonDown(0)) { if (ignoreUI || !IsPointerOverUIObject()) { touch0StartPosition = Input.mousePosition; touch0StartTime = Time.time; touch0LastPosition = touch0StartPosition; isTouching = true; if (onStartTouch != null) onStartTouch(Input.mousePosition); } } if (Input.GetMouseButton(0) && isTouching) { Vector2 move = (Vector2)Input.mousePosition - touch0LastPosition; touch0LastPosition = Input.mousePosition; if (move != Vector2.zero) { OnSwipe(move); } } if (Input.GetMouseButtonUp(0) && isTouching) { if (Time.time - touch0StartTime <= maxDurationForTap && Vector2.Distance(Input.mousePosition, touch0StartPosition) <= maxDistanceForTap) { OnClick(Input.mousePosition); } if (onEndTouch != null) onEndTouch(Input.mousePosition); isTouching = false; cameraControlEnabled = true; } float scrollData = -Input.GetAxis("Mouse ScrollWheel"); targetZoom += scrollData * zoomSpeed; targetZoom = Mathf.Clamp(targetZoom, minZoom, maxZoom); cam.orthographicSize = Mathf.Lerp(cam.orthographicSize, targetZoom, Time.deltaTime * smoothFactor); // if (Input.mouseScrollDelta.y != 0) { // OnPinch(Input.mousePosition, 1, Input.mouseScrollDelta.y < 0 ? (1 / mouseScrollSpeed) : mouseScrollSpeed, Vector2.right); // } } void UpdateWithTouch() { int touchCount = Input.touches.Length; if (touchCount == 1) { Touch touch = Input.touches[0]; switch (touch.phase) { case TouchPhase.Began: { if (ignoreUI || !IsPointerOverUIObject()) { touch0StartPosition = touch.position; touch0StartTime = Time.time; touch0LastPosition = touch0StartPosition; isTouching = true; if (onStartTouch != null) onStartTouch(touch0StartPosition); } break; } case TouchPhase.Moved: { touch0LastPosition = touch.position; if (touch.deltaPosition != Vector2.zero && isTouching) { OnSwipe(touch.deltaPosition); } break; } case TouchPhase.Ended: { if (Time.time - touch0StartTime <= maxDurationForTap && Vector2.Distance(touch.position, touch0StartPosition) <= maxDistanceForTap && isTouching) { OnClick(touch.position); } if (onEndTouch != null) onEndTouch(touch.position); isTouching = false; cameraControlEnabled = true; break; } case TouchPhase.Stationary: case TouchPhase.Canceled: break; } } else if (touchCount == 2) { Touch touch0 = Input.touches[0]; Touch touch1 = Input.touches[1]; if (touch0.phase == TouchPhase.Ended || touch1.phase == TouchPhase.Ended) return; isTouching = true; float previousDistance = Vector2.Distance(touch0.position - touch0.deltaPosition, touch1.position - touch1.deltaPosition); float currentDistance = Vector2.Distance(touch0.position, touch1.position); if (Math.Abs(previousDistance - currentDistance) > double.Epsilon) { OnPinch((touch0.position + touch1.position) / 2, previousDistance, currentDistance); } } else { if (isTouching) { if (onEndTouch != null) onEndTouch(touch0LastPosition); isTouching = false; } cameraControlEnabled = true; } } void OnClick(Vector2 position) { if (onTap != null && (ignoreUI || !IsPointerOverUIObject())) { onTap(position); } } void OnSwipe(Vector2 deltaPosition) { if (onSwipe != null) { onSwipe(deltaPosition); } if (controlCamera && cameraControlEnabled) { if (cam == null) cam = Camera.main; if (cam != null) cam.transform.position -= (cam.ScreenToWorldPoint(deltaPosition) - cam.ScreenToWorldPoint(Vector2.zero)); } } void OnPinch(Vector2 center, float oldDistance, float newDistance) { if (onPinch != null) { onPinch(oldDistance, newDistance); } if (controlCamera && cameraControlEnabled) { if (cam == null) cam = Camera.main; if (cam != null && cam.orthographic) { var currentPinchPosition = cam.ScreenToWorldPoint(center); cam.orthographicSize = Mathf.Max(0.1f, cam.orthographicSize * oldDistance / newDistance); var newPinchPosition = cam.ScreenToWorldPoint(center); cam.transform.position -= newPinchPosition - currentPinchPosition; } else { if (cam != null) cam.fieldOfView = Mathf.Clamp(cam.fieldOfView * oldDistance / newDistance, 0.1f, 179.9f); } } } /// Checks if the the current input is over canvas UI private bool IsPointerOverUIObject() { if (EventSystem.current == null) return false; PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current) { position = new Vector2(Input.mousePosition.x, Input.mousePosition.y) }; List results = new List(); EventSystem.current.RaycastAll(eventDataCurrentPosition, results); return results.Count > 0; } /// Cancels camera movement for the current motion. Resets to use camera at the end of the touch motion. public void CancelCamera() { cameraControlEnabled = false; } void CameraInBounds() { if (controlCamera && useBounds && cam != null && cam.orthographic) { cam.orthographicSize = Mathf.Min(cam.orthographicSize, ((boundMaxY - boundMinY) / 2) - 0.001f); cam.orthographicSize = Mathf.Min(cam.orthographicSize, (Screen.height * (boundMaxX - boundMinX) / (2 * Screen.width)) - 0.001f); Vector2 margin = cam.ScreenToWorldPoint((Vector2.up * Screen.height / 2) + (Vector2.right * Screen.width / 2)) - cam.ScreenToWorldPoint(Vector2.zero); float marginX = margin.x; float marginY = margin.y; float camMaxX = boundMaxX - marginX; float camMaxY = boundMaxY - marginY; float camMinX = boundMinX + marginX; float camMinY = boundMinY + marginY; float camX = Mathf.Clamp(cam.transform.position.x, camMinX, camMaxX); float camY = Mathf.Clamp(cam.transform.position.y, camMinY, camMaxY); cam.transform.position = new Vector3(camX, camY, cam.transform.position.z); } } } }