For develop my own Hand gesture script, I’m tried to keep testing the touch logic on device,
also the unity team had so many dark logic hidden behind the document,
and the official Touch to Mouse Simulator (Input.simulateMouseWithTouches) basically cause you trouble to debug.
Therefore I started to develop my Mouse simulate touch script.
the concept is simple, to simulate 1 finger drag and 2 finger stretch in/out & rotate based on mouse input.
giving the benefit to actually test the Touch script on editor instead of write another system,
(of course with limitation, only LEFT CLICK are accurate).
the feature list follow.
- Detect Mouse Click & Drag and identify: record position, delta position, touch phase
- Simulate 2nd finger stretch in/out based on Mouse Wheel input value.
- Simulate 2nd finger rotate around mouse position
if anyone feel useful, please feel free to use it. and welcome advise.
CTouch.cs
Important : the position and delta position was changed to viewport scale
using UnityEngine; using System.Collections.Generic; public class CTouch : MonoBehaviour { #region Variables [SerializeField] bool m_Debug = true; [SerializeField] Camera m_Camera; [SerializeField] eRotateMethod m_RotateMethod = eRotateMethod.click_n_Wheel; [SerializeField] float m_IdleActionTimeout = .5f; public enum eRotateMethod { click_n_Wheel = 0, x_Wheel } public class TouchCache { public int fingerId = -1; public Vector2 position = Vector2.zero; public Vector2 deltaPosition = Vector2.zero; public TouchPhase phase = TouchPhase.Ended; public float deltaTime = 0f; private object _touch; public bool IsMouse { get { return _touch == null; } } public Touch GetTouch() { return IsMouse ? new Touch() : (Touch)_touch; } public Camera m_Camera = null; public void ConvertFrom(Touch touch) { fingerId = touch.fingerId; position = touch.position; deltaPosition = touch.deltaPosition; phase = touch.phase; deltaTime = touch.deltaTime; _touch = touch; } public Vector2 GetViewPosition() { return m_Camera.ScreenToViewportPoint(position); } public Vector2 GetViewDeltaPosition() { return m_Camera.ScreenToViewportPoint(deltaPosition); } public Ray GetRay() { return m_Camera.ScreenPointToRay(new Vector3(position.x, position.y, m_Camera.nearClipPlane)); } public TouchCache(Camera camera) { m_Camera = camera; Reset(); } public void Reset() { fingerId = -1; position = Vector2.one * 0.5f; deltaPosition = Vector2.zero; phase = TouchPhase.Ended; deltaTime = 0f; _touch = null; } } private List<TouchCache> m_Cache; float m_RotateDelta, m_ZoomDelta; const int _MaxCacheNum = 3; #endregion #region Getter public TouchCache[] touches { get { return m_Cache.ToArray(); } } public int touchCount { get; private set; } #endregion #region System void OnEnable() { if (m_Camera == null) m_Camera = Camera.main; m_Cache = new List<TouchCache>(_MaxCacheNum) { new TouchCache(m_Camera), new TouchCache(m_Camera), new TouchCache(m_Camera), }; touchCount = 0; m_RotateDelta = m_ZoomDelta = 0f; if (DevManager.Instance != null) DevManager.Instance.Register(this); } void OnDisable() { if (DevManager.Instance != null) DevManager.Instance.UnRegister(this); } void FixedUpdate() { AnalyticInput(); } void OnGUI() { if (!m_Debug) return; DebugGUI(); } [DevGUICallBack("CTouch")] public void DebugGUI() { for (int i = 0; i < m_Cache.Count; i++) { GUILayout.Label(string.Format("Touch[{0,3} - {1,15}] on Position{2}, delta {3}", i, m_Cache[i].phase.ToString("F"), m_Cache[i].position, m_Cache[i].deltaPosition)); Ray ray = m_Cache[i].GetRay(); Color color = i == 0 ? Color.red : i == 1 ? Color.green : Color.yellow; if (m_Cache[i].phase < TouchPhase.Ended) Debug.DrawRay(ray.origin, ray.direction, color); } GUILayout.Label("ZoomDelta :" + m_ZoomDelta); GUILayout.Label("RotateDelta :" + m_RotateDelta); GUILayout.Label("TouchCount :" + touchCount); } #endregion #region Main void AnalyticInput() { if (Input.touchSupported) DeviceTouchInput(); else if (Input.mousePresent) MouseTouchSimulator(); else { Debug.LogWarning("Unknow device: CTouch disable."); touchCount = 0; } } #endregion #region Mouse Simulator void MouseTouchSimulator() { Vector3 mousePosition = Input.mousePosition; Vector2 mouseScrollDelta = Input.mouseScrollDelta; // 1st Touch - mapping drag & click MouseTouchDragSimulator((Vector2)mousePosition); touchCount = m_Cache[0].phase < TouchPhase.Ended ? 1 : 0; // 2nd Touch - mapping wheel to zoom MouseTouchZoomSimulator(mousePosition, mouseScrollDelta); if (m_Cache[1].phase < TouchPhase.Ended) touchCount++; // 3rd Touch - mapping click & wheel to rotation MouseTouchRotateHelper(mouseScrollDelta); if (m_Cache[2].phase < TouchPhase.Ended) touchCount++; } void MouseTouchDragSimulator(Vector2 mousePosition) { if (Input.GetMouseButtonDown(0)) { m_Cache[0].fingerId = 0; m_Cache[0].phase = TouchPhase.Began; m_Cache[0].position = mousePosition; m_Cache[0].deltaPosition = Vector2.zero; } else if (Input.GetMouseButtonUp(0)) { m_Cache[0].Reset(); } else if (m_Cache[0].phase < TouchPhase.Ended) { // touch state m_Cache[0].deltaTime = Time.deltaTime; m_Cache[0].deltaPosition = mousePosition - m_Cache[0].position; m_Cache[0].phase = TouchPhase.Stationary; // not allow elseif here, touch & move can happen at same time. if (m_Cache[0].position != mousePosition) { m_Cache[0].phase = TouchPhase.Moved; m_Cache[0].position = mousePosition; } else if (!Input.GetMouseButton(0)) { // special case: on mouse release click off screen. // e.g. change application, lose force..etc m_Cache[0].Reset(); m_Cache[0].phase = TouchPhase.Canceled; } } else if (m_Cache[0].phase == TouchPhase.Canceled) { // special case: back to End state after one frame. m_Cache[0].Reset(); } } void MouseTouchZoomSimulator(Vector3 mousePosition, Vector2 mouseScrollDelta) { float scroll = Input.GetAxis("Mouse ScrollWheel"); if (!Mathf.Approximately(scroll, 0f)) { // calculate m_ZoomDelta = Mathf.Clamp(Mathf.Abs(scroll), 0f, .5f) * Mathf.Sign(scroll); if (m_RotateMethod == eRotateMethod.x_Wheel) m_RotateDelta += mouseScrollDelta.x * 10f; else if (m_RotateMethod == eRotateMethod.click_n_Wheel && Input.GetMouseButton(1)) m_RotateDelta += scroll * 1f; Vector3 anchor = new Vector3(.5f, .5f, m_Camera.nearClipPlane); if (m_Cache[0].phase < TouchPhase.Ended) { Vector3 view = m_Camera.ScreenToViewportPoint(m_Cache[0].position); anchor = new Vector3(view.x, view.y, m_Camera.nearClipPlane); } m_Cache[1].fingerId = 1; m_Cache[1].deltaTime = Time.deltaTime; Vector3 vCenter = m_Camera.ViewportToWorldPoint(anchor); Vector3 vRadius = m_Camera.ViewportToWorldPoint(new Vector3(anchor.x + m_ZoomDelta, anchor.y, m_Camera.nearClipPlane)); Vector3 vRotate = m_Camera.ViewportToWorldPoint(new Vector3(anchor.x + m_ZoomDelta * Mathf.Cos(m_RotateDelta), anchor.y * Mathf.Sin(m_RotateDelta), m_Camera.nearClipPlane)); Vector3 offset3D = m_Camera.WorldToViewportPoint(Input.GetMouseButton(1) ? vRotate : vRadius); Debug.DrawLine(vCenter, offset3D, Color.magenta, 0.1f); m_Cache[1].deltaPosition = m_Cache[1].position - new Vector2(offset3D.x, offset3D.y); m_Cache[1].position = offset3D; // touch state if (m_Cache[1].phase >= TouchPhase.Ended) { m_Cache[1].phase = TouchPhase.Began; m_Cache[1].deltaPosition = Vector2.zero; } else if (m_Cache[1].phase == TouchPhase.Began) { m_Cache[1].phase = TouchPhase.Moved; } } else if (m_Cache[1].phase < TouchPhase.Ended) { m_Cache[1].deltaTime += Time.deltaTime; if (m_Cache[1].deltaTime > m_IdleActionTimeout) { m_Cache[1].Reset(); m_ZoomDelta = 0f; m_RotateDelta = 0f; } } } void MouseTouchRotateHelper(Vector2 mouseScrollDelta) { // this touch only for rotate without hold down button if (m_Cache[0].phase >= TouchPhase.Ended && m_Cache[1].phase < TouchPhase.Ended && !Input.GetMouseButton(0)) { // touch m_Cache[2].fingerId = 2; m_Cache[2].deltaTime = Time.deltaTime; m_Cache[2].deltaPosition = Vector2.zero; m_Cache[2].position = Vector2.one * .5f; // viewport center // touch state if (m_Cache[2].phase >= TouchPhase.Ended) m_Cache[2].phase = TouchPhase.Began; else if (m_Cache[2].phase == TouchPhase.Began) m_Cache[2].phase = TouchPhase.Stationary; } else if (m_Cache[1].phase == TouchPhase.Ended && m_Cache[2].phase < TouchPhase.Ended) { m_Cache[2].Reset(); } } #endregion #region Device input void DeviceTouchInput() { int filled = 0; for (int x = 0; x < Input.touchCount && filled < m_Cache.Count - 1; x++) { if (Input.touches[x].phase != TouchPhase.Ended) { m_Cache[filled].ConvertFrom(Input.touches[x]); filled++; } else { m_Cache[x].Reset(); } } touchCount = filled; } #endregion }
CCameraOrbit.cs
using UnityEngine; [RequireComponent(typeof(CTouch))] public class CCameraOrbit : MonoBehaviour { [SerializeField] CTouch m_Input; [SerializeField] float m_Speed = 30f; [SerializeField] float m_InputMulipler = 200f; [SerializeField] float m_MobileInputMulipler = 600f; [SerializeField] float m_AngleCap = 90f; [SerializeField] float m_Distance = 10f; [SerializeField] LayerMask m_LayerMask = 0; [SerializeField] Collider m_DetectArea = null; private Quaternion m_TargetRotate; private Quaternion m_DefaultRotate; private bool m_IsRotate = false; private int m_FingerId = -1; private bool m_CheckDelectArea = false; void OnValidate() { if (m_Input) m_Input = GetComponent<CTouch>(); } void Awake() { m_TargetRotate = m_DefaultRotate = transform.rotation; } void OnEnable() { transform.rotation = m_TargetRotate = m_DefaultRotate; if (DevManager.Instance != null) DevManager.Instance.Register(this); // ensure the component status, since NGUI will fuck this up. m_DetectArea.gameObject.layer = Mathf.CeilToInt(Mathf.Log(m_LayerMask.value, 2)); m_DetectArea.enabled = true; } void OnDisable() { if (DevManager.Instance != null) DevManager.Instance.UnRegister(this); } [DevGUICallBack("CTouch")] public void OnDebugGUI() { GUILayout.Label("Status : " + (m_IsRotate ? "Rotating" : "Idle")); GUILayout.Label("Finger Id: " + m_FingerId); GUILayout.Space(10f); // check if detect area fuckup. m_CheckDelectArea = GUILayout.Toggle(m_CheckDelectArea, "Check detect area."); if (m_CheckDelectArea) { foreach (CTouch.TouchCache touch in m_Input.touches) { if (touch.phase < TouchPhase.Ended) GUILayout.Label("Finger : " + touch.phase.ToString("F") + " pos:" + touch.position); } GUILayout.Label("Detecting Object: " + m_DetectArea.name + ", enable=" + m_DetectArea.enabled + ", activeSelf=" + m_DetectArea.gameObject.activeInHierarchy + ", localScale=" + m_DetectArea.transform.localScale); if (GUILayout.Button("make that collider fucking big " + m_DetectArea.transform.lossyScale)) m_DetectArea.transform.localScale = Vector3.one * 1000; GUILayout.Space(10f); GUILayout.Label("Layer=" + LayerMask.LayerToName(m_DetectArea.gameObject.layer) + ", vs. Camera Mask=" + LayerMask.LayerToName(Mathf.CeilToInt(Mathf.Log(m_LayerMask.value, 2)))); m_DetectArea.isTrigger = GUILayout.Toggle(m_DetectArea.isTrigger, "Toggle IsTrigger"); } GUILayout.Space(10f); GUILayout.Label("Speed "+ m_Speed); m_Speed = float.Parse(GUILayout.TextField(m_Speed.ToString())); GUILayout.Label("Input Mulipler "+ m_InputMulipler); m_InputMulipler = float.Parse(GUILayout.TextField(m_InputMulipler.ToString())); GUILayout.Label("Distance "+ m_Distance); m_Distance = GUILayout.HorizontalSlider(m_Distance, 0f, float.MaxValue); GUILayout.Label("Angle Cap " + m_AngleCap); m_AngleCap = GUILayout.HorizontalSlider(m_AngleCap, 1f, 90f); } void FixedUpdate() { if(!m_IsRotate) // don't combine this checking, otherwise "else" will keep running even no input { if (m_Input.touchCount > 0) { for (int i =0; !m_IsRotate && i < m_Input.touches.Length; i++) { if (m_Input.touches[i].phase < TouchPhase.Ended) { foreach(RaycastHit hit in Physics.RaycastAll(m_Input.touches[i].GetRay(), m_Distance, m_LayerMask)) { if (hit.collider == m_DetectArea) { m_IsRotate = true; m_TargetRotate = transform.rotation; m_FingerId = m_Input.touches[i].fingerId; break; } } } } } } else { bool alive = false; foreach (CTouch.TouchCache touch in m_Input.touches) { if(touch.fingerId == m_FingerId && touch.phase < TouchPhase.Ended) { alive = true; if(Mathf.Abs(touch.deltaPosition.x) > 0f) { float diff = touch.GetViewDeltaPosition().x * (Application.isMobilePlatform ? m_MobileInputMulipler : m_InputMulipler); float speed = OverTurnAngleAccuracy(diff); m_TargetRotate *= Quaternion.AngleAxis(speed, Vector3.up); // Accumulate angle //Debug.LogFormat("diff={0:F2}, signAngle={1:F2}, predictAngle={2:F2} overflowAngle={3:F2}", // diff, signAngle, predictAngle, overflowAngle); Debug.DrawRay(transform.position, Quaternion.AngleAxis(speed, Vector3.up) * Vector3.forward, Color.red); } } } if (!alive) { m_FingerId = -1; m_IsRotate = false; return; } } Debug.DrawRay(transform.position, m_TargetRotate * Vector3.forward, Color.cyan); Debug.DrawRay(transform.position, transform.forward, Color.yellow); transform.rotation = Quaternion.Lerp(transform.rotation, m_TargetRotate, Time.deltaTime * m_Speed); // Debug.DrawRay(transform.position, m_TargetRotate * Vector3.forward, Color.red, 0.1f); } private float OverTurnAngleAccuracy(float amount) { // amount = Mathf.Clamp(amount, -360f, 360f); Quaternion predict = m_TargetRotate * Quaternion.AngleAxis(amount, Vector3.up); float angleDiff = Mathf.Abs(AngleBetweenDirectionSigned(transform.forward, predict * Vector3.forward, Vector3.up)), overTurnHotfix = Mathf.Lerp(1f, 0.1f, ((angleDiff >= m_AngleCap) ? 1f : angleDiff / m_AngleCap)); return amount * overTurnHotfix; } public float AngleBetweenDirectionSigned(Vector3 direction1, Vector3 direction2, Vector3 normal) { return Mathf.Rad2Deg * Mathf.Atan2(Vector3.Dot(normal, Vector3.Cross(direction1, direction2)), Vector3.Dot(direction1, direction2)); } }