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. css画图那些事

    上一篇css3写了一些基本的图形,想到是不是能用css3画个动物,便在网上找图片.于是选中一只大鹏鸟 也不难,一步步的写出身体部位,再定位上去就好了.来一张效果图,后面给两个加了动画,稍微难看一点,后 ...

  2. 浅谈 JSONP

    说起跨域的解决方案,总是会说到 JSONP,但是很多时候都没有仔细去了解过 JSONP,可能是因为现在 JSONP 用的不是很多(多数时候都是配置响应头实现跨域),也可能是因为用 JSONP 的场景一 ...

  3. impdp导入expdp导出数据库实例

    impdp命令在cmd下直接用,不必登录oracle.只能导入expdp导出的dmp文件. expdp导出的时候,需要创建 DIRECTORY 导出什么表空间,导入也要什么表空间. 导出什么用户,导入 ...

  4. Apache去掉index.php

    把 #LoadModule rewrite_module modules/mod_rewrite.so 前面的#去掉, 再把权限AllowOverride None都改为AllowOverride A ...

  5. Python自动化之跨域访问jsonp

    这里提到了JSONP,那有人就问了,它同JSON有什么区别不同和区别呢,接下我们就来看看,百度百科有以下说明: ''' 1. JSON(JavaScript Object Notation) 是一种轻 ...

  6. 论文笔记 M. Saquib Sarfraz_Pose-Sensitive Embedding_re-ranking_2018_CVPR

    1. 摘要 作者使用一个pose-sensitive-embddding,把姿态的粗糙.精细信息结合在一起应用到模型中. 用一个新的re-ranking方法,不需要重新计算新的ranking列表,是一 ...

  7. Linux学习笔记(第十二章)

    grep进阶 grep:以整行为单位进行截取 截取的特殊符号 正规表示法特殊字符 注意: sed用法 格式化打印 awk 用法 diff档案对比: path旧文档升级为新文档

  8. NULLIF与ISNULL的交叉使用

    事件源于字词字段拼接,由于不清楚NULLIF的本质导致惨剧发生. ', 'T5')), '6063-T5') ', 'T5'), ''), '6063-T5') 函数f_CTRL_GetAlloy功能 ...

  9. hive的实践部分

      一.hive的事务 (1)什么是事务 要知道hive的事务,首先要知道什么是transaction(事务)?事务就是一组单元化操作,这些操作要么都执行,要么都不执行,是一个不可分割的工作单位. 事 ...

  10. 用Modelsim SE 直接仿真 Altera(Intel PSG) IP核 需要注意的问题

    如果我们直接用Modelsim SE仿真 Altera IP核,首先会进入Quartus II目录下找到IP核对应的仿真库源文件,然后在Modelsim SE中进行编译,添加到Modelsim SE的 ...