Unity新手引导

介绍

这里要介绍的是通过着色器(Shader)来实现高亮某一UI

效果

先看效果

一个圆形范围高亮显示,一个矩形范围显示

圆形

这里先上C#代码,后面会给出Shader的代码

/// <summary>
/// 圆形遮罩镂空引导
/// </summary>
public class CircleGuidanceController : MonoBehaviour
{
	/// <summary>
	/// 要高亮显示的目标
	/// </summary>
	public RectTransform Target;
	
	/// <summary>
	/// 区域范围缓存
	/// </summary>
	private Vector3[] _corners = new Vector3[4];

	/// <summary>
	/// 镂空区域圆心
	/// </summary>
	private Vector4 _center;

	/// <summary>
	/// 镂空区域半径
	/// </summary>
	private float _radius;

	/// <summary>
	/// 遮罩材质
	/// </summary>
	private Material _material;

	/// <summary>
	/// 当前高亮区域的半径
	/// </summary>
	private float _currentRadius;

	/// <summary>
	/// 高亮区域缩放的动画时间
	/// </summary>
	private float _shrinkTime = 0.5f;

    /// <summary>
    /// 时间渗透组件
    /// </summary>
    private GuidanceEventPenetrate _eventPenetrate;

    /// <summary>
    /// 世界坐标向画布坐标转换
    /// </summary>
    /// <param name="canvas">画布</param>
    /// <param name="world">世界坐标</param>
    /// <returns>返回画布上的二维坐标</returns>
    private Vector2 WorldToCanvasPos(Canvas canvas, Vector3 world)
	{
		Vector2 position;

		RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform,
			world, canvas.GetComponent<Camera>(), out position);
		return position;
	}

    public void SetTarget(RectTransform target,bool isPauseGame, float timer=0.5f)
    {
        this.isPauseGame = isPauseGame;
        Target = target;
        _shrinkTime = timer;
        _eventPenetrate = GetComponent<GuidanceEventPenetrate>();
        if (_eventPenetrate != null)
            _eventPenetrate.SetTarget(Target);
        //获取画布
        Canvas canvas = GameObject.Find("Canvas").GetComponent<Canvas>();
        //获取高亮区域的四个顶点的世界坐标
        Target.GetWorldCorners(_corners);
        //计算最终高亮显示区域的半径
        _radius = Vector2.Distance(WorldToCanvasPos(canvas, _corners[0]), WorldToCanvasPos(canvas, _corners[2])) / 2f;
        //计算高亮显示区域的圆心
        float x = _corners[0].x + ((_corners[3].x - _corners[0].x) / 2f);
        float y = _corners[0].y + ((_corners[1].y - _corners[0].y) / 2f);
        Vector3 centerWorld = new Vector3(x, y, 0);
        Vector2 center = WorldToCanvasPos(canvas, centerWorld);
        //设置遮罩材料中的圆心变量
        Vector4 centerMat = new Vector4(center.x, center.y, 0, 0);
        _material = GetComponent<Image>().material;
        _material.SetVector("_Center", centerMat);
        //计算当前高亮显示区域的半径
        RectTransform canRectTransform = canvas.transform as RectTransform;
        if (canRectTransform != null)
        {
            //获取画布区域的四个顶点
            canRectTransform.GetWorldCorners(_corners);
            //将画布顶点距离高亮区域中心最远的距离作为当前高亮区域半径的初始值
            foreach (Vector3 corner in _corners)
            {
                _currentRadius = Mathf.Max(Vector3.Distance(WorldToCanvasPos(canvas, corner), center), _currentRadius);
            }
        }
        _material.SetFloat("_Slider", _currentRadius);

        if (mShink != null)
        {
            StopCoroutine(mShink);
        }
        if (isPauseGame)
        {
            mShink=StartCoroutine(StartShink());
        }
    }
    private Coroutine mShink;
    private bool isPauseGame;
    /// <summary>
    /// 脱离timeScale影响
    /// </summary>
    /// <returns></returns>
    IEnumerator StartShink()
    {
        float length = _currentRadius - _radius;

        for (float i = 0; i < _shrinkTime; i+= Time.unscaledDeltaTime)
        {
            float scale = i / _shrinkTime;
            if (scale >= 1) scale = 1;
            float value = _currentRadius - (length * scale);
            _material.SetFloat("_Slider", value);
            yield return 0;
        }
        _material.SetFloat("_Slider", _radius);
    }

	/// <summary>
	/// 收缩速度
	/// </summary>
	private float _shrinkVelocity = 0f;

	private void Update()
	{
        if (isPauseGame) return;

        if (Target == null) return;

		//从当前半径到目标半径差值显示收缩动画
		float value = Mathf.SmoothDamp(_currentRadius, _radius, ref _shrinkVelocity, _shrinkTime);
             
		if (!Mathf.Approximately(value, _currentRadius))
		{
			_currentRadius = value;
			_material.SetFloat("_Slider", _currentRadius);
		}
	}

    public void ClearTaget()
    {
        Target = null;
    }
}

