鸟语天空
UGUI—新手引导遮罩层挖洞
post by:追风剑情 2022-6-22 13:26

思路:用Image组件做遮罩层,利用Shader裁剪掉按钮区域的渲染。

一、工程截图

11111.png

22222.png

二、设计用于挖洞的Shader

//新手引导遮罩层&挖洞
Shader "Custom/UIGuideMask"
{
    Properties
    {
        //背景颜色
        _BackgroundColor("Background Color", Color) = (0.0, 0.0, 0.0, 0.5)
        //rect.xy=(0,0),为屏幕左上角坐标
        //rect.zw=(CanvasWidth, CanvasHeight),为屏幕右下角坐标
        _ScreenClipRect ("Screen Clip Rect", Vector) = (0,0,0,0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            fixed4 _BackgroundColor;
            float4 _ScreenClipRect;

            v2f vert(appdata v)
            {
                v2f o;
                //省略下面这句会报警告: Output value 'vert' is not completely initialized
                UNITY_INITIALIZE_OUTPUT(v2f, o);
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                //i.vertex.xy被Unity自动转成了屏幕坐标
                fixed4 col = _BackgroundColor;
                col.a *= (1 - UnityGet2DClipping(i.vertex.xy, _ScreenClipRect));
                clip(col.a - 0.001);
                return col;
            }
            ENDCG
        }
    }
}


三、引导类

using System;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 新手引导遮罩层
/// </summary>
[ExecuteInEditMode, RequireComponent(typeof(Image))]
[DisallowMultipleComponent]
public class UIGuideMask : MonoBehaviour, ICanvasRaycastFilter
{
    private RectTransform canvasRT;
    private float canvasHeight;
    private Material material;
    private RectTransform targetRect;

    public Action OnClickTargetRect;

    private void Awake()
    {
        var canvas = GetComponentInParent<Canvas>();
        canvasRT = canvas.GetComponent<RectTransform>();
        canvasHeight = canvasRT.sizeDelta.y;
        var image = GetComponent<Image>();
        material = image.material;
    }

    private void OnDisable()
    {
        targetRect = null;
        OnClickTargetRect = null;
    }

    private void Update()
    {
        if (Input.GetMouseButtonUp(0))
        {
            Vector2 mousePosition = Input.mousePosition;
            if (ContainsScreenPoint(mousePosition))
            {
                //如果用户点击了引导区域,则调用回调函数
                //可在外部回调函数判断是否继续下一步引导
                OnClickTargetRect?.Invoke();
            }
        }
    }

    // 显示引导
    public void Show(RectTransform target, Action callback)
    {
        this.targetRect = target;
        this.OnClickTargetRect = callback;
        //获取对象的世界坐标
        Vector3[] fourCornersArray = new Vector3[4];
        target.GetWorldCorners(fourCornersArray);
        Vector3 ltPos = fourCornersArray[1];//左上角
        Vector3 rdPos = fourCornersArray[3];//右下角
        //注意:
        //C#获取到的屏幕(0,0)在左下角,
        //Shader中的屏幕(0,0)在左上角。
        ltPos = RectTransformUtility.WorldToScreenPoint(null, ltPos);
        rdPos = RectTransformUtility.WorldToScreenPoint(null, rdPos);
        //与Shader中的屏幕原点保持一致(即,Y轴反向)
        ltPos.y = canvasHeight - ltPos.y;
        rdPos.y = canvasHeight - rdPos.y;
        //裁剪区域
        Vector4 clipRect = new Vector4();
        clipRect.x = ltPos.x;
        clipRect.y = ltPos.y;
        clipRect.z = rdPos.x;
        clipRect.w = rdPos.y;
        material.SetVector("_ScreenClipRect", clipRect);
    }

    // 判断屏幕坐标点是否在目标区域内
    public bool ContainsScreenPoint(Vector2 screenPoint)
    {
        if (targetRect == null)
            return false;
        bool result = RectTransformUtility.RectangleContainsScreenPoint(targetRect, screenPoint);
        return result;
    }

    // 事件是否向下渗透
    // true:不渗透。false:渗透
    public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
    {
        return !ContainsScreenPoint(sp);
    }
}

四、测试类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestGuide : MonoBehaviour
{
    public UIGuideMask guide;
    public List<RectTransform> guideList = new List<RectTransform>();

    void Start()
    {
        BeginGuide();
    }

    private void BeginGuide()
    {
        if (guideList.Count == 0)
            return;
        var rt = guideList[0];
        guideList.RemoveAt(0);
        guide.Show(rt, ()=>
        {
            BeginGuide();
        });
    }
}


运行测试 (因为做了事件向下渗透判断,所以引导区的按钮是能响应鼠标事件的)

11117.gif

评论:
发表评论:
昵称

邮件地址 (选填)

个人主页 (选填)

内容