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

在本文发框,筆者將簡(jiǎn)單絮叨絮叨如何做一個(gè)代碼極簡(jiǎn)但功能完善的基于 UGUI 的搖桿組件。

前言:

筆者需要一個(gè)搖桿咕痛,找了幾個(gè)別人寫好的輪子,感覺(jué)不怎么好用喇嘱,那就練練手寫一個(gè)唄茉贡。

需求:

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

分析:

  1. 根據(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è)置。
  2. 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谱姓。
  3. 搖柄動(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)色向量表示蝶桶。
  4. 那怎么限制搖柄被拖拽的最遠(yuǎn)距離呢慨绳?
    答: 上面已經(jīng)分析了,藍(lán)色向量就是搖桿 Handle 的局部坐標(biāo)。我們把藍(lán)色向量的長(zhǎng)度限制住脐雪,然后賦值回去不就OK啦郭蕉。

  5. 上圖中有一個(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è)

更新:

  1. 修復(fù)坐標(biāo)轉(zhuǎn)換時(shí)z軸未正確換算的問(wèn)題。
  2. 不會(huì)被多個(gè)手指誤觸厉碟。
  3. 新增驅(qū)動(dòng)小玩偶示例喊巍,使用三種方法控制其運(yùn)動(dòng)
  4. 整理目錄,完善第一人稱Demo箍鼓,剝離指向器為可選組件崭参,新增動(dòng)態(tài)/靜態(tài)搖桿功能。 - 更新 2019年11月28日

鏈接:

Unity UGUI Joystick

結(jié)語(yǔ):

  • 由于是使用 UGUI 做的款咖,所以直接能在移動(dòng)端/觸控屏上使用阵翎。
  • 觸屏設(shè)備支持多個(gè)搖桿同時(shí)搞事情。
  • Canvas 所有的Render Mode下均能正常使用之剧。
  • 轉(zhuǎn)載請(qǐng)注明出處郭卫,謝謝!

希望對(duì)大家有所幫助背稼,喜歡本文記得給個(gè)贊喲贰军!謝謝~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子词疼,更是在濱河造成了極大的恐慌俯树,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贰盗,死亡現(xiàn)場(chǎng)離奇詭異许饿,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)舵盈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門陋率,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人秽晚,你說(shuō)我怎么就攤上這事瓦糟。” “怎么了赴蝇?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵菩浙,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我句伶,道長(zhǎng)劲蜻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任考余,我火速辦了婚禮斋竞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘秃殉。我一直安慰自己坝初,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布钾军。 她就那樣靜靜地躺著鳄袍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吏恭。 梳的紋絲不亂的頭發(fā)上拗小,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音樱哼,去河邊找鬼哀九。 笑死,一個(gè)胖子當(dāng)著我的面吹牛搅幅,可吹牛的內(nèi)容都是我干的阅束。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼茄唐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼息裸!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤呼盆,失蹤者是張志新(化名)和其女友劉穎年扩,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體访圃,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡厨幻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腿时。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片况脆。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖圈匆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捏雌,我是刑警寧澤跃赚,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站性湿,受9級(jí)特大地震影響纬傲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肤频,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一叹括、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宵荒,春花似錦汁雷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至暑刃,卻和暖如春厢漩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背岩臣。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工溜嗜, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人架谎。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓炸宵,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親谷扣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子焙压,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 1、UGUI概述 1.1、Unity界面發(fā)展史 【老版本界面onGUI】=>【GUI插件NGUI】=>【新版本界面...
    兜兜_2925閱讀 27,418評(píng)論 2 23
  • Canvas 渲染順序 遵循刷油漆規(guī)則(畫家算法) 依次由Render CameraDepth值涯曲、Sorting ...
    沉麟閱讀 1,476評(píng)論 0 0
  • 一野哭、Unity3D中有哪些坐標(biāo)系? 坐標(biāo)系這個(gè)概念最早是由法國(guó)數(shù)學(xué)家笛卡爾提出的幻件,并由此創(chuàng)造了用代數(shù)方法來(lái)研究幾何...
    OneMore2018閱讀 4,573評(píng)論 0 7
  • 我之前在網(wǎng)上看過(guò)一個(gè)插件叫做出JScolor 顏色拾取器 說(shuō)白了就是通過(guò)1*1PX的DOM設(shè)置顏色值通過(guò)JS來(lái)獲...
    凡凡的小web閱讀 1,257評(píng)論 0 0
  • 17年7月绰沥,high轉(zhuǎn)江西篱蝇。 第一站,江州徽曲。 江州是名副其實(shí)的-江中之州零截。萬(wàn)里湖光,千帆不盡秃臣。 潯陽(yáng)江邊...
    納納小姐閱讀 546評(píng)論 4 2