飞道的博客

手游新手引导功能(圆形和矩形遮罩)

340人阅读  评论(0)

在游戏开发中经常会做新手引导功能,这是我目前在的公司一款手游产品
引导功能会遇到以下几个问题
1:圆形,矩形遮罩
2:事件穿透
3:业务需求
(强引导/弱引导)

现在市面上的手游引导可分为强引导,弱引导
我根据策划需求制作的是弱引导 ,在策划配置的触发情况下出现引导,也就是说1引导和2引导之前没有强关联性,便于玩家自由操作
先看下游戏效果图如下


上面两张是具体的效果图,下面是遮罩shader根据目标GameObject大小切割成圆形/矩形


在本项目中UI层使用的是xlua 后面代码基本以lua 展示

第一点:圆形/矩形shader遮罩

Shader “UIMask/GuideRoundAndRectangleMask”
{
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)
	_Radius("Radius", Range(0,2000)) = 1000
	_MaskType("Mask Type",Float) = 0  // 0 圆 1 矩形
	_Rectangle("Rectangle",Vector) = (0,0,0,0)
}

	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_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;

			float _Radius;
			float2 _Center;
			float _MaskType;
			float4 _Rectangle;

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

				OUT.texcoord = IN.texcoord;

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

				if (_MaskType == 0) {
					if (distance(IN.worldPosition.xy, _Center.xy) <= _Radius)
					{
						color.a = 0;
					}
				}
				else {
					//UnityGet2DClipping这个函数实现了判断2D空间中的一点是否在一个矩形区域中
					if (UnityGet2DClipping(IN.worldPosition.xy, _Rectangle))
					{
						color.a = 0;
					}
				}
				return color;

			}
		ENDCG
		}
	}

}
在shader 中标注的很清楚,我们对圆形遮罩定义圆心和半径,根据 (distance(IN.worldPosition.xy, _Center.xy) <= _Radius) 计算 区域是否透明
这里提供lua 调用C#代码设置shader数据代码

/// <summary>
/// 设置圆形点击遮罩
/// </summary>
/// <param name="targetGo"></param>
/// <param name="maskCenterGo"></param>
/// <param name="circleValue"></param>
public void SetCircleGuideMask(GameObject targetGo,GameObject maskCenterGo,float circleValue,float x=0f,float y=0f)
{
    // 设置事件透传对象
    gameObject.GetComponent<EventPermeate>().target = targetGo.gameObject;
    //设置聚焦pos
    Vector3 newV3 = GetScreenPoint(maskCenterGo);
    var center = new Vector4(newV3.x+x, newV3.y+y, newV3.z, 0f);
    material.SetFloat("_MaskType", 0f);
    material.SetVector("_Center", center);
    // 设置半径
    material.SetFloat("_Radius", circleValue);
}

targetGo 是点击业务界面的按钮对象,maskCenterGo 是显示圆形遮罩中心点对象,circleValue 是圆半径,x,y 屏幕中心点偏移,GetScreenPoint(go)是获取go在屏幕坐标

 private Vector3 GetScreenPoint(GameObject go)
    {
        var target = go.GetComponent<RectTransform>();
        if (target == null) return Vector3.zero;
        Canvas canvas = getUICanvas();
        Vector3[] corners = new Vector3[4];
        target.GetWorldCorners(corners);
        var diameter = Vector2.Distance(WordToCanvasPos(canvas, corners[0]), WordToCanvasPos(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 center = new Vector3(x, y, 0f);
        Vector2 position = Vector2.zero;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, center, canvas.GetComponent<Camera>(), out position);
        center = new Vector3(position.x, position.y, 0f);
        return center;
    }

C# 设置矩形遮罩

/// <summary>
/// 设置矩形点击遮罩
/// </summary>
/// <param name="targetGo"></param>
/// <param name="maskCenterGo"></param>
/// <param name="circleValue"></param>
public void SetRectangleGuideMask(GameObject targetGo, GameObject maskCenterGo)
{
    // 设置事件透传对象
    gameObject.GetComponent<EventPermeate>().target = targetGo.gameObject;
    RectTransform rec = maskCenterGo.GetComponent<RectTransform>();
    Vector3[] _corners = new Vector3[4];
    rec.GetWorldCorners(_corners);
    Canvas canvas = getUICanvas();
    Vector2 pos1 = WordToCanvasPos(canvas,_corners[0]);//选取左下角
    Vector2 pos2 = WordToCanvasPos(canvas,_corners[2]);//选取右上角
    if(material==null)
    {
        material = GetComponent<Image>().material;
        if (material == null)
        {
            Debug.LogError("GuideMask.cs material == null ");
            return;
        }
    }
    material.SetVector("_Rectangle", new Vector4(pos1.x, pos1.y, pos2.x, pos2.y));
    material.SetFloat("_MaskType", 1f);
}

矩形相关参数我就不一一解释了 和圆形代码参数类似

第二点:事件穿透

这里策划提出的需求是不克隆拷贝引导玩家要点击的按钮,我的做法是将

GetComponent<EventPermeate>().target = targetGo.gameObject

进行事件穿透,只触发目标的点击事件,
EventPermeate.cs源码如下:

public class EventPermeate : MonoBehaviour,IPointerClickHandler,IPointerDownHandler, IPointerUpHandler
{
    [HideInInspector]
    public GameObject target;

    public void OnPointerDown(PointerEventData eventData)
    {
        PassEvent(eventData, ExecuteEvents.pointerDownHandler);
    }
    public void OnPointerUp(PointerEventData eventData)
    {
        PassEvent(eventData, ExecuteEvents.pointerUpHandler);
    }
    public void OnPointerClick(PointerEventData eventData)
    {
        PassEvent(eventData, ExecuteEvents.submitHandler);
        PassEvent(eventData, ExecuteEvents.pointerClickHandler);
    }
    public void PassEvent<T>(PointerEventData data, ExecuteEvents.EventFunction<T> function)
        where T : IEventSystemHandler
    {
        List<RaycastResult> results = new List<RaycastResult>();
        EventSystem.current.RaycastAll(data, results);
        GameObject current = data.pointerCurrentRaycast.gameObject;
        for (int i = 0; i < results.Count; i++)
        {
            if (target == results[i].gameObject)
            {
                ExecuteEvents.Execute(results[i].gameObject, data, function);
                break;
            }
        }
    }
}

第三点:业务需求

策划定了两张表,cfg_noviceGuideBase,引导基础表,cfg_noviceGuideStep引导分步表

id :引导的主键ID,skip :该引导触发后延迟几秒显示跳过按钮 防止用户引导中卡死,condition:该引导触发条件,parameter:参数,priority:优先级,firstStep:引导触发后开始的第一步id 关联到-cfg_noviceGuideStep。
对于:引导分步表


我们的引导有断线重连的概念 – 玩家进行引导ID为1,步骤1002 时手机突然断网掉线/或没电关机、再次上线 ID为1的引导是否继续、从哪个步骤开始 由nextStep和key 决定
下面开始说明lua 代码相关


转载:https://blog.csdn.net/qq_19666459/article/details/106489759
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场