在游戏中经常可以看到新手引导的效果是全屏上方有一张黑色遮罩,指定区块高亮展示,并且鼠标的点击事件可以穿透黑色遮罩点击到下方的按钮。(如下图所示)
那这个效果是怎么实现的呢?下面提供两种实现方案:
1、使用四个canvas进行拼接(理解成本低,但不推荐)
这样能保证界面中可以保留一个矩形的空白区块。由于中心区块没有任何阻挡射线检测的障碍,因此也能满足点击区域内按钮的需求

只需要知道空白区域的Rect数据,就可以计算出4个Canvas的尺寸以及位置。
优点:理解成本低,实现方法简单
缺点:Canvas之间的拼接可能会出现缝隙(与UI界面渲染逻辑有关,不太好避免)
2、重写MaskableGraphic,实现挖空效果(推荐)
主要分两步实现:
①美术效果层面的镂空展示
②射线检测层面的过滤
美术效果层面的镂空展示
OnPopulateMesh 算是一个比较经典的函数,这个可以让我们实现图片的裁剪
例如我们需要裁切中间区域:
1、先绘制一个覆盖整个屏幕的矩形(外部矩形)
2、在目标区域(内部矩形)挖空,形成镂空效果
3、顶点和三角形设置:创建8个顶点(4个外部顶点,4个内部顶点),然后通过8个三角形连接这些顶点,形成外部矩形和内部镂空区域之间的边框。
通过这种方法,实现"空白区域"的镂空效果
射线检测层面的过滤
实现`ICanvasRaycastFilter`接口的`IsRaycastLocationValid`方法
在目标区域内的点击事件可以穿透(返回false)
遮罩部分会拦截事件(返回true)
这样,只有遮罩部分响应事件,目标区域则允许事件穿透到后面的UI元素。
代码实现
cs
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:LT
vert.position = new Vector3(outerLx, outerTy);
vh.AddVert(vert);
// 1 - Outer:RT
vert.position = new Vector3(outerRx, outerTy);
vh.AddVert(vert);
// 2 - Outer:RB
vert.position = new Vector3(outerRx, outerBy);
vh.AddVert(vert);
// 3 - Outer:LB
vert.position = new Vector3(outerLx, outerBy);
vh.AddVert(vert);
// 4 - Inner:LT
vert.position = new Vector3(_targetMin.x, _targetMax.y);
vh.AddVert(vert);
// 5 - Inner:RT
vert.position = new Vector3(_targetMax.x, _targetMax.y);
vh.AddVert(vert);
// 6 - Inner:RB
vert.position = new Vector3(_targetMax.x, _targetMin.y);
vh.AddVert(vert);
// 7 - Inner:LB
vert.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_EDITOR
void Update()
{
// _canRefresh = true;
// _RefreshView();
}
#endif
}
}
使用方法
使用GuideMask.SetTarget()赋值需要镂空的RectTransform即可
