[Unity 3d] 使用UGUI做一個類似王者榮耀的搖桿

在本文,筆者將簡單絮叨絮叨如何做一個代碼極簡但功能完善的基于 UGUI 的搖桿組件。

前言:

筆者需要一個搖桿,找了幾個別人寫好的輪子,感覺不怎么好用,那就練練手寫一個唄。

需求:

  1. 在一定范圍內都能觸發(fā)搖桿。
  2. 在觸發(fā)區(qū)域按下后,搖桿 (方位盤+搖柄) 展示出來。
  3. 拖拽鼠標,搖桿跟隨,且驅動方位指示器。
  4. 要支持設置搖桿可用的軸(僅激活 x/y 軸 OR 全部激活)。
  5. 要有搖桿底盤固定/動態(tài)一鍵切換的功能。(2019.11新增的需求)

分析:

  1. 根據需求,我們使用 UGUI搭建一個這樣的UI架構:


    • Joystick 用來監(jiān)聽 UGUI 光標事件,也就限制了搖桿范圍。
    • Backgroud 作為Handle 和 Direction的父節(jié)點(容器),使得 Handle 和 Direction 更方便運算和控制狀態(tài)。
    • Direction 切圖切成了這樣,所以將 Pivot 手動拖到了 BackGround 中心。如切圖到位就不用設置。
  2. UI搭好了,該如何驅動它們呢?
    答: 很簡單,只需要繼承以下幾個接口就好啦:

    • IPointerDownHandler - 當鼠標按下時,更新搖桿 (BackGround 游戲對象) 的位置
    • IDragHandler - 當鼠標拖拽時,更新 Handle 位置(其實挺有意思的,拖拽到某一刻的世界坐標減去按下時的坐標就是 Handle 本地坐標)
    • IPointerUpHandler - 當鼠標釋放時,復位 BackGround 和 Handle。
  3. 搖柄動起來了,可是我們怎么驅動其他游戲對象運動呢?
    答:在每一幀,通過OnValueChanged事件向注冊了該事件的游戲對象分發(fā)搖桿相對于 BackGround 的偏移量以驅動這些游戲對象運動。

    這個偏移量實際上也就是 Handle 在 BackGround 游戲對象中的局部坐標,如下圖藍色向量:


    告訴你什么叫靈魂畫手
    • BackGround位置由 紅色向量表示。
    • Handle 位置由綠色向量表示。
    • Handle 偏移量由藍色向量表示。
  4. 那怎么限制搖柄被拖拽的最遠距離呢?
    答: 上面已經分析了,藍色向量就是搖桿 Handle 的局部坐標。我們把藍色向量的長度限制住,然后賦值回去不就OK啦。

  5. 上圖中有一個 黃色的方向指示器,怎么同步它呢 ?
    答: 在本例做好 Pivot 設置,然后設置 localEulerAngles 的 z 軸就好啦。
    如果指示器切圖的中心與 BackGround 重合,Pivot 都不需要設置。

演示:

代碼:

以下代碼已經不是最新的了,請挪步文末 Github 獲取工程體驗更佳!

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;
namespace zFrame.UI
{
    public class Joystick : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
    {
        public float maxRadius = 100; //Handle 移動最大半徑
        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) //搖桿拖拽進行時驅動事件
            {
                joysticValue.x = handle.anchoredPosition.x / maxRadius;
                joysticValue.y = handle.anchoredPosition.y / maxRadius;
                OnValueChanged.Invoke(joysticValue);
            }
        }
        //按下時同步搖桿位置
        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 //無奈,這個坐標轉換不得不做啊,就算來來回回的折騰。
            };
             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;
        }
        // 當鼠標拖拽時
        void IDragHandler.OnDrag(PointerEventData eventData)
        {
            Vector2 backGroundPos = (null == eventData.pressEventCamera) ?
                backGround.position : eventData.pressEventCamera.WorldToScreenPoint(backGround.position);
            Vector2 direction = eventData.position - backGroundPos; //得到方位盤中心指向光標的向量
            float distance = Vector3.Magnitude(direction); //獲取向量的長度
            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); //獲取向量的長度
                //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));
            }
        }
        // 當鼠標停止拖拽時
        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).
  • 因為 Canvas 有 3 個渲染模式,所以為了適配這三個模式,在非 Overlay 模式下,必須進行坐標轉換。
  • 借助 RectTransformUtility.ScreenPointToLocalPoint(WorldPoint)InRectangle(...) 可以做到不需要我們自己寫坐標轉換,但注釋掉了利用Utility 處理的那部分代碼。因為其效率相對低,原由如下:


    用在本例則太多不必要的射線檢測

更新:

  1. 修復坐標轉換時z軸未正確換算的問題。
  2. 不會被多個手指誤觸。
  3. 新增驅動小玩偶示例,使用三種方法控制其運動
  4. 整理目錄,完善第一人稱Demo,剝離指向器為可選組件,新增動態(tài)/靜態(tài)搖桿功能。 - 更新 2019年11月28日

鏈接:

Unity UGUI Joystick

結語:

  • 由于是使用 UGUI 做的,所以直接能在移動端/觸控屏上使用。
  • 觸屏設備支持多個搖桿同時搞事情。
  • Canvas 所有的Render Mode下均能正常使用。
  • 轉載請注明出處,謝謝!

希望對大家有所幫助,喜歡本文記得給個贊喲!謝謝~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 1、UGUI概述 1.1、Unity界面發(fā)展史 【老版本界面onGUI】=>【GUI插件NGUI】=>【新版本界面...
    兜兜_2925閱讀 27,575評論 2 23
  • Canvas 渲染順序 遵循刷油漆規(guī)則(畫家算法) 依次由Render CameraDepth值、Sorting ...
    沉麟閱讀 1,661評論 0 0
  • 一、Unity3D中有哪些坐標系? 坐標系這個概念最早是由法國數學家笛卡爾提出的,并由此創(chuàng)造了用代數方法來研究幾何...
    OneMore2018閱讀 4,690評論 0 7
  • 我之前在網上看過一個插件叫做出JScolor 顏色拾取器 說白了就是通過1*1PX的DOM設置顏色值通過JS來獲...
    凡凡的小web閱讀 1,334評論 0 0
  • 17年7月,high轉江西。 第一站,江州。 江州是名副其實的-江中之州。萬里湖光,千帆不盡。 潯陽江邊...
    納納小姐閱讀 695評論 4 4

友情鏈接更多精彩內容