在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有多张导出图
这情况我还没测试到,不确定好不好使,但如果有问题应该也是可以解决的。