这里由于我项目中有的引导会把游戏暂停(timeScale),这样会导致收缩动画也被暂停,所以我增加了一个协程通道,通过协程来播放收缩动画。下面是圆形着色器代码:


Shader "UI/BeginnerGuidance/CircleGuidance"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
        
        _Center("Center",vector) = (0,0,0,0)
        _Slider("Slider",Range(0,1500)) = 1500
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

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

            #pragma multi_compile __ UNITY_UI_CLIP_RECT
            #pragma multi_compile __ UNITY_UI_ALPHACLIP

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float2 _Center;
            float _Slider;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

                OUT.texcoord = v.texcoord;

                OUT.color = v.color * _Color;
                return OUT;
            }

            sampler2D _MainTex;

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

                #ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif
                
                color.a *= (distance(IN.worldPosition.xy, _Center.xy) > _Slider);
                color.rgb *= color.a;

                return color;
            }
        ENDCG
        }
    }
}

矩形

代码基本都有注释,挂载在UI上即可,UI材质球着色器使用对应的Shader

/// <summary>
/// 矩形引导组件
/// </summary>
public class RectGuidanceController : MonoBehaviour
{

	/// <summary>
	/// 高亮显示的目标
	/// </summary>
	public RectTransform Target;
	
	/// <summary>
	/// 区域范围缓存
	/// </summary>
	private Vector3[] _corners = new Vector3[4];

	/// <summary>
	/// 镂空区域中心
	/// </summary>
	private Vector4 _center;

	/// <summary>
	/// 最终的偏移值X
	/// </summary>
	private float _targetOffsetX = 0f;

	/// <summary>
	/// 最终的偏移值Y
	/// </summary>
	private float _targetOffsetY = 0f;

	/// <summary>
	/// 遮罩材质
	/// </summary>
	private Material _material;
	
	/// <summary>
	/// 当前的偏移值X
	/// </summary>
	private float _currentOffsetX = 0f;

	/// <summary>
	/// 当前的偏移值Y
	/// </summary>
	private float _currentOffsetY = 0f;

	/// <summary>
	/// 动画收缩时间
	/// </summary>
	private float _shrinkTime = 0.5f;

	/// <summary>
	/// 时间渗透组件
	/// </summary>
	private GuidanceEventPenetrate _eventPenetrate;

