在本文发框,筆者將簡(jiǎn)單絮叨絮叨如何做一個(gè)代碼極簡(jiǎn)但功能完善的基于 UGUI 的搖桿組件。
前言:
筆者需要一個(gè)搖桿咕痛,找了幾個(gè)別人寫好的輪子,感覺(jué)不怎么好用喇嘱,那就練練手寫一個(gè)唄茉贡。
需求:
- 在一定范圍內(nèi)都能觸發(fā)搖桿。
- 在觸發(fā)區(qū)域按下后者铜,搖桿 (方位盤+搖柄) 展示出來(lái)腔丧。
- 拖拽鼠標(biāo),搖桿跟隨作烟,且驅(qū)動(dòng)方位指示器愉粤。
- 要支持設(shè)置搖桿可用的軸(僅激活 x/y 軸 OR 全部激活)。
- 要有搖桿底盤固定/動(dòng)態(tài)一鍵切換的功能拿撩。(2019.11新增的需求)
分析:
-
根據(jù)需求科汗,我們使用 UGUI搭建一個(gè)這樣的UI架構(gòu):
- Joystick 用來(lái)監(jiān)聽(tīng) UGUI 光標(biāo)事件,也就限制了搖桿范圍绷雏。
- Backgroud 作為Handle 和 Direction的父節(jié)點(diǎn)(容器)头滔,使得 Handle 和 Direction 更方便運(yùn)算和控制狀態(tài)。
- Direction 切圖切成了這樣涎显,所以將 Pivot 手動(dòng)拖到了 BackGround 中心坤检。如切圖到位就不用設(shè)置。
-
UI搭好了期吓,該如何驅(qū)動(dòng)它們呢早歇?
答: 很簡(jiǎn)單,只需要繼承以下幾個(gè)接口就好啦:- IPointerDownHandler - 當(dāng)鼠標(biāo)按下時(shí)讨勤,更新?lián)u桿 (BackGround 游戲?qū)ο? 的位置
- IDragHandler - 當(dāng)鼠標(biāo)拖拽時(shí)箭跳,更新 Handle 位置(其實(shí)挺有意思的,拖拽到某一刻的世界坐標(biāo)減去按下時(shí)的坐標(biāo)就是 Handle 本地坐標(biāo))
- IPointerUpHandler - 當(dāng)鼠標(biāo)釋放時(shí)潭千,復(fù)位 BackGround 和 Handle谱姓。
-
搖柄動(dòng)起來(lái)了,可是我們?cè)趺打?qū)動(dòng)其他游戲?qū)ο筮\(yùn)動(dòng)呢刨晴?
答:在每一幀屉来,通過(guò)OnValueChanged
事件向注冊(cè)了該事件的游戲?qū)ο蠓职l(fā)搖桿相對(duì)于 BackGround 的偏移量以驅(qū)動(dòng)這些游戲?qū)ο筮\(yùn)動(dòng)。
這個(gè)偏移量實(shí)際上也就是 Handle 在 BackGround 游戲?qū)ο笾械木植孔鴺?biāo)狈癞,如下圖藍(lán)色向量:
告訴你什么叫靈魂畫手- BackGround位置由 紅色向量表示茄靠。
- Handle 位置由綠色向量表示。
- Handle 偏移量由藍(lán)色向量表示蝶桶。
-
那怎么限制搖柄被拖拽的最遠(yuǎn)距離呢慨绳?
答: 上面已經(jīng)分析了,藍(lán)色向量就是搖桿 Handle 的局部坐標(biāo)。我們把藍(lán)色向量的長(zhǎng)度限制住脐雪,然后賦值回去不就OK啦郭蕉。
-
上圖中有一個(gè) 黃色的方向指示器,怎么同步它呢 喂江?
答: 在本例做好 Pivot 設(shè)置,然后設(shè)置 localEulerAngles 的 z 軸就好啦旁振。
如果指示器切圖的中心與 BackGround 重合获询,Pivot 都不需要設(shè)置。
演示:
代碼:
以下代碼已經(jīng)不是最新的了,請(qǐng)挪步文末 Github 獲取工程體驗(yàn)更佳拐袜!
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;
namespace zFrame.UI
{
public class Joystick : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
public float maxRadius = 100; //Handle 移動(dòng)最大半徑
public JoystickEvent OnValueChanged = new JoystickEvent(); //事件
[System.Serializable] public class JoystickEvent : UnityEvent<Vector2> { }
private RectTransform backGround, handle,direction; //搖桿背景吉嚣、搖桿手柄、方向指引
private Vector2 joysticValue = Vector2.zero;
public bool IsDraging { get; private set; }
private void Awake()
{
backGround = transform.Find("BackGround") as RectTransform;
handle = transform.Find("BackGround/Handle") as RectTransform;
direction = transform.Find("BackGround/Direction") as RectTransform;
direction.gameObject.SetActive(false);
}
void Update()
{
if (IsDraging) //搖桿拖拽進(jìn)行時(shí)驅(qū)動(dòng)事件
{
joysticValue.x = handle.anchoredPosition.x / maxRadius;
joysticValue.y = handle.anchoredPosition.y / maxRadius;
OnValueChanged.Invoke(joysticValue);
}
}
//按下時(shí)同步搖桿位置
void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
{
Vector3 backGroundPos = new Vector3() // As it is too long for trinocular operation so I create Vector3 like this.
{
x = eventData.position.x,
y = eventData.position.y,
z = (null == eventData.pressEventCamera) ? backGround.position.z :
eventData.pressEventCamera.WorldToScreenPoint(backGround.position).z //無(wú)奈蹬铺,這個(gè)坐標(biāo)轉(zhuǎn)換不得不做啊,就算來(lái)來(lái)回回的折騰尝哆。
};
backGround.position = (null == eventData.pressEventCamera)?backGroundPos : eventData.pressEventCamera.ScreenToWorldPoint(backGroundPos);
//Vector3 vector;
//if (RectTransformUtility.ScreenPointToWorldPointInRectangle(transform as RectTransform, eventData.position, eventData.pressEventCamera, out vector))
//{
// backGround.position = vector;
//}
IsDraging = true;
}
// 當(dāng)鼠標(biāo)拖拽時(shí)
void IDragHandler.OnDrag(PointerEventData eventData)
{
Vector2 backGroundPos = (null == eventData.pressEventCamera) ?
backGround.position : eventData.pressEventCamera.WorldToScreenPoint(backGround.position);
Vector2 direction = eventData.position - backGroundPos; //得到方位盤中心指向光標(biāo)的向量
float distance = Vector3.Magnitude(direction); //獲取向量的長(zhǎng)度
float radius = Mathf.Clamp(distance, 0, maxRadius); //鎖定 Handle 半徑
handle.localPosition = direction.normalized * radius; //更新 Handle 位置
UpdateDirectionArrow(direction);
//Vector2 vector;
//if (RectTransformUtility.ScreenPointToLocalPointInRectangle(backGround, eventData.position, eventData.pressEventCamera, out vector))
//{
//float distance = Vector3.Magnitude(vector); //獲取向量的長(zhǎng)度
//float radius = Mathf.Clamp(distance, 0, maxRadius); //鎖定 Handle 半徑
//handle.localPosition = vector.normalized * radius; //更新 Handle 位置
//UpdateDirectionArrow(vector);
//}
}
//更新指向器的朝向
private void UpdateDirectionArrow(Vector2 position)
{
if (position.x!=0||position.y!=0)
{
direction.gameObject.SetActive(true);
direction.localEulerAngles= new Vector3 (0,0,Vector2.Angle(Vector2.right,position)*(position.y>0?1:-1));
}
}
// 當(dāng)鼠標(biāo)停止拖拽時(shí)
void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
{
direction.gameObject.SetActive(false);
backGround.localPosition = Vector3.zero;
handle.localPosition = Vector3.zero;
IsDraging = false;
}
}
}
- 需要注意的是 RectTransform.positon/localPositon 是受 Pivot 影響的。所以本例中的 BackGround 甜攀、Handle 的 Pivot 均為 (0.5秋泄,0.5).
- 因?yàn)?Canvas 有 3 個(gè)渲染模式,所以為了適配這三個(gè)模式规阀,在非 Overlay 模式下恒序,必須進(jìn)行坐標(biāo)轉(zhuǎn)換。
-
借助 RectTransformUtility.ScreenPointToLocalPoint(WorldPoint)InRectangle(...) 可以做到不需要我們自己寫坐標(biāo)轉(zhuǎn)換,但注釋掉了利用Utility 處理的那部分代碼谁撼。因?yàn)槠湫氏鄬?duì)低歧胁,原由如下:
用在本例則太多不必要的射線檢測(cè)
更新:
- 修復(fù)坐標(biāo)轉(zhuǎn)換時(shí)z軸未正確換算的問(wèn)題。
- 不會(huì)被多個(gè)手指誤觸厉碟。
- 新增驅(qū)動(dòng)小玩偶示例喊巍,使用三種方法控制其運(yùn)動(dòng)
- 整理目錄,完善第一人稱Demo箍鼓,剝離指向器為可選組件崭参,新增動(dòng)態(tài)/靜態(tài)搖桿功能。 - 更新 2019年11月28日
鏈接:
結(jié)語(yǔ):
- 由于是使用 UGUI 做的款咖,所以直接能在移動(dòng)端/觸控屏上使用阵翎。
- 觸屏設(shè)備支持多個(gè)搖桿同時(shí)搞事情。
- Canvas 所有的Render Mode下均能正常使用之剧。
- 轉(zhuǎn)載請(qǐng)注明出處郭卫,謝謝!
希望對(duì)大家有所幫助背稼,喜歡本文記得給個(gè)贊喲贰军!謝謝~