unity不规则点击_【Unity游戏开发】UGUI不规则区域点击的实现
一、簡介
馬三從上一家公司離職了,最近一直在出去面試,忙得很,所以這一篇博客拖到現在才寫出來。馬三在上家公司工作的時候,曾處理了一個UGUI不規則區域點擊的問題,制作過程中也有一些收獲和需要注意坑,因此記錄成博客與大家分享。眾所周知在UGUI中,響應點擊通常是依附在一張圖片上的,而圖片不管美術怎么給你切,導進Unity之后都是一個矩形,如果要做其他形狀,最多只能旋轉一下,或者自己做一些處理。而為了美術效果,很多時候我們不得不需要特定形狀的UI,并且讓它們實現精準的響應點擊。例如下圖就是一個不規則的點擊區域。
圖1:UGUI不規則點擊區域示意圖
下面是處理了不規則區域點擊后的演示效果,當點擊按鈕的時候,會對點擊次數進行累加并且打印到控制臺。可以看到進行了不規則區域點擊處理以后,對我們原來的普通矩形Sprite的點擊不會產生到影響,而不規則區域的表現效果也符合我們的預期。
圖2:規則區域與不規則區域點擊效果對比
二、針對UGUI不規則區域點擊的兩種處理方法
針對UGUI的不規則區域響應點擊,一般來說有兩種處理辦法:
1.精靈像素檢測:該方法是指通過讀取精靈(Sprite)在某一點的像素值(RGBA),如果該點的像素值中的Alpha小于一定的閾值(比如0.5)則表示該點處是透明的,即用戶點擊的位置在精靈邊界以外,否則用戶點擊的位置在精靈邊界內部。
2.通過算法計算碰撞區域:通過一定的算法,手動計算出碰撞區域,然后在判斷用戶是點擊在了精靈上面,還是點擊在精靈外部。
1.精靈像素檢測法
首先來說下精靈像素檢測法,因為它實現起來比較簡單也好理解。uGUI在處理控件是否被點擊的時候,主要是根據IsRaycastLocationValid這個方法的返回值來進行判斷的,而這個方法用到的基本原理則是判斷指定點對應像素的RGBA數值中的Alpha是否大于某個指定臨界值。例如,我們知道半透明通常是指Alpha=0.5,而對一個后綴名為png格式的圖片來說半透明或者完全透明的區域理論上不應該被響應的,所以根據這個原理,我們只需要設定一個透明度的臨界值,然后對當前鼠標位置對應的像素進行判斷就可以了,因此這種方法叫做精靈像素檢測。對于上面的這個IsRaycastLocationValid接口,我們可以通過下載UGUI源碼或者反編譯的方式看到它的實現:
1 public virtual boolIsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)2 {3 //當透明度>=1.0時,表示點擊在可響應區域返回true
4 if(this.m_EventAlphaThreshold >=1f){5 return true;6 }7
8 //當沒有指定精靈時返回true,因為不指定Spirte的時候,Unity將其區域填充為默認的白色,全部區域都是可以響應點擊的
9 Sprite overrideSprite = this.overrideSprite;10 if(overrideSprite == null){11 return true;12 }13
14 //坐標系轉換
15 Vector2 local;16 RectTransformUtility.ScreenPointToLocalPointInRectangle(base.rectTransform, screenPoint, eventCamera, reflocal);17 Rect pixelAdjustedRect = base.GetPixelAdjustedRect ();18 local.x += base.rectTransform.get_pivot ().x *pixelAdjustedRect.get_width ();19 local.y += base.rectTransform.get_pivot ().y *pixelAdjustedRect.get_height ();20 local = this.MapCoordinate(local, pixelAdjustedRect);21 Rect textureRect =overrideSprite.get_textureRect ();22 Vector2 vector = new Vector2(local.x / textureRect.get_width (), local.y /textureRect.get_height ());23
24 //計算屏幕坐標對應的UV坐標
25 float num = Mathf.Lerp(textureRect.get_x (), textureRect.get_xMax (), vector.x) / (float)overrideSprite.get_texture().get_width();26 float num2 = Mathf.Lerp(textureRect.get_y (), textureRect.get_yMax (), vector.y) / (float)overrideSprite.get_texture().get_height();27 boolresult;28
29 //核心方法:像素檢測
30 try{31 result = (overrideSprite.get_texture().GetPixelBilinear(num, num2).a >= this.m_EventAlphaThreshold);32 }catch(UnityException ex){33 Debug.LogError("Using clickAlphaThreshold lower than 1 on Image whose sprite texture cannot be read." + ex.Message + "Also make sure to disable sprite packing for this sprite.", this);34 result = true;35 }36
38 returnresult;39 }
可以看到大概的思路就是經過一系列的坐標轉換之后,將一個UV坐標的Alpha值與臨界值作比較。基于這個像素這個思路我們又可以衍生出兩種解決方案,一是直接更改臨界值,二是在像素檢測的思路上進行拓展與重寫,定制我們自己的像素檢測方法。
先來看下第一種直接更改閾值的方法,Unity在Image組件中為我們暴露出了一條屬性alphaHitTestMinimumThreshold。關于它的含義我們可以參考Unity的官方文檔:
圖3:alphaHitTestMinimumThreshold屬性文檔
大概的意思就是點擊的時候會將該像素的Alpah值與該閾值進行比較,Alpha小于該閾值的部分的點擊事件會被忽略掉,意思也就是某一像素的Alpha只有大于設定的閾值,你才能接到響應事件。當值為1的時候,表示只有完全不透明的部分才能響應。默認值為0,即一個Image不管透明不透明的部分,都會參與事件的響應。為了能夠讓alphaHitTestMinimumThreshold這個屬性生效和工作,我們需要把Advance選項中的Read/Writeable屬性勾選上。
因此我們將alphaHitTestMinimumThreshold值設置為一個合理的范圍就可以實現不規則區域的點擊了,代碼如下:
1 usingSystem.Collections;2 usingSystem.Collections.Generic;3 usingUnityEngine;4 usingUnityEngine.UI;5
6 ///
7 ///不規則區域Button8 ///
9 [RequireComponent(typeof(RectTransform))]10 [RequireComponent(typeof(Image))]11 public classIrregulaButton : MonoBehaviour12 {13 [Tooltip("設定Sprite響應的Alpha閾值")]14 [Range(0, 0.5f)]15 public float alpahThreshold = 0.5f;16
17 private voidAwake()18 {19 var image = this.GetComponent();20 if (null !=image)21 {22 image.alphaHitTestMinimumThreshold =alpahThreshold;23 }24 }25 }
第二種基于像素檢測的解決方案是自己重寫IsRaycastLocationValid接口里面像素檢測方法,將屏幕坐標轉換為UI坐標,然后再根據Sprite的類型做一些處理,最后根據x,y坐標取出像素的Alpha值與我們的閾值進行比較,具體代碼如下:
usingUnityEngine;usingUnityEngine.UI;///
///不規則區域圖形檢測組件///
[RequireComponent(typeof(RectTransform))]
[RequireComponent(typeof(Image))]public classIrregularRaycastMask : MonoBehaviour, ICanvasRaycastFilter
{privateImage _image;privateSprite _sprite;
[Tooltip("設定Sprite響應的Alpha閾值")]
[Range(0, 0.5f)]public float alpahThreshold = 0.5f;voidStart()
{
_image= GetComponent();
}///
///重寫IsRaycastLocationValid接口///
///
///
///
public boolIsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
_sprite=_image.sprite;var rectTransform =(RectTransform)transform;
Vector2 localPositionPivotRelative;
RectTransformUtility.ScreenPointToLocalPointInRectangle((RectTransform)transform, sp, eventCamera,outlocalPositionPivotRelative);//轉換為以屏幕左下角為原點的坐標系
var localPosition = new Vector2(localPositionPivotRelative.x + rectTransform.pivot.x *rectTransform.rect.width,
localPositionPivotRelative.y+ rectTransform.pivot.y *rectTransform.rect.height);var spriteRect =_sprite.textureRect;var maskRect =rectTransform.rect;var x = 0;var y = 0;//轉換為紋理空間坐標
switch(_image.type)
{caseImage.Type.Sliced:
{var border =_sprite.border;//x 軸裁剪
if (localPosition.x
{
x= Mathf.FloorToInt(spriteRect.x +localPosition.x);
}else if (localPosition.x > maskRect.width -border.z)
{
x= Mathf.FloorToInt(spriteRect.x + spriteRect.width - (maskRect.width -localPosition.x));
}else{
x= Mathf.FloorToInt(spriteRect.x + border.x +((localPosition.x- border.x) /(maskRect.width- border.x - border.z)) *(spriteRect.width- border.x -border.z));
}//y 軸裁剪
if (localPosition.y
{
y= Mathf.FloorToInt(spriteRect.y +localPosition.y);
}else if (localPosition.y > maskRect.height -border.w)
{
y= Mathf.FloorToInt(spriteRect.y + spriteRect.height - (maskRect.height -localPosition.y));
}else{
y= Mathf.FloorToInt(spriteRect.y + border.y +((localPosition.y- border.y) /(maskRect.height- border.y - border.w)) *(spriteRect.height- border.y -border.w));
}
}break;caseImage.Type.Simple:default:
{//轉換為統一UV空間
x = Mathf.FloorToInt(spriteRect.x + spriteRect.width * localPosition.x /maskRect.width);
y= Mathf.FloorToInt(spriteRect.y + spriteRect.height * localPosition.y /maskRect.height);
}break;
}//如果texture導入過程報錯,則刪除組件
try{return _sprite.texture.GetPixel(x, y).a >alpahThreshold;
}catch(UnityException e)
{
Debug.LogError("Mask texture not readable, set your sprite to Texture Type 'Advanced' and check 'Read/Write Enabled'" +e.Message);
Destroy(this);return false;
}
}
}
最后為了驗證我們的組件是否生效,可以在按鈕上掛載一個ButtonClickCounter?腳本,當接收到點擊事件的時候,記錄點擊次數并打印到控制臺方便觀察,具體代碼如下:
1 usingSystem.Collections;2 usingSystem.Collections.Generic;3 usingUnityEngine;4 usingUnityEngine.UI;5
6 ///
7 ///按鈕點擊次數計數器8 ///
9 public classButtonClickCounter : MonoBehaviour10 {11 private int count = 0;12 private stringbtnName;13
14 voidStart()15 {16 var text = this.transform.Find("Text").GetComponent();17 btnName =text.text;18 }19
20
21 public voidClick()22 {23 count++;24 Debug.Log(string.Format("{0}點擊了{1}次!", btnName, count));25 }26 }
我們只要簡單地直接把組件掛載到Image上面便可以生效了,具體截圖如下:
圖4:不規則區域檢測組件使用
2.通過算法計算碰撞區域法
對于這種實現不規則碰撞區域的方法,馬三并沒有進行深入地研究,因為馬三覺得挑選一個可靠的檢測碰撞算法不是很容易,既要考慮到它的精準性又要考慮當圖形復雜以后的計算效率,因此從易用性上面來講,不如第一種實現方案好。關于這種方法的實現和原理,馬三也是從網上搜集的一些資料進行整理的,感興趣的讀者可以深入研究一下哈,下面很多內容都是馬三搜集整理網上大神的文章的資料得來的,其中給出了許多鏈接,大家可以直接參看鏈接里面的內容。
該方法是指給精靈(Sprite)添加一個多邊形碰撞器(Rolygon Collider)組件,利用該組件來標記精靈的邊界,這樣通過比較鼠標位置和邊界可以判斷點擊是否發生在精靈內部。關于這個算法與實現,PayneQin大神已經在他的博客中做了很詳細的解析和說明,大家可以直接去看他的博客。知乎上關于判斷一個點是否在多邊形內部也有很多算法地討論,具體可以看這里。其中這篇文獻提供了判斷一個點是否在任意多邊形內部的兩種方法,分別為Corssing Number和Winding Number。這兩種方法在理論層面的相關細節請大家自行閱讀這篇文章,PayneQin大神選擇的是前者實現,其基本思想是計算從該點引出的射線與多邊形邊界相交的次數,當其為奇數時表示該點在多邊形內部,當其為偶數時表示在多邊形外部。馬三在網上找到了相關的實現(偷懶):
1 boolContainsPoint2(Vector2[] polyPoints,Vector2 p)2 {3 //統計射線和多邊形交叉次數
4 int cn = 0;5
6 //遍歷多邊形頂點數組中的每條邊
7 for(int i=0; i
10 polyPoints [i].x += transform.GetComponent().position.x;11 polyPoints [i].y += transform.GetComponent().position.y;12
13 //從當前位置發射向上向下兩條射線
14 if(((polyPoints [i].y <= p.y) && (polyPoints [i + 1].y >p.y))15 || ((polyPoints [i].y > p.y) && (polyPoints [i + 1].y <=p.y)))16 {17 //compute the actual edge-ray intersect x-coordinate
18 float vt = (float)(p.y - polyPoints [i].y) / (polyPoints [i + 1].y -polyPoints [i].y);19
20 //p.x < intersect
21 if(p.x < polyPoints [i].x + vt * (polyPoints [i + 1].x -polyPoints [i].x))22 ++cn;23 }24 }25
26 //實際測試發現cn為0的情況即為宣雨松算法中存在的問題27 //所以在這里進行屏蔽直接返回false這樣就可以讓透明區域不再響應
28 if(cn == 0)29 return false;30
31 //返回true表示在多邊形外部否則表示在多邊形內部
32 return cn % 2 == 0;33 }
基于上面算法制作的多邊形碰撞器實現的不規則按鈕,以正五邊形舉例(PayneQin大神實現,馬三只是搬運工):
1 /*
2 * 基于多邊形碰撞器實現的不規則按鈕3 * 作者:PayneQin4 * 日期:2016年7月9日5 */
6
7 usingUnityEngine;8 usingSystem.Collections;9 usingUnityEngine.UI;10 usingUnityEngine.EventSystems;11
12 public classUnregularButtonWithCollider : MonoBehaviour,IPointerClickHandler13 {14 ///
15 ///多邊形碰撞器16 ///
17 PolygonCollider2D polygonCollider;18
19 voidStart()20 {21 //獲取多邊形碰撞器
22 polygonCollider = transform.GetComponent();23 }24
25
26 public voidOnPointerClick(PointerEventData eventData)27 {28 //對2D屏幕坐標系進行轉換
29 Vector2 local;30 local.x = eventData.position.x - (float)Screen.width / 2.0f;31 local.y = eventData.position.y - (float)Screen.height / 2.0f;32 if(ContainsPoint(polygonCollider.points,local))33 {34
35 Debug.Log ("這是一個正五邊形!");36 }37
38 }39
40 ///
41 ///判斷指定點是否在給定的任意多邊形內42 ///
43 boolContainsPoint(Vector2[] polyPoints,Vector2 p)44 {45 //統計射線和多邊形交叉次數
46 int cn = 0;47
48 //遍歷多邊形頂點數組中的每條邊
49 for(int i=0; i
52 polyPoints [i].x += transform.GetComponent().position.x;53 polyPoints [i].y += transform.GetComponent().position.y;54
55 //從當前位置發射向上向下兩條射線
56 if(((polyPoints [i].y <= p.y) && (polyPoints [i + 1].y >p.y))57 || ((polyPoints [i].y > p.y) && (polyPoints [i + 1].y <=p.y)))58 {59 //compute the actual edge-ray intersect x-coordinate
60 float vt = (float)(p.y - polyPoints [i].y) / (polyPoints [i + 1].y -polyPoints [i].y);61
62 //p.x < intersect
63 if(p.x < polyPoints [i].x + vt * (polyPoints [i + 1].x -polyPoints [i].x))64 ++cn;65 }66
67 }68
69 //實際測試發現cn為0的情況即為宣雨松算法中存在的問題70 //所以在這里進行屏蔽直接返回false這樣就可以讓透明區域不再響應
71 if(cn == 0)72 return false;73
74 //返回true表示在多邊形外部否則表示在多邊形內部
75 return cn % 2 == 0;76 }
三、需要注意的坑
在像素檢測法實現UGUI不規則碰撞區域的過程中,馬三也遇到了很多需要注意的問題,在這里和大家分享一下:
1.圖片需要開啟Read/Writeable屬性
如果選擇使用像素檢測法實現的話,需要注意開啟Texture的Read/Writeable屬性(我們需要讀寫該Texture的像素值),而且他必須是Advance類型。這樣這張圖片就不能打進我們的圖集里面了,必須以散圖的形式存在于工程當中,不利于統一管理。而且開啟了Read/Writeable屬性屬性的話,在程序運行的時候,它會在內存中多復制出來一份,必然會影響到游戲的運行效率。所以盡量還是減少游戲中這種不規則UI的出現。
2.像素檢測有偏移,不準確的問題
馬三在實際操作的過程中,發現實際點擊的時候經常會有偏移(經常偏下一些),有的透明的地方可以點擊,而明明是不透明的地方卻不能點擊。剛開始馬三還以為是圖片格式或者是圖片本身有什么問題,反反復復確認了好多次。直到后來馬三在unity論壇上找到了這篇文章,才找到問題的癥結所在。
對于如下圖所示的這種周圍有空白區域的圖片,我們需要在Unity圖片導入設置的時候,將Mesh Type格式設置為Full Rect,而unity導入時默認幫我們設置的是Tight模式。
? ? ? ? ?
圖5:周圍有空白的圖片 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 圖6:正確的導入設置
那么,它們有什么區別呢?關于它們的區別,Unity官方是這樣解釋的:
圖7:Full Rect和Tight兩種Mesh Type的官方解釋
總的來說就是,用Tight模式的話,如果你的圖片周圍有空白像素,它會幫你壓縮掉減小面積,以減少DrawCall,但是會增加Sprite的面數。如果用Full Rect模式不會壓縮,也不會增加面數,直接創建一個quab,然后把圖片扔上去。如果尺寸小于32x32的話,Unity默認使用Full Rect格式導入,否則使用Tight格式導入。因此如果我們不對Mesh Type進行設置的話,原來的一些空白區域就相當于裁剪掉了,這樣相對于左下角的坐標來說,一些像素坐標就發生了偏移,而我們使用的是像素檢測方法,必然也會導致偏移誤差。
四、總結
通過本篇博客,馬三和大家一起學習了如何在Unity中實現UGUI不規則區域的點擊,希望本篇博客能為大家的工作過程中帶來一些幫助與啟發。
如果覺得本篇博客對您有幫助,可以掃碼小小地鼓勵下馬三,馬三會寫出更多的好文章,支持微信和支付寶喲!
? ? ??
參考資料:
https://blog.csdn.net/qinyuanpei/article/details/51868638
https://blog.csdn.net/shenmifangke/article/details/53504036
https://www.zhihu.com/question/26551754?f3fb8ead20=b6b9d1289bcc893ff2fa0abd1e65fc52
作者:馬三小伙兒
出處:https://www.cnblogs.com/msxh/p/9283266.html
請尊重別人的勞動成果,讓分享成為一種美德,歡迎轉載。另外,文章在表述和代碼方面如有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!
總結
以上是生活随笔為你收集整理的unity不规则点击_【Unity游戏开发】UGUI不规则区域点击的实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 当Apple TV+的生态化反梦,撞上一
- 下一篇: 什么是网站备案