Unity Joystick手势操作
Unity Joystick手勢(shì)操作
作者:無聊
實(shí)現(xiàn)原因
由于制作Demo的需要,第三方的相關(guān)插件都過于重量級(jí),所以就自己實(shí)現(xiàn)了一個(gè)簡單的手勢(shì)操作方案。
基本功能
本文實(shí)現(xiàn)了一個(gè)簡易的Unity JoyStick手勢(shì)操作,主要實(shí)現(xiàn)三個(gè)功能,操縱桿(Joystick)、相機(jī)旋轉(zhuǎn)(Rotate)與縮放(Scale)。
基本邏輯結(jié)構(gòu)如下:
protected void LateUpdate() {AroundByMobileInput(); }void AroundByMobileInput() {if (Input.touchCount > 0 && Input.touchCount <= 2){for (int i = 0; i < Input.touchCount; i++){if (Input.touches[i].phase == TouchPhase.Began){判斷可能存在縮放操作的計(jì)時(shí)標(biāo)記如果在屏幕左半邊,則初始化Joystick記錄觸摸信息,包括位置、ID如果在屏幕右半邊,則初始化Rotate記錄觸摸信息,包括位置、ID計(jì)時(shí)器增加 不能操作縮放}else if (Input.touches[i].phase == TouchPhase.Moved || Input.touches[i].phase == TouchPhase.Stationary){根據(jù)ID來執(zhí)行相應(yīng)操作,Joystick還是Rotate操作計(jì)時(shí)器操作}else if (Input.touches[i].phase == TouchPhase.Canceled || Input.touches[i].phase == TouchPhase.Ended){同樣根據(jù)ID來執(zhí)行最后的收尾工作}}}根據(jù)計(jì)時(shí)器的時(shí)間判斷是否可以進(jìn)行縮放操作 canScaleif (canScale){雙指縮放操作} }手勢(shì)操作的實(shí)現(xiàn)均是根據(jù)Unity提供的Input的TouchPhase來判斷狀態(tài),然后三個(gè)主要功能Joystick、Rotate和Scale根據(jù)其狀態(tài)和Input的Position變換等各種屬性來進(jìn)行操作。
下面分別列出三個(gè)主要功能,各自單獨(dú)的具體代碼實(shí)現(xiàn)。
Joystick
Joystick的實(shí)現(xiàn)原理是記錄觸摸點(diǎn)第一幀的初始位置,顯示操縱桿背景圖。然后根據(jù)之后觸摸的位置與初始位置間的相對(duì)位移計(jì)算出偏移量,即是主角需要使用的位移值,顯示操縱桿。最后當(dāng)觸摸停止時(shí)候清空數(shù)據(jù)。
public GameObject m_OnPad;// Joystick的GameObject public Image m_Bottom;// 背景圖片 public Image m_Stick;// 操縱桿圖片private Vector3 m_BeginPos = Vector2.zero;// Joystick初始化位置 private Vector2 m_CenterPos = Vector2.zero; private Vector3 m_Dir = Vector3.zero;// 最后計(jì)算相對(duì)的偏移距離,主角需要使用的位移值private float m_DisLimit = 1.0f;// 用于限定操縱桿在一定范圍內(nèi) private int m_MoveFingerId = -1; private bool m_HasMove = false;protected virtual void Start() {m_DisLimit = m_Bottom.rectTransform.sizeDelta.x / 2; }protected void AroundByMobileInput() {if (Input.touchCount > 0 && Input.touchCount <= 2){for (int i = 0; i < Input.touchCount; i++){if (Input.touches[i].phase == TouchPhase.Began){if (!EventSystem.current.IsPointerOverGameObject(Input.touches[i].fingerId)){m_MoveFingerId = Input.touches[i].fingerId;m_BeginPos = Input.touches[i].rawPosition;showJoyStick();}}else if (Input.touches[i].phase == TouchPhase.Moved || Input.touches[i].phase == TouchPhase.Stationary){if (Input.touches[i].fingerId == m_MoveFingerId){setStickCenterPos(Input.touches[i].position);}}else if (Input.touches[i].phase == TouchPhase.Canceled || Input.touches[i].phase == TouchPhase.Ended){if (Input.touches[i].fingerId == m_MoveFingerId){hideJoyStick();m_MoveFingerId = -1;}}}} }Vector2 convertTouchPosToUIPos(Vector2 touchPosition) {Vector2 localPoint;RectTransformUtility.ScreenPointToLocalPointInRectangle(m_OnPad.transform as RectTransform, touchPosition, null, out localPoint);return localPoint; }void showJoyStick() {m_Bottom.gameObject.SetActive(true);m_Stick.gameObject.SetActive(true);m_OnPad.SetActive(true);m_CenterPos = convertTouchPosToUIPos(m_BeginPos);m_Bottom.rectTransform.localPosition = m_CenterPos;m_Stick.rectTransform.localPosition = m_CenterPos;m_Dir.x = 0;m_Dir.z = 0;m_Dir.Normalize(); }void hideJoyStick() {m_OnPad.SetActive(false);m_Bottom.rectTransform.localPosition = Vector2.zero;m_Stick.rectTransform.localPosition = Vector2.zero;m_Dir.x = 0;m_Dir.z = 0;m_Dir.Normalize(); }void setStickCenterPos(Vector2 touch) {Vector2 pos = convertTouchPosToUIPos(touch);float dis = Vector2.Distance(pos, m_CenterPos);if (dis > m_DisLimit){Vector2 dir = pos - m_CenterPos;dir.Normalize();dir * = m_DisLimit;pos = m_CenterPos + dir;}m_Stick.transform.localPosition = pos;m_Dir.x = pos.x - m_CenterPos.x;m_Dir.z = pos.y - m_CenterPos.y;m_Dir.Normalize(); }Rotate
Rotate同理,也是根據(jù)觸摸點(diǎn)第一幀的初始位置,與之后的位置的相對(duì)位移來計(jì)算相機(jī)的旋轉(zhuǎn)量。
public Vector2 CurrentAngles; private Vector2 targetAngles;private int m_RotateFingerId = -1; private Vector2 m_PreRotatePos;// 用于保存上一幀手指的觸碰位置 private bool m_HasRotate = false;protected virtual void Start() {CurrentAngles = targetAngles = transform.eulerAngles; }protected void AroundByMobileInput() {if (Input.touchCount > 0 && Input.touchCount <= 2){for (int i = 0; i < Input.touchCount; i++){if (Input.touches[i].phase == TouchPhase.Began){if (!EventSystem.current.IsPointerOverGameObject(Input.touches[i].fingerId)){if (!m_HasRotate){m_RotateFingerId = Input.touches[i].fingerId;m_PreRotatePos = Input.touches[i].position;m_HasRotate = true;}}}else if (Input.touches[i].phase == TouchPhase.Moved || Input.touches[i].phase == TouchPhase.Stationary){if (Input.touches[i].fingerId == m_RotateFingerId){Vector2 delta = Input.touches[i].position - m_PreRotatePos;targetAngles.y += delta.x;targetAngles.x -= delta.y;//Range.//targetAngles.x = Mathf.Clamp(targetAngles.x, angleRange.min, angleRange.max);m_PreRotatePos = Input.touches[i].position;}}else if (Input.touches[i].phase == TouchPhase.Canceled || Input.touches[i].phase == TouchPhase.Ended){if (Input.touches[i].fingerId == m_RotateFingerId){m_RotateFingerId = -1;m_HasRotate = false;}}}}//Lerp.CurrentAngles = Vector2.Lerp(CurrentAngles, targetAngles, Time.deltaTime);//Update transform position and rotation.transform.rotation = Quaternion.Euler(CurrentAngles); }Scale
Scale則是根據(jù)兩根手指觸碰的距離與初始距離的相對(duì)大小來計(jì)算相機(jī)的位移。
public Transform target;// 主角public float CurrentDistance; private float targetDistance;private bool m_IsSingleFinger = true;private Vector2 oldPosition1;// 記錄上一次手機(jī)觸摸位置判斷用戶是在左放大還是縮小手勢(shì) private Vector2 oldPosition2;protected virtual void Start() {CurrentDistance = targetDistance = Vector3.Distance(transform.position, target.position); }protected void AroundByMobileInput() {if (Input.touchCount == 1){m_IsSingleFinger = true;}if (Input.touchCount > 1){// 計(jì)算出當(dāng)前兩點(diǎn)觸摸點(diǎn)的位置 if (m_IsSingleFinger){oldPosition1 = Input.GetTouch(0).position;oldPosition2 = Input.GetTouch(1).position;}if (Input.touches[0].phase == TouchPhase.Moved && Input.touches[1].phase == TouchPhase.Moved){var tempPosition1 = Input.GetTouch(0).position;var tempPosition2 = Input.GetTouch(1).position;float currentTouchDistance = Vector3.Distance(tempPosition1, tempPosition2);float lastTouchDistance = Vector3.Distance(oldPosition1, oldPosition2);// 計(jì)算上次和這次雙指觸摸之間的距離差距 // 然后去更改攝像機(jī)的距離 targetDistance -= (currentTouchDistance - lastTouchDistance) * Time.deltaTime * mouseSettings.wheelSensitivity;// 備份上一次觸摸點(diǎn)的位置,用于對(duì)比 oldPosition1 = tempPosition1;oldPosition2 = tempPosition2;m_IsSingleFinger = false;}}//Range//targetDistance = Mathf.Clamp(targetDistance, distanceRange.min, distanceRange.max);CurrentDistance = Mathf.Lerp(CurrentDistance, targetDistance, Time.deltaTime);transform.position = target.position - transform.forward * CurrentDistance; }問題解決
三個(gè)主要功能的邏輯很簡潔,但是編寫過程中也遇到了一些問題,一些是實(shí)現(xiàn)單獨(dú)功能方面的,一些是合并的時(shí)候各自操作會(huì)沖突的。
1.
手勢(shì)操作需要兼容手指觸碰和外設(shè)手柄。但是在使用外設(shè)手柄的時(shí)候有時(shí)候遇到Joystick卡住的問題,如圖1
?
圖1. 使用外設(shè)手柄Joystick卡住
原因是在外設(shè)手柄或虛擬按鍵的時(shí)候,遙感的觸碰事件初始獲取的一定是設(shè)定的虛擬區(qū)域的中心位置,由于之前使用m_BeginPos = Input.touches[i].position獲取的是當(dāng)前觸摸的位置,當(dāng)搖桿移動(dòng)過快時(shí),導(dǎo)致第一次獲取的位置并不是虛擬區(qū)域的中心。把此點(diǎn)初始化成了搖桿的中心,而虛擬遙感的移動(dòng)位置無法超過虛擬區(qū)域,造成遙感被卡住。如圖2。
?
圖2. 外設(shè)手柄的遙感可移動(dòng)范圍
而Unity提供了另外一個(gè)方法Input.touches.rawPosition,這個(gè)方法獲取的是觸摸事件的初始值。則更改初始化位置的代碼為m_BeginPos = Input.touches[i].rawPosition;之后,再使用外設(shè)手柄的時(shí)候,Joystick的初始化位置就固定為手機(jī)設(shè)定的初始位置。問題解決,如圖3。
?
圖3. 使用外設(shè)手柄出現(xiàn)的問題被解決
rawPosition是觸摸事件的初始位置,而position是當(dāng)前移動(dòng)到的位置。
2.
手勢(shì)操作與UI布局有時(shí)會(huì)產(chǎn)生沖突,如圖4。
?
圖4. 手勢(shì)操作與UI布局沖突
針對(duì)這個(gè)問題,Unity已提供很好的解決方法。Unity提供了EventSystem.IsPointerOverGameObject來判斷手勢(shì)操作是否點(diǎn)擊到了UI上。直接在所有手勢(shì)初始化操作的時(shí)候增加一個(gè)判斷即可:
if (!EventSystem.current.IsPointerOverGameObject(Input.touches[i].fingerId))EventSystem.current.IsPointerOverGameObject方法針對(duì)的是Unity提供UGUI的,并且需要勾選組件上的Raycast Target才能生效。
修改后點(diǎn)擊按鈕與手勢(shì)操作不再?zèng)_突,如圖5。
?
圖5. 手勢(shì)操作與UI布局不再?zèng)_突
3.
Rotate操作最初使用的是Input.GetAxis接口來判斷相機(jī)的旋轉(zhuǎn)角度targetAngles。這樣做會(huì)導(dǎo)致Joystick和Rotate操作會(huì)沖突,如圖6。
?
圖6. Joystick和Rotate操作沖突
原因是,Input.GetAxis接口并沒有細(xì)化到是第幾個(gè)手指觸碰,所以如果同時(shí)有兩個(gè)手指觸碰屏幕的時(shí)候會(huì)產(chǎn)生沖突。
因此將Input.GetAxis接口換成Input.touches的操作即可。 具體代碼如下:
Input.touches可以根據(jù)touches[i]判斷是觸碰點(diǎn)的位置和順序,然后根據(jù)ID和順序限制觸碰點(diǎn)只能單獨(dú)進(jìn)行Joystick操作或者Rotate操作,根據(jù)Input.touches.position的變換來計(jì)算旋轉(zhuǎn)角度。更改后如圖7,問題解決。
?
圖7. Joystick和Rotate操作不再?zèng)_突
4.
Joystick操作中,操作桿的位置需要限定在背景圖的范圍之內(nèi)。
?
圖8. 操作桿的移動(dòng)范圍
解決該問題使用Unity提供的接口RectTransformUtility.ScreenPointToLocalPointInRectangle,該方法是將屏幕空間上的點(diǎn)轉(zhuǎn)換為RectTransform的局部空間中位于其矩形平面上的位置。
其中m_OnPad傳入是Joystick的背景GameObject,touchPosition是觸碰的位置,即將當(dāng)前Joystick的觸碰位置傳入,轉(zhuǎn)換成背景圖的RectTransform局部空間的坐標(biāo),代碼如下:
Vector2 convertTouchPosToUIPos(Vector2 touchPosition) {Vector2 localPoint;RectTransformUtility.ScreenPointToLocalPointInRectangle(m_OnPad.transform as RectTransform, touchPosition, null, out localPoint);return localPoint; }然后,在顯示Joystick的操作桿位置的時(shí)候,然后限定局部坐標(biāo)的位置與中心的距離,就可以將操作桿設(shè)置在圓盤的范圍內(nèi)。m_CenterPos是觸碰第一幀的時(shí)候記錄的初始化坐標(biāo),也需要轉(zhuǎn)換成局部坐標(biāo);m_DisLimit是限定局部坐標(biāo)的位置與中心的距離,需要初始化,具體代碼如下:
void setStickCenterPos(Vector2 touchPosition) {Vector2 pos = convertTouchPosToUIPos(touchPosition);float dis = Vector2.Distance(pos, m_CenterPos);if (dis > m_DisLimit)//{Vector2 dir = pos - m_CenterPos;dir.Normalize();dir * = m_DisLimit;pos = m_CenterPos + dir;}m_Stick.transform.localPosition = pos; }5.
Scale與Joystick和Rotate沖突,即在兩個(gè)手指同時(shí)操作的情況下,三個(gè)操作同時(shí)進(jìn)行,如圖9。
?
圖9. Scale與Joystick和Rotate沖突
而解決問題很簡單,則是考慮到需求,將需要雙指操作的Scale操作和兩個(gè)單指操作的Joystick移動(dòng)和Rotate區(qū)分開來即可。原理是使用一個(gè)計(jì)時(shí)器記錄兩個(gè)觸碰點(diǎn)的間隔時(shí)間,如果這個(gè)時(shí)間超過了0.1s,則禁止Scale操作;同樣如果在Scale操作的時(shí)候也禁止Joystick和Rotate操作。
偽代碼如下:
bool m_Timer = false;// 判斷計(jì)時(shí)器開始與否 float m_IntervalTime = 0;// 記錄間隔時(shí)間 bool canScale = false;// 判斷能否進(jìn)行Scale操作void AroundByMobileInput() {if (Input.touchCount > 0 && Input.touchCount <= 2){for (int i = 0; i < Input.touchCount; i++){if (Input.touches[i].phase == TouchPhase.Began){if (i == 0){m_Timer = true;m_IntervalTime = 0;}else if (i == 1){m_Timer = false;}初始化Joystick初始化Rotate}else if (Input.touches[i].phase == TouchPhase.Moved || Input.touches[i].phase == TouchPhase.Stationary){根據(jù)ID來執(zhí)行相應(yīng)操作,Joystick還是Rotate操作if (m_Timer){m_IntervalTime += Time.unscaledDeltaTime;}}else if (Input.touches[i].phase == TouchPhase.Canceled || Input.touches[i].phase == TouchPhase.Ended){同樣根據(jù)ID來執(zhí)行最后的收尾工作}}}if (m_IntervalTime < 0.1f){canScale = true;}else{canScale = false;}if (canScale){雙指縮放操作} }同時(shí),如果需要的話可以更進(jìn)一步限制雙指Scale操作只在屏幕右半邊才有效,這樣只需在初始化操作的時(shí)候進(jìn)行判斷即可
if (Input.touches[i].position.x > Screen.width / 2)修改之后問題解決,如圖10&11。
?
圖10. Scale與Joystick和Rotate不再?zèng)_突
?
圖11. 限定Scale操作在右半邊
6.
在上文功能的具體代碼中可看到,為每種操作都記錄相應(yīng)的Input.touches[i].fingerId,目的是以便合并的時(shí)候下幀使用時(shí)可以獲取相同的操作的觸摸點(diǎn)。
總結(jié)
以上是生活随笔為你收集整理的Unity Joystick手势操作的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2022飞鱼科技-鱼苗夏令营实习-游戏客
- 下一篇: stm32固件库(STM32F10x标准