• UGUI实现不规则区域点击响应


    UGUI实现不规则区域点击响应

    前言

    大家吼啊!最近工作上事情特别多,没怎么打理博客。今天无意打开cnblog才想起该写点东西了。今天给大家讲一个Unity中不规则区域点击响应的实现方法,使用UGUI。

    本脚本编写时基于Unity 5.3,使用其他版本的Unity可能需要做一些小修改。

    本文参考了这篇文章:http://alienryderflex.com/polygon/


    为什么要这么做

    大家都知道在UGUI中,响应点击通常是依附在一张图片上的,而图片不管美术怎么给你切,导进Unity之后都是一个矩形,如果要做其他形状,最多只能旋转一下。

    可能有旁友会说,什么时候会用到这个功能呢?

    开心农场这个页游,相信大家都玩过。里面的田地是一块一块的菱形。

    美术提供给我们的每一块地的切片,肯定并且只能是这样的(格子表示背景透明)。

    这样就会有一个问题:在Unity里面把田地拼出来,要拼成一块挨着一块的效果,图片与图片之间必然会有重叠。

    如果像这样直接挂载Button脚本运行,在点击的时候如果点到了图片相交的位置,Unity默认会根据图层先后来传递点击消息,就会造成想点A地结果点到了B地的错误效果。

    于是这个时候就需要把图片的点击区域缩小一些,让它只包含田地本身的部分。

    可能有旁友会说,这种需求太少见了啊,搞这么复杂干什么。

    少你妹啊= =上上个月我做花千骨手游前端的门派药圃(类似开心农场)功能,特么不就遇到这种需求了吗!

    好了不废话了,技多不压身,防范于未然嘛。


    算法简介

    一个思路是,点下去的坐标是已知的,可以把这个坐标转换为相对于图片本身的坐标,然后取出图片Texture上这个点对应像素的Alpha值。如果这个点是全透明的,那么不拦截点击事件,否则响应点击。

    这样做并不是一个坏思路。要实现取颜色的功能,需要在Unity里面对Texture设置Read/Write Enabled,但这样会增大一倍内存占用。手机应用内存寸土寸金,这种方法应当放弃。

    另一个思路就是今天我们要使用的思路,根据多边形的顶点进行计算。

    不规则多边形点击响应可以用一种更规范的说法来表示:已知一任意多边形,和其n个顶点的坐标,求任意一点是否被包含在这个多边形内部。

    对于这个问题,计算方法有很多种,这里给出一个Crossing Number算法。

    这个算法的思路是,从该点发射一条射线,依次与多边形的每条边相交,如果射线与多边形的交点数为奇数,则这个点在多边形内,否则在多边形外(我姿势水平不够不知道怎么证明它,只知道确实是这样的)。这个方法适用于凸多边形和凹多边形。

    如图所示,点A在多边形内部,点B在多边形外部。从点A和点B分别做一条直线,可以看出点A的单边交点数都是奇数,而点B为偶数。

    那么怎么判断一个点是否和一根线段有交点呢?在已知线段的两个端点的坐标的情况下,可以从点发出一条射线,当射线与线段相交时,根据点斜式需要满足:


    实现

    首先编写判定的方法。使用Unity自带的Vector2结构用于存储坐标信息。这里是从这个点作出水平射线计算。

    /// <summary>
    /// 使用Crossing Number算法获取指定的点是否处于指定的多边形内
    /// </summary>
    private static bool _Contains(Vector2[] pVertexs, Vector2 pPoint)
    {
        var crossNumber = 0;
    
        for (int i = 0, count = pVertexs.Length; i < count; i++)
        {
            var vec1 = pVertexs[i];
            var vec2 = i == count - 1 // 如果当前已到最后一个顶点,则下一个顶点用第一个顶点的数据
                ? pVertexs[0]
                : pVertexs[i + 1];
    
            if (((vec1.y <= pPoint.y) && (vec2.y > pPoint.y))
                || ((vec1.y > pPoint.y) && (vec2.y <= pPoint.y)))
            {
                if (pPoint.x < vec1.x + (pPoint.y - vec1.y) / (vec2.y - vec1.y) * (vec2.x - vec1.x))
                {
                    crossNumber += 1;
                }
            }
        }
    
        return (crossNumber & 1) == 1;
    }
    

    UGUI的Image类有一个IsRaycastLocationValid虚方法,重写后可以根据返回值决定该次点击消息是否会被该Image吞噬。于是可以考虑创建一个继承于Image的类。

    而多边形区域的设定,我们希望在编辑器里能直观地编辑,而不是通过设置数字。为了方便,可以直接在Image上挂载一个自带的PolygonCollider2D脚本,这个脚本提供了编辑器编辑功能。

    这个继承类还需要实现IPointerUpHandlerIPointerDownHandlerIPointerClickHandler三个接口,方便执行点击回调(可根据需求删减)。

    [RequireComponent(typeof(PolygonCollider2D))] 
    public class PolygonClick : Image,
        IPointerUpHandler,
        IPointerDownHandler,
        IPointerClickHandler
        
    {
        private PolygonClickedEvent m_OnPointerClick = new PolygonClickedEvent();
        private PolygonClickedEvent m_OnPointerDown = new PolygonClickedEvent();
        private PolygonClickedEvent m_OnPointerUp = new PolygonClickedEvent();
    
        public class PolygonClickedEvent : UnityEvent<PolygonClick> { }
    }
    

    并且,我们需要在Start()中将PolygonCollider2D的顶点数据缓存下来,并且禁用它节省计算开销。别忘了在OnDestroy()中将这些缓存置为null,手动释放引用计数避免GC发生。

    然后重写IsRaycastLocationValid方法,对点击的点进行判定。这里需要将screenPoint参数转换为UI的坐标。转换有很多种作法,这里我使用了自己写的一个脚本进行转换(代码放在最后面)。

    /// <summary>
    /// 重写方法,用于干涉点击射线有效性
    /// </summary>
    public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
    {
        if (this.m_Vertexs == null)
        {
            return base.IsRaycastLocationValid(screenPoint, eventCamera);
        }
        else
        {
            // 点击的坐标转换为相对于图片的坐标
            //
            UICanvasHelper.Instance.ScreenToUIPoint(ref screenPoint);
            var selfPoint = UICanvasHelper.Instance.WorldToScreenPoint(this.m_RectTransform.position, UICanvasHelper.Instance.UICamera);
            screenPoint.x -= selfPoint.x;
            screenPoint.y -= selfPoint.y;
            // 判断点击是否在区域内
            //
            return _Contains(this.m_Vertexs, screenPoint);
        }
    }
    

    然后是对点击事件的响应:

    public void OnPointerUp(PointerEventData eventData)
    {
        if (this.m_OnPointerUp != null)
        {
            this.m_OnPointerUp.Invoke(this);
        }
    }
    
    
    public void OnPointerClick(PointerEventData eventData)
    {
        if (this.m_OnPointerClick != null)
        {
            this.m_OnPointerClick.Invoke(this);
        }
    }
    
    
    public void OnPointerDown(PointerEventData eventData)
    {
        if (this.m_OnPointerDown != null)
        {
            this.m_OnPointerDown.Invoke(this);
        }
    }
    

    使用方法

    新建一个GameObject,挂载PolygonCollider2D脚本,再挂载PolygonClick脚本。在编辑器里对PolygonCollider2DCollider进行编辑,这个Collider的区域就是点击有效的区域。如果点到区域外就穿透到下一个层级了。

    添加点击回调的示例代码:

    var pc = transform.GetComponent<PolygonClick>();
    pc.PointerDown.AddListener(this._PointerDown);
    pc.PointerClick.AddListener(this._PointerClick);
    pc.PointerUp.AddListener(this._PointerUp);
    

    使用还是非常简单的。


    完整代码

    最后放上完整代码。首先是PolygonClick

    //————————————————————————————————————————————
    //  PolygonClick.cs
    //
    //  Created by Chiyu Ren on ‏‎2016-08-16 11:21
    //————————————————————————————————————————————
    
    using UnityEngine;
    using UnityEngine.UI;
    using UnityEngine.EventSystems;
    using UnityEngine.Events;
    
    using TooSimpleFramework.Components;
    
    
    namespace TooSimpleFramework.UI
    {
        /// <summary>
        /// 支持设置多边形区域作为点击判断的组件
        /// 多边形区域编辑由PolygonCollider2D组件提供
        /// </summary>
        [RequireComponent(typeof(PolygonCollider2D))] 
        public class PolygonClick : Image,
            IPointerUpHandler,
            IPointerClickHandler,
            IPointerDownHandler
        {
            private PolygonClickedEvent m_OnPointerClick = new PolygonClickedEvent();
            private PolygonClickedEvent m_OnPointerDown = new PolygonClickedEvent();
            private PolygonClickedEvent m_OnPointerUp = new PolygonClickedEvent();
    
            private RectTransform m_RectTransform = null;
            private Vector2[] m_Vertexs = null;
    
    
            protected override void Start()
            {
                base.Start();
                // 收集变量
                this.m_RectTransform = base.GetComponent<RectTransform>();
                var c = base.GetComponent<PolygonCollider2D>();
                if (c != null)
                {
                    this.m_Vertexs = c.points;
                    c.enabled = false;
                }
            }
    
    
            protected override void OnDestroy()
            {
                base.OnDestroy();
    
                this.m_RectTransform = null;
                this.m_Vertexs = null;
                this.m_OnPointerUp.RemoveAllListeners();
                this.m_OnPointerClick.RemoveAllListeners();
                this.m_OnPointerDown.RemoveAllListeners();
                this.m_OnPointerUp = null;
                this.m_OnPointerClick = null;
                this.m_OnPointerDown = null;
            }
    
    
            /// <summary>
            /// 点下时发生
            /// </summary>
            public PolygonClickedEvent PointerDown
            {
                get { return this.m_OnPointerDown; }
            }
    
    
            /// <summary>
            /// 点击时发生
            /// </summary>
            public PolygonClickedEvent PointerClick
            {
                get { return this.m_OnPointerClick; }
            }
    
    
            /// <summary>
            /// 点击松开时发生
            /// </summary>
            public PolygonClickedEvent PointerUp
            {
                get { return this.m_OnPointerUp; }
            }
    
    
            /// <summary>
            /// 重写方法,用于干涉点击射线有效性
            /// </summary>
            public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
            {
                if (this.m_Vertexs == null)
                {
                    return base.IsRaycastLocationValid(screenPoint, eventCamera);
                }
                else
                {
                    // 点击的坐标转换为相对于图片的坐标
                    //
                    UICanvasHelper.Instance.ScreenToUIPoint(ref screenPoint);
                    var selfPoint = UICanvasHelper.Instance.WorldToScreenPoint(this.m_RectTransform.position, UICanvasHelper.Instance.UICamera);
                    screenPoint.x -= selfPoint.x;
                    screenPoint.y -= selfPoint.y;
                    // 判断点击是否在区域内
                    //
                    return _Contains(this.m_Vertexs, screenPoint);
                }
            }
    
    
            public void OnPointerUp(PointerEventData eventData)
            {
                if (this.m_OnPointerUp != null)
                {
                    this.m_OnPointerUp.Invoke(this);
                }
            }
    
    
            public void OnPointerClick(PointerEventData eventData)
            {
                if (this.m_OnPointerClick != null)
                {
                    this.m_OnPointerClick.Invoke(this);
                }
            }
    
    
            public void OnPointerDown(PointerEventData eventData)
            {
                if (this.m_OnPointerDown != null)
                {
                    this.m_OnPointerDown.Invoke(this);
                }
            }
    
    
            /// <summary>
            /// 使用Crossing Number算法获取指定的点是否处于指定的多边形内
            /// </summary>
            private static bool _Contains(Vector2[] pVertexs, Vector2 pPoint)
            {
                var crossNumber = 0;
    
                for (int i = 0, count = pVertexs.Length; i < count; i++)
                {
                    var vec1 = pVertexs[i];
                    var vec2 = i == count - 1 // 如果当前已到最后一个顶点,则下一个顶点用第一个顶点的数据
                        ? pVertexs[0]
                        : pVertexs[i + 1];
    
                    if (((vec1.y <= pPoint.y) && (vec2.y > pPoint.y))
                        || ((vec1.y > pPoint.y) && (vec2.y <= pPoint.y)))
                    {
                        if (pPoint.x < vec1.x + (pPoint.y - vec1.y) / (vec2.y - vec1.y) * (vec2.x - vec1.x))
                        {
                            crossNumber += 1;
                        }
                    }
                }
    
                return (crossNumber & 1) == 1;
            }
    
    
            public class PolygonClickedEvent : UnityEvent<PolygonClick> { }
        }
    }
    

    这个UICanvasHelper脚本,建议挂载在Canvas上。它除了可以计算坐标转换,还可以进行分辨率自适配。需要注意的是Canvas不能配置为World Space模式。

    //————————————————————————————————————————————
    //  UICanvasHelper.cs
    //
    //  Created by Chiyu Ren on 2016-08-28 00:02
    //————————————————————————————————————————————
    
    using UnityEngine;
    using UnityEngine.UI;
    
    
    namespace TooSimpleFramework.Components
    {
        /// <summary>
        /// UI画布助手
        /// </summary>
        public class UICanvasHelper : MonoBehaviour
        {
            #region Public Members
            public CanvasScaler UICanvasScaler;
            public Camera UICamera;
            #endregion
    
    
            #region Properties
            public static UICanvasHelper Instance { get; private set; }
            #endregion
    
    
            #region Private Members
            private float m_fWidthScale = -1;
            private float m_fHeightScale = -1;
            private float m_fMatchValue = -1;
            #endregion
    
    
            void Start()
            {
                Instance = this;
    
                this._SetUIMatch();
                this._SetSizeScale();
            }
    
    
            void OnDestroy()
            {
                Instance = null;
            }
    
    
            #region Public Methods
            /// <summary>
            /// 世界坐标转换为屏幕坐标
            /// </summary>
            public Vector2 WorldToScreenPoint(Vector3 pWorldPosition, Camera pCamera = null)
            {
                if (pCamera == null)
                {
                    pCamera = Camera.main;
                }
    
    #if UNITY_EDITOR // 编辑器模式可能随时要调整画面大小
                this._SetSizeScale();
    #endif
    
                Vector2 ret = pCamera.WorldToScreenPoint(pWorldPosition);
                this._SetPositionScale(ref ret);
    
                return ret;
            }
    
            /// <summary>
            /// 屏幕坐标转换为UI坐标
            /// </summary>
            public void ScreenToUIPoint(ref Vector2 pPosition)
            {
    #if UNITY_EDITOR // 编辑器模式可能随时要调整画面大小
                this._SetSizeScale();
    #endif
    
                this._SetPositionScale(ref pPosition);
            }
            #endregion
    
    
            #region Private Methods
            /// <summary>
            /// 设置分辨率适配比例
            /// </summary>
            private void _SetUIMatch()
            {
                this.UICanvasScaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
    
                var scale = Screen.width / (float)(Screen.height);
                if (scale > 1.5f)
                {
                    this.m_fMatchValue = 1;
                }
                else if (scale < 1.4f)
                {
                    this.m_fMatchValue = 0;
                }
                else
                {
                    this.m_fMatchValue = 0.5f;
                }
                this.UICanvasScaler.matchWidthOrHeight = this.m_fMatchValue;
            }
    
            /// <summary>
            /// 设置尺寸缩放比例
            /// </summary>
            private void _SetSizeScale()
            {
                this.m_fWidthScale = this.UICanvasScaler.referenceResolution.x / Screen.width;
                this.m_fHeightScale = this.UICanvasScaler.referenceResolution.y / Screen.height;
            }
    
            /// <summary>
            /// 将传入的坐标转换为缩放后的值
            /// </summary>
            private void _SetPositionScale(ref Vector2 pPosition)
            {
                pPosition.x = (pPosition.x - Screen.width * 0.5f) * ((1 - this.m_fMatchValue) * this.m_fWidthScale + this.m_fMatchValue * m_fHeightScale);
                pPosition.y = (pPosition.y - Screen.height * 0.5f) * ((1 - this.m_fMatchValue) * this.m_fWidthScale + this.m_fMatchValue * m_fHeightScale);
            }
            #endregion
        }
    }
    

    后记

    最后的最后,这个类还可以继续优化,把多边形区域编辑做成单独的编辑器扩展类,这样就可以少挂一个PolygonCollider2D脚本节省内存。

    另外这个算法效率其实并不高,多边形的边数变多之后运算量会比较恐怖,实际运用中要控制多边形边数。

    再一个可以忽略的问题,就是点下去的点如果刚好在多边形的边上,运算结果是不确定的。

    很久没写博客了,今天怒写了一篇,我感觉到非常高兴。讲三句话!

    第一,没想好;

    第二,还是没想好;

    第三,马上就要过年了,在这里祝大家春节遇快,阖家欢洛,万似如意!

    很惭愧,就做了一点微小的工作,谢谢大家!

  • 相关阅读:
    ASP.NET MVC & EF 构建智能查询 二、模型的设计与ModelBinder
    ASP.NET MVC & EF 构建智能查询 一、智能查询的需求与设计
    在ASP.NET中自动合并小图片并使用CSS Sprite显示出来
    Entity Framework with MySQL Provider 更新行数为0的Bug
    为ASP.NET MVC 2.0添加Razor模板引擎 (on .NET4)
    ASP.NET MVC 2.0 in Vs2010 :使用C# 4.0中使用动态类型来传递ViewData
    问题贴
    Visual Studio 2010 RC 下安装ASP.NET MVC 2.0 RTM
    Microsoft Ajax CDN与Google Ajax CDN 你来试试哪个快
    ASP.NET MVC & EF 构建智能查询 三、解析QueryModel
  • 原文地址:https://www.cnblogs.com/GuyaWeiren/p/6338152.html
Copyright © 2020-2023  润新知