Unity Mask镂空效果(常用于新手引导或高亮显示UI元素)
在游戏中经常可以看到新手引导的效果是全屏上方有一张黑色遮罩,指定区块高亮展示,并且鼠标的点击事件可以穿透黑色遮罩点击到下方的按钮。(如下图所示)
那这个效果是怎么实现的呢?下面提供两种实现方案:
1、使用四个canvas进行拼接(理解成本低,但不推荐)
这样能保证界面中可以保留一个矩形的空白区块。由于中心区块没有任何阻挡射线检测的障碍,因此也能满足点击区域内按钮的需求
只需要知道空白区域的Rect数据,就可以计算出4个Canvas的尺寸以及位置。
优点:理解成本低,实现方法简单
缺点:Canvas之间的拼接可能会出现缝隙(与UI界面渲染逻辑有关,不太好避免)
2、重写MaskableGraphic,实现挖空效果(推荐)
主要分两步实现:
①美术效果层面的镂空展示
②射线检测层面的过滤
美术效果层面的镂空展示
OnPopulateMesh 算是一个比较经典的函数,这个可以让我们实现图片的裁剪
例如我们需要裁切中间区域:
1、先绘制一个覆盖整个屏幕的矩形(外部矩形)
2、在目标区域(内部矩形)挖空,形成镂空效果
3、顶点和三角形设置:创建8个顶点(4个外部顶点,4个内部顶点),然后通过8个三角形连接这些顶点,形成外部矩形和内部镂空区域之间的边框。
通过这种方法,实现“空白区域”的镂空效果
射线检测层面的过滤
实现`ICanvasRaycastFilter`接口的`IsRaycastLocationValid`方法
在目标区域内的点击事件可以穿透(返回false)
遮罩部分会拦截事件(返回true)
这样,只有遮罩部分响应事件,目标区域则允许事件穿透到后面的UI元素。
代码实现
using System.Collections;
using UnityEngine;
using UnityEngine.UI;namespace GameComponent
{public class GuideMask : MaskableGraphic, ICanvasRaycastFilter{[SerializeField]private RectTransform _target;private Vector3 _targetMin = Vector3.zero;private Vector3 _targetMax = Vector3.zero;private bool _canRefresh = true;private Transform _cacheTrans = null;/// <summary>/// 设置镂空的目标/// </summary>public void SetTarget(RectTransform target){_canRefresh = true;_target = target;_RefreshView();}private void _SetTarget(Vector3 tarMin, Vector3 tarMax){if (tarMin == _targetMin && tarMax == _targetMax)return;_targetMin = tarMin;_targetMax = tarMax;SetAllDirty();}private void _RefreshView(){if (!_canRefresh) return;_canRefresh = false;if (null == _target){_SetTarget(Vector3.zero, Vector3.zero);SetAllDirty();}else{StartCoroutine(RefreshNextFrame());}}private IEnumerator RefreshNextFrame(){yield return null;Vector3 targetLocalPos = _cacheTrans.InverseTransformPoint(_target.position);Rect targetRect = _target.rect;Vector2 pivot = _target.pivot;float leftOffset = targetRect.width * pivot.x;float rightOffset = targetRect.width * (1 - pivot.x);float bottomOffset = targetRect.height * pivot.y;float topOffset = targetRect.height * (1 - pivot.y);Vector3 targetMin = new Vector3(targetLocalPos.x - leftOffset, targetLocalPos.y - bottomOffset, 0);Vector3 targetMax = new Vector3(targetLocalPos.x + rightOffset, targetLocalPos.y + topOffset, 0);_SetTarget(targetMin, targetMax);}protected override void OnPopulateMesh(VertexHelper vh){if (_targetMin == Vector3.zero && _targetMax == Vector3.zero){base.OnPopulateMesh(vh);return;}vh.Clear();UIVertex vert = UIVertex.simpleVert;vert.color = color;Vector2 selfPiovt = rectTransform.pivot;Rect selfRect = rectTransform.rect;float outerLx = -selfPiovt.x * selfRect.width;float outerBy = -selfPiovt.y * selfRect.height;float outerRx = (1 - selfPiovt.x) * selfRect.width;float outerTy = (1 - selfPiovt.y) * selfRect.height;// 0 - Outer:LTvert.position = new Vector3(outerLx, outerTy);vh.AddVert(vert);// 1 - Outer:RTvert.position = new Vector3(outerRx, outerTy);vh.AddVert(vert);// 2 - Outer:RBvert.position = new Vector3(outerRx, outerBy);vh.AddVert(vert);// 3 - Outer:LBvert.position = new Vector3(outerLx, outerBy);vh.AddVert(vert);// 4 - Inner:LTvert.position = new Vector3(_targetMin.x, _targetMax.y);vh.AddVert(vert);// 5 - Inner:RTvert.position = new Vector3(_targetMax.x, _targetMax.y);vh.AddVert(vert);// 6 - Inner:RBvert.position = new Vector3(_targetMax.x, _targetMin.y);vh.AddVert(vert);// 7 - Inner:LBvert.position = new Vector3(_targetMin.x, _targetMin.y);vh.AddVert(vert);// 设定三角形vh.AddTriangle(4, 0, 1);vh.AddTriangle(4, 1, 5);vh.AddTriangle(5, 1, 2);vh.AddTriangle(5, 2, 6);vh.AddTriangle(6, 2, 3);vh.AddTriangle(6, 3, 7);vh.AddTriangle(7, 3, 0);vh.AddTriangle(7, 0, 4);}bool ICanvasRaycastFilter.IsRaycastLocationValid(Vector2 screenPos, Camera eventCamera){if (null == _target) return true;// 将目标对象范围内的事件镂空(使其穿过)return !RectTransformUtility.RectangleContainsScreenPoint(_target, screenPos, eventCamera);}protected override void Awake(){base.Awake();_cacheTrans = GetComponent<RectTransform>();}#if UNITY_EDITORvoid Update(){// _canRefresh = true;// _RefreshView();}
#endif}
}
使用方法
使用GuideMask.SetTarget()赋值需要镂空的RectTransform即可