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元素。

代码实现

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即可

相关推荐
wanhengidc6 小时前
云手机是由什么组成的?
运维·服务器·web安全·游戏·智能手机
jtymyxmz9 小时前
《Unity Shader》8.4 透明度混合
unity·游戏引擎
世洋Blog10 小时前
利用<<左移运算符优雅的设计游戏能力的任意组合和判断
游戏·unity·c#
da_vinci_x13 小时前
PS 3D Viewer (Beta):概念美术的降维打击,白模直接在PS里转光打影出5张大片
人工智能·游戏·3d·prompt·aigc·材质·游戏美术
め.14 小时前
用Unity复刻童年经典游戏—愤怒的小鸟
游戏
喵了几个咪14 小时前
游戏字体渲染
开发语言·python·游戏
张丶大帅15 小时前
别踩白块游戏(附源代码)
c语言·游戏
2501_9400940215 小时前
模拟器全部BIOS合集 RetroArch BIOS 解决模拟器提示缺少bios的问题 通用所有游戏模拟器
游戏
lqj_本人15 小时前
鸿蒙与Qt的双线程模型:主线程与UI线程的博弈
qt·ui·harmonyos
wanhengidc16 小时前
跨境电商为什么依赖于云手机
运维·服务器·游戏·智能手机·云计算