unity spine 实现不规则区域的点击

在unity的ui中,美术提供的spine动画可能是不规则形状的,会有很多透明区域。当这种spine作为可点击区域的时候就很容易点到我们不希望响应的地方,特别是当元素比较密集的时候。

比如一个世界地图,如果每个版块是用spine来做的,那边界就非常容易误触。

初步想到的几种解决方案

1、用2d形状碰撞器。但是连线画边界很麻烦,而且做不到一劳永逸。或许可以自己写组件,但这里不详细说了。
2、改用组件Image,它是内置支持对透明区域进行点击过滤的,或许可以借用Image组件来作为Button的交互媒介,只要Image裁剪成spine的形状即可。
3、对SkeletonGraphic进行扩展(继承),使其像Image那样可以支持对被点击到的位置进行二次确认。

这里建议用第3种方法,最简单可靠。

第2种 用Image替代

我最先尝试的方案,成功了,但是没完全成功,会与Mask组件冲突。需要用Mask的可以直接跳过这里。

思路:先绘制spine,写入模板值。再绘制作为响应触媒的Image,比较模板值。关掉spine的raycast target,通过Image接收点击给Button。
实现过程:
1、内置的UI-Default.shader支持模板测试,直接新建个材质调参数就行了。
在这里插入图片描述
compare=3代表模板值要求相等,ID是要比较的id,operate=0代表通过模板测试之后不用改变模板值。其它的数字含义可以自己查。

2、spine的内置shader——"Spine/SkeletonGraphic"虽然也支持,但是被隐藏了,没有显示到编辑器面板上,所以自己拷贝了一下,去掉这些地方的HideInInspector特性

        _StencilComp ("Stencil Comparison", Float) = 8 //8 = always, 2=less, 3=equal
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0 //0=keep, 2=replace

然后同样新建个材质,传入参数
在这里插入图片描述
8代表总是通过测试,id写2,操作2代表将模板缓冲区的值替换为id的值。
需要勾选alpha clip,这样才会让那些纯透明区域的模板值保持为默认值。

3、在spine节点下面新增一个Image节点,挂上Button组件。替换spine和Image的材质为刚刚新建的。
4、设置这个Image的初始透明度为一个阈值,比如0.1,不干扰显示就行。代码里设置Image.alphaHitTestMinimumThreshold是0.1,如此透明度小于0.1的区域将不会接收点击。

结果,绘制一个类似非洲的版块如下图,这里为了方便查看,我把Image的初始透明度调高了。
在这里插入图片描述

问题
当这个spine上级有任意一个拥有Mask组件的父节点时,这个方法会失效。

因为裁剪图片的方法是用模板测试,而内置的Mask组件会强制修改所有子节点的模板测试,使自己写的模板测试无法生效
强制替换涵盖了所有子节点(包括层次更深的子节点)身上的Image和SkeletonGraphic等图形组件。可以去查看Mask.cs和SkeletonGraphic.cs里的代码。

第3种 重写SkeletonGraphic

既然Image都能实现,那spine应该也能实现。
先看看Image是怎么实现的,它实现了ICanvasRaycastFilter接口,方法是这样

public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
            if (alphaHitTestMinimumThreshold <= 0)
                return true;
            if (alphaHitTestMinimumThreshold > 1)
                return false;
                ……
                ……
}

然后 Graphic.cs里的Raycast方法会检查碰到的图形组件有没有ICanvasRaycastFilter。
那我们也实现一下这个接口就行了。内置的SkeletonGraphic没有实现,所以需要自己继承一个出来。

using Spine;
using UnityEngine;
using Spine.Unity;

#if NEW_PREFAB_SYSTEM
	[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[RequireComponent(typeof(CanvasRenderer), typeof(RectTransform)), DisallowMultipleComponent]
[AddComponentMenu("Spine/SkeletonGraphicTest")]
public class SkeletonGraphicTest : SkeletonGraphic, ICanvasRaycastFilter
{
    public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
    {
        // Texture2D t2 = this.mainTexture as Texture2D; //这个是图集,别直接采样,你没有uv坐标
        // foreach (var VARIABLE in TexturesMultipleCanvasRenderers.Items)
        // {
        //     Texture t23 = VARIABLE;
        // }

        if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, sp, eventCamera, out Vector2 local))
            return false;

        MatchRectTransformWithBounds(); //要算点击位置的相对偏移了,更新一下尺寸

        var size = GetComponent<RectTransform>().sizeDelta;
        var localOffsetX = local.x + size.x * 0.5f; //local的值相对于RectTransform的中心点,得转换成相对于左下角的
        var localOffsetY = local.y + size.y * 0.5f;

        // 获取当前绘制出来的的 Attachment 图片
        foreach (var slot in this.Skeleton.DrawOrder)
        {
            if (slot.Attachment is RegionAttachment regionAttachment)
            {
                var region = regionAttachment.Region as AtlasRegion;
                var page = region.page;
                var mat = page.rendererObject as Material;
                Texture2D texture2D = mat.mainTexture as Texture2D;

                float initPosX = 0; //区域的左下角相对于图集的位置
                float initPosY = 0;
                if (region.degrees == 90)
                {
                    //正常按BLX、BLY等坐标算,左下角为0,0,然后顺时针排列,到最后右下角是7,8
                    initPosX = texture2D.width * regionAttachment.UVs[0];
                    initPosY = texture2D.height * regionAttachment.UVs[1];
                }
                else
                {
                    //否则右下角才是0,0,然后顺时针排列,到最后右上角是7,8
                    initPosX = texture2D.width * regionAttachment.UVs[2];
                    initPosY = texture2D.height * regionAttachment.UVs[3];
                }

                int clickPointX = (int)(initPosX + localOffsetX); //点击的点相对于图集的位置
                int clickPointY = (int)(initPosY + localOffsetY);

                Color pixelColor = texture2D.GetPixel(clickPointX, clickPointY);

                if (pixelColor.a > 0.1)
                {
                    return true;
                }
            }
        }

        return false;
    }
}

有几个地方解释一下,spine比起Image不好处理的一点在于,它用的是图集纹理,在渲染时动态生成网格和uv。如果我们直接拿接收到的点击位置去采样主纹理的话,会去采样图集,结果必然是错的。后面做的一切一切都是为了获取 点击位置对应到图集纹理的uv坐标。

spine的图集会被划分成多个区域,专有类型为AtlasRegion,这里包含了渲染实时我们需要的信息。

点击spine一个位置,可能会点到多个重叠的区域,所以用了循环,只要有任意一个区域点到了不透明就算。

最后,自己写的spine组件在编辑器面板上不好看怎么办,把内置的编辑器类也拷贝过来。

using Spine.Unity.Editor;
using UnityEditor;

[CustomEditor(typeof(SkeletonGraphicTest))]
public class SkeletonGraphicInspectorTest : SkeletonGraphicInspector
{

}

至此,可以直接用我们自己的spine组件来实现不规则区域点击了。开启spine的raycast target,在spine节点下挂个Button即可,无需额外的Image。

顺便记下1个问题:
有的spine有多张导出图
这情况我还没测试到,不确定好不好使,但如果有问题应该也是可以解决的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值