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脚本,这个脚本提供了编辑器编辑功能。
这个继承类还需要实现IPointerUpHandler、IPointerDownHandler和IPointerClickHandler三个接口,方便执行点击回调(可根据需求删减)。
[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脚本。在编辑器里对PolygonCollider2D的Collider进行编辑,这个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实现不规则区域点击响应的更多相关文章
- 【Unity游戏开发】UGUI不规则区域点击的实现
一.简介 马三从上一家公司离职了,最近一直在出去面试,忙得很,所以这一篇博客拖到现在才写出来.马三在上家公司工作的时候,曾处理了一个UGUI不规则区域点击的问题,制作过程中也有一些收获和需要注意坑,因 ...
- Unity不规则按钮点击区域(UGUI)
文章目录 一. 前言 二. 最终效果 三. 实现 1.创建UICamera 2. UIPolygon节点 3. 编辑碰撞区域 5. 运行测试 6. UIPolygon代码 一. 前言 游戏开发中,可能 ...
- 小程序Echarts 构建中国地图并锚定区域点击事件
小程序Echarts 构建中国地图并锚定区域点击事件 Step1 效果展示 使用的绘图框架为 Echarts for Wexin 具体API文档地址请点击 ----> Step2 条件准备 1. ...
- C++ 中利用 Opencv 得到不规则的ROI 区域(已知不规则区域)
因为需要,之前写了一个利用mask 得到不规则ROI 区域的程序. 现在需要修改,发现自己都看不懂是怎么做的了.. 所以把它整理下来. 首先利用 鼠标可以得到 你想要的不规则区域的 顶点信息.具体这里 ...
- ngui处理不规则按钮点击
吐个槽 棋牌类游戏做什么中国地图!!! 然后就要用到不规则按钮点击了 你懂的 213的unity虽然已经加入了polygoncollider 2d的支持 但是 但是 但是 是2d的 也就是说如果不 ...
- 【GIS新探索】算法实现在不规则区域内均匀分布点
1 概要 在不规则区域内均匀分布点,这个需求初看可能不好理解.如果设想一下需求场景就比较简单了. 场景1:在某个地区范围内,例如A市区有100W人口,需要将这100W人口在地图上面相对均匀的标识出来. ...
- [OpenGL] 不规则区域的填充算法
不规则区域的填充算法 一.简单递归 利用Dfs实现简单递归填充. 核心代码: // 简单深度搜索填充 (四连通) void DfsFill(int x, int y) { || y < || x ...
- 2019-11-29-WPF-非客户区的触摸和鼠标点击响应
原文:2019-11-29-WPF-非客户区的触摸和鼠标点击响应 title author date CreateTime categories WPF 非客户区的触摸和鼠标点击响应 lindexi ...
- 2019-8-8-WPF-非客户区的触摸和鼠标点击响应
title author date CreateTime categories WPF 非客户区的触摸和鼠标点击响应 lindexi 2019-08-08 16:48:31 +0800 2019-07 ...
随机推荐
- ES6中map和set用法
ES6中map和set用法 --转载自廖雪峰的官方网站 一.map Map是一组键值对的结构,具有极快的查找速度. 举个例子,假设要根据同学的名字查找对应的成绩,如果用Array实现,需要两个Arra ...
- U-Mail如何实现邮件营销自动化?
对于很多企业来说,人力成本可能就是最大的成本支出了,如果能节省这方面成本支出,也就意味着公司增收了,因此很多公司在做营销工作时,都希望营销能够高效率.有系统.有规划.循序渐进的开展,同时还要减轻营销人 ...
- 【http学习杂记】2017年7月14日
1. 连接超时 连接超时是tcp协议层次, 此时服务器还没有处理请求数据,也就是说服务器的逻辑开没有执行 2. 请求超时 请求超时属于服务器已经连接成功并开始处理,但是时间比较长,大于你设置的请求超时 ...
- Git操作(基础篇)
Git操作(基础篇) Git是一款免费.开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目.Git的读音为/gɪt/.Git是一个开源的分布式版本控制系统,用以有效.高速的处理从很小到非常 ...
- Java Math类学习
1. java.lang.Math Math类其成员皆为静态成员(static),无需创建对象,直接用类名Math作为前缀使用它们即可. 2. Math类有两个静态常量:E(自然对数)和PI(圆周 ...
- 1192. [HNOI2006]鬼谷子的钱袋【进制】
Description 鬼谷子非常聪明,正因为这样,他非常繁忙,经常有各诸侯车的特派员前来向他咨询时政.有一天,他在咸阳游历的时候,朋友告诉他在咸阳最大的拍卖行(聚宝商行)将要举行一场拍卖会,其中有一 ...
- HashMap实现原理及源码分析之JDK7
攻克集合第一关!! 转载 http://www.cnblogs.com/chengxiao/ 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如m ...
- 在存放源程序的文件夹中建立一个子文件夹 myPackage。例如,在“D:\java”文件夹之中创建一个与包同名的子文件夹 myPackage(D:\java\myPackage)。在 myPackage 包中创建一个YMD类,该类具有计算今年的年份、可以输出一个带有年月日的字符串的功能。设计程序SY31.java,给定某人姓名和出生日期,计算该人年龄,并输出该人姓名、年龄、出生日期。程序使用YM
题目补充: 在存放源程序的文件夹中建立一个子文件夹 myPackage.例如,在“D:\java”文件夹之中创建一个与包同名的子文件夹 myPackage(D:\java\myPackage).在 m ...
- 【题解】洛谷P1169 [ZJOI2007] 棋盘制作(坐标DP+悬线法)
次元传送门:洛谷P1169 思路 浙江省选果然不一般 用到一个从来没有听过的算法 悬线法: 所谓悬线法 就是用一条线(长度任意)在矩阵中判断这条线能到达的最左边和最右边及这条线的长度 即可得到这个矩阵 ...
- ZOJ 2475 Benny's Compiler(dfs判断有向图给定点有没有参与构成环)
B - Benny's Compiler Time Limit:2000MS Memory Limit:65536KB 64bit IO Format:%lld & %llu ...