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脚本节省内存。

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

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

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

第一,没想好;

第二,还是没想好;

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

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

UGUI实现不规则区域点击响应的更多相关文章

  1. 【Unity游戏开发】UGUI不规则区域点击的实现

    一.简介 马三从上一家公司离职了,最近一直在出去面试,忙得很,所以这一篇博客拖到现在才写出来.马三在上家公司工作的时候,曾处理了一个UGUI不规则区域点击的问题,制作过程中也有一些收获和需要注意坑,因 ...

  2. Unity不规则按钮点击区域(UGUI)

    文章目录 一. 前言 二. 最终效果 三. 实现 1.创建UICamera 2. UIPolygon节点 3. 编辑碰撞区域 5. 运行测试 6. UIPolygon代码 一. 前言 游戏开发中,可能 ...

  3. 小程序Echarts 构建中国地图并锚定区域点击事件

    小程序Echarts 构建中国地图并锚定区域点击事件 Step1 效果展示 使用的绘图框架为 Echarts for Wexin 具体API文档地址请点击 ----> Step2 条件准备 1. ...

  4. C++ 中利用 Opencv 得到不规则的ROI 区域(已知不规则区域)

    因为需要,之前写了一个利用mask 得到不规则ROI 区域的程序. 现在需要修改,发现自己都看不懂是怎么做的了.. 所以把它整理下来. 首先利用 鼠标可以得到 你想要的不规则区域的 顶点信息.具体这里 ...

  5. ngui处理不规则按钮点击

    吐个槽  棋牌类游戏做什么中国地图!!!  然后就要用到不规则按钮点击了 你懂的 213的unity虽然已经加入了polygoncollider 2d的支持 但是 但是 但是 是2d的 也就是说如果不 ...

  6. 【GIS新探索】算法实现在不规则区域内均匀分布点

    1 概要 在不规则区域内均匀分布点,这个需求初看可能不好理解.如果设想一下需求场景就比较简单了. 场景1:在某个地区范围内,例如A市区有100W人口,需要将这100W人口在地图上面相对均匀的标识出来. ...

  7. [OpenGL] 不规则区域的填充算法

    不规则区域的填充算法 一.简单递归 利用Dfs实现简单递归填充. 核心代码: // 简单深度搜索填充 (四连通) void DfsFill(int x, int y) { || y < || x ...

  8. 2019-11-29-WPF-非客户区的触摸和鼠标点击响应

    原文:2019-11-29-WPF-非客户区的触摸和鼠标点击响应 title author date CreateTime categories WPF 非客户区的触摸和鼠标点击响应 lindexi ...

  9. 2019-8-8-WPF-非客户区的触摸和鼠标点击响应

    title author date CreateTime categories WPF 非客户区的触摸和鼠标点击响应 lindexi 2019-08-08 16:48:31 +0800 2019-07 ...

随机推荐

  1. ES6中map和set用法

    ES6中map和set用法 --转载自廖雪峰的官方网站 一.map Map是一组键值对的结构,具有极快的查找速度. 举个例子,假设要根据同学的名字查找对应的成绩,如果用Array实现,需要两个Arra ...

  2. U-Mail如何实现邮件营销自动化?

    对于很多企业来说,人力成本可能就是最大的成本支出了,如果能节省这方面成本支出,也就意味着公司增收了,因此很多公司在做营销工作时,都希望营销能够高效率.有系统.有规划.循序渐进的开展,同时还要减轻营销人 ...

  3. 【http学习杂记】2017年7月14日

    1. 连接超时 连接超时是tcp协议层次, 此时服务器还没有处理请求数据,也就是说服务器的逻辑开没有执行 2. 请求超时 请求超时属于服务器已经连接成功并开始处理,但是时间比较长,大于你设置的请求超时 ...

  4. Git操作(基础篇)

    Git操作(基础篇) Git是一款免费.开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目.Git的读音为/gɪt/.Git是一个开源的分布式版本控制系统,用以有效.高速的处理从很小到非常 ...

  5. Java Math类学习

    1.  java.lang.Math Math类其成员皆为静态成员(static),无需创建对象,直接用类名Math作为前缀使用它们即可. 2.  Math类有两个静态常量:E(自然对数)和PI(圆周 ...

  6. 1192. [HNOI2006]鬼谷子的钱袋【进制】

    Description 鬼谷子非常聪明,正因为这样,他非常繁忙,经常有各诸侯车的特派员前来向他咨询时政.有一天,他在咸阳游历的时候,朋友告诉他在咸阳最大的拍卖行(聚宝商行)将要举行一场拍卖会,其中有一 ...

  7. HashMap实现原理及源码分析之JDK7

    攻克集合第一关!! 转载 http://www.cnblogs.com/chengxiao/ 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如m ...

  8. 在存放源程序的文件夹中建立一个子文件夹 myPackage。例如,在“D:\java”文件夹之中创建一个与包同名的子文件夹 myPackage(D:\java\myPackage)。在 myPackage 包中创建一个YMD类,该类具有计算今年的年份、可以输出一个带有年月日的字符串的功能。设计程序SY31.java,给定某人姓名和出生日期,计算该人年龄,并输出该人姓名、年龄、出生日期。程序使用YM

    题目补充: 在存放源程序的文件夹中建立一个子文件夹 myPackage.例如,在“D:\java”文件夹之中创建一个与包同名的子文件夹 myPackage(D:\java\myPackage).在 m ...

  9. 【题解】洛谷P1169 [ZJOI2007] 棋盘制作(坐标DP+悬线法)

    次元传送门:洛谷P1169 思路 浙江省选果然不一般 用到一个从来没有听过的算法 悬线法: 所谓悬线法 就是用一条线(长度任意)在矩阵中判断这条线能到达的最左边和最右边及这条线的长度 即可得到这个矩阵 ...

  10. ZOJ 2475 Benny's Compiler(dfs判断有向图给定点有没有参与构成环)

    B - Benny's Compiler Time Limit:2000MS     Memory Limit:65536KB     64bit IO Format:%lld & %llu ...