Unity3D Mouse to Touch Simulator

Unity3D Mouse to Touch Simulator

 

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).

m2t

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

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

*

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料