	/// <summary>
	/// 世界坐标到画布坐标的转换
	/// </summary>
	/// <param name="canvas">画布</param>
	/// <param name="world">世界坐标</param>
	/// <returns>转换后在画布的坐标</returns>
	private Vector2 WorldToCanvasPos(Canvas canvas, Vector3 world)
	{
		Vector2 position;
		RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, world,
			canvas.GetComponent<Camera>(), out position);
		return position;
	}

    public void SetTarget(RectTransform rectTransform, bool isPauseGame, float timer=0.5f)
    {
        this.isPauseGame = isPauseGame;
        Target = rectTransform;
        _shrinkTime = timer;
        _eventPenetrate = GetComponent<GuidanceEventPenetrate>();
        if (_eventPenetrate != null)
            _eventPenetrate.SetTarget(Target);
        //获取画布
        Canvas canvas = GameObject.Find("Canvas").GetComponent<Canvas>();
        //获取高亮区域四个顶点的世界坐标
        Target.GetWorldCorners(_corners);
        //计算高亮显示区域咋画布中的范围
        _targetOffsetX = Vector2.Distance(WorldToCanvasPos(canvas, _corners[0]), WorldToCanvasPos(canvas, _corners[3])) / 2f;
        _targetOffsetY = Vector2.Distance(WorldToCanvasPos(canvas, _corners[0]), WorldToCanvasPos(canvas, _corners[1])) / 2f;
        //计算高亮显示区域的中心
        float x = _corners[0].x + ((_corners[3].x - _corners[0].x) / 2f);
        float y = _corners[0].y + ((_corners[1].y - _corners[0].y) / 2f);
        Vector3 centerWorld = new Vector3(x, y, 0);
        Vector2 center = WorldToCanvasPos(canvas, centerWorld);
        //设置遮罩材料中中心变量
        Vector4 centerMat = new Vector4(center.x, center.y, 0, 0);
        _material = GetComponent<Image>().material;
        _material.SetVector("_Center", centerMat);
        //计算当前偏移的初始值
        RectTransform canvasRectTransform = (canvas.transform as RectTransform);
        if (canvasRectTransform != null)
        {
            //获取画布区域的四个顶点
            canvasRectTransform.GetWorldCorners(_corners);
            //求偏移初始值
            for (int i = 0; i < _corners.Length; i++)
            {
                if (i % 2 == 0)
                    _currentOffsetX = Mathf.Max(Vector3.Distance(WorldToCanvasPos(canvas, _corners[i]), center), _currentOffsetX);
                else
                    _currentOffsetY = Mathf.Max(Vector3.Distance(WorldToCanvasPos(canvas, _corners[i]), center), _currentOffsetY);
            }
        }
        //设置遮罩材质中当前偏移的变量
        _material.SetFloat("_SliderX", _currentOffsetX);
        _material.SetFloat("_SliderY", _currentOffsetY);

        if (mShink != null)
        {
            StopCoroutine(mShink);
        }
        if (isPauseGame)
        {
            mShink=StartCoroutine(StartShink());
        }
    }

	private float _shrinkVelocityX = 0f;
	private float _shrinkVelocityY = 0f;
    private Coroutine mShink;
    private bool isPauseGame; //是否受时间影响 true 影响 false 不影响
    /// <summary>
    /// 脱离timeScale影响
    /// </summary>
    /// <returns></returns>
    IEnumerator StartShink()
    {
        float lengthX = _currentOffsetX - _targetOffsetX;
        float lengthY = _currentOffsetY - _targetOffsetY;
        for (float i = 0; i < _shrinkTime; i += Time.unscaledDeltaTime)
        {
            float scale = i / _shrinkTime;
            if (scale >= 1) scale = 1;
            float valueX = _currentOffsetX - (lengthX * scale);
            _material.SetFloat("_SliderX", valueX);
            float valueY= _currentOffsetY - (lengthY * scale);
            _material.SetFloat("_SliderY", valueY);
            yield return 0;
        }
        _material.SetFloat("_SliderX", _targetOffsetX);
        _material.SetFloat("_SliderY", _targetOffsetY);
    }
    private void Update()
	{
        if (isPauseGame) return;
		//从当前偏移值到目标偏移值差值显示收缩动画
		float valueX = Mathf.SmoothDamp(_currentOffsetX, _targetOffsetX, ref _shrinkVelocityX, _shrinkTime);
		float valueY = Mathf.SmoothDamp(_currentOffsetY, _targetOffsetY, ref _shrinkVelocityY, _shrinkTime);
		if (!Mathf.Approximately(valueX, _currentOffsetX))
		{
			_currentOffsetX = valueX;
			_material.SetFloat("_SliderX",_currentOffsetX);
		}

		if (!Mathf.Approximately(valueY, _currentOffsetY))
		{
			_currentOffsetY = valueY;
			_material.SetFloat("_SliderY",_currentOffsetY);
		}
	}
    public void ClearTaget()
    {
        Target = null;
    }
}

矩形着色器代码:


Shader "UI/BeginnerGuidance/Rect"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
        
        _Center("Center",vector) = (0,0,0,0)
        _SliderX("SliderX",Range(0,1500)) = 1500
        _SliderY("SliderY",Range(0,1500)) = 1500
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

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

            #pragma multi_compile __ UNITY_UI_CLIP_RECT
            #pragma multi_compile __ UNITY_UI_ALPHACLIP

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            
            float2 _Center;
            float _SliderX;
            float _SliderY;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

                OUT.texcoord = v.texcoord;

                OUT.color = v.color * _Color;
                return OUT;
            }

            sampler2D _MainTex;

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

                #ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif

                float2 dis = IN.worldPosition.xy - _Center.xy;
                color.a *= (abs(dis.x) > _SliderX) || (abs(dis.y) > _SliderY);
                color.rgb *= color.a;

                return color;
            }
        ENDCG
        }
    }
}

UI事件向下穿透

上面的内容只是实现了一个高亮缓动的动画,但是Raycast是被屏蔽了的,这个时候我们需要高亮的地方实现UI事件的穿透,让事件能向下传递。我们只需要实现Unity的ICanvasRaycastFilter接口

public class GuidanceEventPenetrate : MonoBehaviour, ICanvasRaycastFilter
{
    private RectTransform mTagrget;

	public void SetTarget(RectTransform target)
	{
        mTagrget = target;
	}
	public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
	{
		if (mTagrget == null)
			return true;

		return !RectTransformUtility.RectangleContainsScreenPoint(mTagrget, sp, eventCamera);
	}
}

RectangleContainsScreenPoint判断一个屏幕点是否在目标Rect范围内,如果在,则返回真

IsRaycastLocationValid为true时,事件向下传递是无效的,被拦截在当前UI界面,为false,则在当前界面是无效的

原理是当点击的位置在高亮目标的范围内,我们让当前的UI遮罩能向下传递事件,否则屏蔽事件

  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值