using System.Collections.Generic; using UnityEngine; namespace ProjectX { [RequireComponent(typeof(InputTarget))] /// /// Component to allow for easy and flexible implementation of dragging objects. It uses colliders as surfaces to drag the object over. If there is a rigidbody on the object physics will be respected. /// public class Drag : MonoBehaviour { /// /// How a collider should be chosen to move over /// public enum MoveMode { /// /// When dragging the object will move over the closest (if any) collider in the colliders list /// CollidersPreferClosest, /// /// When dragging the object will move over the collider (if any) that has the lowest index in the colliders list /// CollidersPreferIndex } /// /// Dragging was started /// public event System.Action onDragStarted = null; /// /// Dragging was stopped /// public event System.Action onDragStopped = null; /// /// Object is being dragged, also dispatched if dragged distance is 0 /// public event System.Action onDrag = null; /// /// Object is being dragged and updated on FixedUpdate, will happen if a RigidBody is attached to the object /// public event System.Action onFixedDrag = null; [Header("Basic Settings:")] /// /// If starting and stopping of dragging should be handled by this component itself. If not StartDragging and StopDragging should be used. /// public bool autoStartAndStop = true; [Header("Raycast Settings:")] /// /// How a collider should be chosen to move over /// public MoveMode moveMode; /// /// If mask should be used in raycast /// public bool useRaycastMask = false; /// /// Mask to use for raycasting of the colliders (so not for input) /// public LayerMask raycastMask; /// /// Maximum distance raycasts will be done for input, can affect performance if there are a lot of distant colliders that have the same mask /// public float maxRaycastDistance = float.MaxValue; /// /// Colliders this object should move over while dragging /// public List colliders = null; [Header("Screen Offset Settings:")] /// /// If the object should stay the same distance from the finger as it was when it was first touched /// public bool useScreenOffset = true; /// /// The offset on screen. /// public Vector2 offsetOnScreen = Vector2.zero; [Header("Dragging Settings:")] /// /// If useScreenOffset is false, the time it takes for the object to move towards the finger. Note that this basically works like reducing the offset and not like easing the following. /// public float moveToFingerTime = 0.4f; /// /// If the object should be prevented from going off the screen, the camera the object was touched on first will be used as reference. /// public bool offscreenPrevention = true; /// /// If the object should align with the surface of the collider it is being dragged over. /// public bool alignWithTargetNormal = false; /// /// Maximum speed at which the object is allowed to move by dragging. Can be used to prevent quick dragging through objects /// public float maxDragVelocity = 222f; /// /// Maximum speed at which the object is allowed to move after release, is never higher than maxDragVelocity /// public float maxReleaseVelocity = 10f; /// /// Time in seconds touch movement is recorded to determine the release velocity. Wrong values will result in seemingly wrong directions after release. /// public float dragStatisticTime = 0.33f; /// /// Maximum speed at which the object is allowed to move after release, is never higher than maxDragVelocity /// public float idleDeceleration = 10f; /// /// Velocity used after releasing, is being decelerated using idleDeceleration every Update /// public Vector3 idleVelocity; /// /// The move to target factor. Whorks when using physics drag. /// public float moveToTargetFactor = 0.18f; /// /// The move to speed factor. Whorks when using physics drag. /// public float moveToSpeedFactor = 0.25f; private Camera cameraToUse = null; private int fingerId = -1; private Vector3 offset = Vector3.zero; private bool adjustedOffestOnScreen = false; private float moveToFingerSpeed = 0f; private List listeners = new List(); private DragStatistics statistics = null; private Collider currentlyOverCollider = null; private InputTarget inputTarget = null; private bool doFixedUpdate = false; private Vector3 fixedUpdatePosition = Vector3.zero; public void StartDragging(int _fingerId, Camera _cameraToUse) { fingerId = _fingerId; cameraToUse = _cameraToUse; Vector3 inputpos = this.GetInputPosition(this.fingerId); Vector3 screenpos = cameraToUse.WorldToScreenPoint(transform.position); offset = inputpos - screenpos; offset.x -= offsetOnScreen.x; offset.y -= offsetOnScreen.y; moveToFingerSpeed = offset.magnitude / moveToFingerTime; if (onDragStarted != null) { onDragStarted(this); } } public void StopDragging() { if (fingerId == -1) return; fingerId = -1; Vector3 newVelocity = statistics.GetResult(); statistics.Reset(); Rigidbody rigidbody = this.GetComponent(); if (rigidbody != null && !rigidbody.isKinematic) { doFixedUpdate = false; } else { newVelocity = newVelocity.normalized * Mathf.Clamp(newVelocity.magnitude, 0, maxDragVelocity); idleVelocity = newVelocity; } if (onDragStopped != null) { onDragStopped(this); } } public void StopDraggingImmediately() { this.fingerId = -1; this.idleVelocity = Vector3.zero; Rigidbody rigidbody = this.GetComponent(); if (rigidbody != null && !rigidbody.isKinematic) { doFixedUpdate = false; rigidbody.AddForce(-rigidbody.velocity, ForceMode.VelocityChange); } if (onDragStopped != null) { onDragStopped(this); } } public void UseAlternativeTarget(InputTarget alternativeTarget) { if (alternativeTarget == null) return; inputTarget.onPress.Detach(this.OnPress); inputTarget = alternativeTarget; inputTarget.GetComponent().enabled = true; inputTarget.onPress.Attach(this.OnPress); } /// /// Get current collider the object is being dragged over (if any) /// public Collider GetCurrentCollider() { return currentlyOverCollider; } void Awake() { statistics = new DragStatistics(dragStatisticTime); inputTarget = this.GetComponent(); inputTarget.onPress.Attach(this.OnPress); InputDetection.instance.onReleaseAnyWhere.Attach(this.OnReleaseAnywhere); if (adjustedOffestOnScreen) { offsetOnScreen *= Screen.height / 768.0f; } } void OnDestroy() { inputTarget.onPress.Detach(this.OnPress); InputDetection.instance.onReleaseAnyWhere.Detach(this.OnReleaseAnywhere); } // Update is called once per frame void Update() { doFixedUpdate = false; if (fingerId > -1) { // Fix for 4 finger gesture on iOS where no release is // being received from Unity #if UNITY_EDITOR #else bool isStillTouched = false; foreach(Touch touch in Input.touches) { if(touch.fingerId == fingerId) { isStillTouched = true; } } if(!isStillTouched) { StopDragging(); return; } #endif if (!useScreenOffset) { offset = Vector3.MoveTowards(offset, new Vector3(-offsetOnScreen.x, -offsetOnScreen.y, 0), moveToFingerSpeed * Time.deltaTime); } Vector3 inputpos = this.GetInputPosition(this.fingerId); Vector3 newpos = Vector3.zero; Vector3 raycastPos = inputpos - offset; raycastPos.x = Mathf.Clamp(raycastPos.x, 0, Screen.width); raycastPos.y = Mathf.Clamp(raycastPos.y, 0, Screen.height); Ray ray = cameraToUse.ScreenPointToRay(raycastPos); int maskToUse = useRaycastMask ? (int)this.raycastMask : cameraToUse.cullingMask; List hits = new List(Physics.RaycastAll(ray, maxRaycastDistance, maskToUse)); hits.RemoveAll(hit => !colliders.Contains(hit.collider)); if (hits.Count == 0) { currentlyOverCollider = null; return; } HitSorter.colliders = colliders; HitSorter.moveMode = moveMode; HitSorter.cameraToUse = cameraToUse; List sortedHits = HitSorter.Sort(hits); RaycastHit selectedHit = sortedHits[0]; newpos = selectedHit.point; currentlyOverCollider = selectedHit.collider; if (alignWithTargetNormal) { transform.forward = selectedHit.normal; } if (offscreenPrevention) { Vector3 viewPortPosition = cameraToUse.WorldToViewportPoint(newpos); viewPortPosition.x = Mathf.Clamp(viewPortPosition.x, 0, 1f); viewPortPosition.y = Mathf.Clamp(viewPortPosition.y, 0, 1f); newpos = cameraToUse.ViewportToWorldPoint(viewPortPosition); } Vector3 delta = newpos - transform.position; Vector3 newVelocity = delta / Time.deltaTime; newVelocity = newVelocity.normalized * Mathf.Clamp(newVelocity.magnitude, 0, maxDragVelocity); statistics.Record(newVelocity, Time.deltaTime); Rigidbody rigidbody = this.GetComponent(); if (rigidbody == null || rigidbody.isKinematic) { Vector3 velocity = (newpos - transform.position) / Time.deltaTime; float speed = Mathf.Clamp(velocity.magnitude, 0f, maxDragVelocity); transform.position += velocity.normalized * speed * Time.deltaTime; } else { doFixedUpdate = true; fixedUpdatePosition = newpos; } if (onDrag != null) { onDrag(this); } } else { Rigidbody rigidbody = this.GetComponent(); if (rigidbody == null || rigidbody.isKinematic) { float speed = idleVelocity.magnitude; speed = Mathf.MoveTowards(speed, 0f, idleDeceleration * Time.deltaTime); idleVelocity = idleVelocity.normalized * speed; if (!Mathf.Approximately(speed, 0)) { transform.position += idleVelocity * Time.deltaTime; } } } } void FixedUpdate() { if (!doFixedUpdate) return; Rigidbody rigidbody = this.GetComponent(); if (rigidbody != null && !rigidbody.isKinematic) { Vector3 targetPos = Vector3.Lerp(transform.position, fixedUpdatePosition, moveToTargetFactor); Vector3 delta = targetPos - transform.position; Vector3 newVelocity = delta / Time.deltaTime; newVelocity = Vector3.Lerp(rigidbody.velocity, newVelocity, moveToSpeedFactor); rigidbody.AddForce(newVelocity - rigidbody.velocity, ForceMode.VelocityChange); if (onFixedDrag != null) { onFixedDrag(this); } } } void OnPress(InputTouch touch) { if (this.enabled && this.autoStartAndStop) { if (this.fingerId == -1) { this.StartDragging(touch.fingerId, touch.caster.camera); } } } void OnReleaseAnywhere(InputTouch touch) { if (autoStartAndStop) { if (this.fingerId == touch.fingerId) { this.StopDragging(); } } } Vector3 GetInputPosition(int finger) { if (Application.isEditor) return Input.mousePosition; foreach (Touch touch in Input.touches) { if (touch.fingerId == finger) return touch.position; } Debug.LogError("Drag.GetInputPosition(): No touch found with finger id " + finger); return Vector3.zero; } private class DragStatistics { private List mVelocities = new List(); private List mTimes = new List(); private float mMaxTime = 0; private float mAccTime = 0; public List velocities { get { return mVelocities; } } public DragStatistics(float _maxTime) { this.mMaxTime = _maxTime; this.Reset(); } public void Reset() { mVelocities.Clear(); mTimes.Clear(); mAccTime = 0f; } // Update is called once per frame public void Record(Vector3 velocity, float deltaTime) { mVelocities.Add(velocity); mTimes.Add(deltaTime); mAccTime += deltaTime; while (mAccTime > mMaxTime) { mVelocities.RemoveAt(0); mAccTime -= mTimes[0]; mTimes.RemoveAt(0); } } public Vector3 GetResult() { Vector3 newVelocity = Vector3.zero; for (int i = 0; i < mVelocities.Count; i++) { newVelocity += mVelocities[i]; } if (mVelocities.Count > 0) { newVelocity /= mVelocities.Count; } return newVelocity; } } private class HitSorter { public static Camera cameraToUse; public static MoveMode moveMode; public static List colliders; private static float distanceA; private static float distanceB; private static int indexA; private static int indexB; private static Vector3 cameraPosition; public static List Sort(List hits) { List result = new List(hits); switch (moveMode) { case MoveMode.CollidersPreferClosest: cameraPosition = cameraToUse.transform.position; result.Sort(SortClosest); break; case MoveMode.CollidersPreferIndex: result.Sort(SortIndex); break; } return result; } static int SortClosest(RaycastHit a, RaycastHit b) { distanceA = Vector3.Distance(a.point, cameraPosition); distanceB = Vector3.Distance(b.point, cameraPosition); if (distanceA < distanceB) return -1; else if (distanceB > distanceA) return 1; else return 0; } static int SortIndex(RaycastHit a, RaycastHit b) { indexA = colliders.IndexOf(a.collider); indexB = colliders.IndexOf(b.collider); if (indexA < indexB) return -1; else if (indexB > indexA) return 1; else return 0; } } } }