起因:
最近领了个需求,需要给项目的弱引导增加个功能,判断它是否被其他UI遮挡住,如果被遮挡了就需要实时将它隐藏,遮挡结束则恢复显示,这个需求乍一看似乎有点不太变态,但细细想想似乎还是能够做到,以下将我的经验分享一下。
遮挡判断:
要想解决这个问题,最重要的是要知道一个ui怎么算是被遮挡了。
-
第一个思路是,我认为一张图像的组成是由贴图采样得到的像素点中所有RGBA值中所有alpha值大于0的部分, 我们需要用最后输出到屏幕上的贴图和原贴图做逐个像素的对比,如果有不一致的地方则认为被遮挡了。但这个明显实现起来过于耗,而且结果不一定尽人意,假如仅有一个像素点被覆盖了,我们也认为它被遮挡了是比较离谱的,虽然我们可以定义个百分比的阙值,但毕竟还是对需求妥协了,所以不如换个更简单的思路。
-
第二个思路,判断网格中所有顶点的采样得到像素值是否被其他颜色覆盖了,这样确实简化了不少,但是要想判断像素值,势必要开启所有图集资源的读写,仅仅是为了这个需求显然是不合理。
-
第三个思路,所以我们简化一步到位,直接判断ui的中心点,并且不判断它的像素值,而是通过一条射线找到该点上的所有graph组件,剔除掉透明的组件并排序好层级关系后,判断该ui是否被遮挡。
在遮挡这,我们即可采取第三个思路来做处理,还是额外说明一点,如果graph是目标子节点,也可以认为它并没遮挡目标,因为它本身就是目标节点的一部分。
自身显隐
除了遮挡会影响弱引导的指引,指向目标本身的显隐也会影响到弱引导的显示,所以我大致总结下有哪些因素会导致指向目标我们“看不见”
-
目标的gameObject.activeInHierarchy为false
-
目标graph的alpha值为0
-
目标上有CanvasGroup组件或其父对象上有CanvasGroup组件,且CanvasGroup组件的alpha值为0
-
目标被Mask组件或者RectMask2D组件裁剪
-
目标的canvasRender组件的cull属性为false(cull:指示是否忽略该渲染器发射的几何形状)
-
目标的canvasRender组件的absoluteDepth属性小于0(absoluteDepth:渲染器相对于根画布的深度)
-
目标不在屏幕范围内
思路总结
综上所述,我们大致就能得出一个结论,当没有任何非透明ui遮挡目标,且目标也未被隐藏,我们就认为弱引导可以正常的指向它,反之则需要隐藏弱引导。
接下来,我将通过代码实现下我们上面所诉的每个条件。
获得UI中心点上所有的UI
var rectTransform = targetGraphic.transform as RectTransform;
var worldPos = rectTransform.TransformPoint(rectTransform.rect.center);
var screenPos = Camera.main.WorldToScreenPoint(worldPos); // 用自己项目的ui摄像机来做转换,我这里demo演示用的Camera.main
// 简化处理,拿到场景中所有的Canvas
List<Canvas> canvases = GameObject.FindObjectsOfType<Canvas>().ToList();
for (int i = 0, icnt = canvases.Count; i < icnt; i++)
{
// 获得该canvas画布下所有注册进去的Graphic
var canvas = canvases[i];
var canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
}
判断ui是否被Mask遮挡住
先总结下Mask的实现原理,Mask激活时,会修改MaskGraphic的材质,该材质会对每个像素点进行标记,将标记结果存入模板缓存中,当子级UI渲染中,如果未通过模板缓冲区的测试,则会丢弃到未通过的像素。
一言难尽,先放代码,后面另开篇文章总结怎么判断ui是否被Mask和RectMask2D遮罩,它的原理和它能否被点击到相关
/// <summary>
/// 是否有被Mask组件或者RectMask2D裁剪,可以假设Rect中心点不显示了,就认为被裁剪了(当然也可以4个边点,我这里从简计算)
/// </summary>
/// <param name="graphic"></param>
/// <returns></returns>
private bool IsMaskCull(Image graphic, Camera eventCamera)
{
if (graphic == null)
{
return false;
}
var t = graphic.transform;
var worldPos = t.TransformPoint((t as RectTransform).rect.center);
var screenPos = eventCamera.WorldToScreenPoint(worldPos);
// 如果有Canvas,且Canvas的overrideSorting属性为true,则上层不再影响它们
bool continueTraversal = true;
bool valid = true;
List<Component> components = new List<Component>();
while (t != null)
{
t.GetComponents(components);
for (int i = 0; i < components.Count; i++)
{
var canvas = components[i] as Canvas;
if (canvas != null && canvas.overrideSorting)
{
continueTraversal = true;
}
var mask = components[i] as Mask;
var rectMask2D = components[i] as RectMask2D;
if (mask == null && rectMask2D == null)
{
continue;
}
var filter = components[i] as ICanvasRaycastFilter; // 很有意思一点,对于Mask和RectMask2D 点不到跟看不到一个道理
valid = filter.IsRaycastLocationValid(screenPos, eventCamera);
if (!valid)
{
return true;
}
}
t = continueTraversal ? t.parent : null;
}
return false;
}
判断canvasGroup是否alpha为0
/// <summary>
/// 返回节点或其父节点最近邻的CanvasGroup的alpah是否为0
/// </summary>
/// <returns></returns>
private bool IsCanvasGroupAlphaZero(Graphic graphic)
{
List<Component> components = new List<Component>();
var t = graphic.transform;
bool hasCanvasGroup = false;
float alpha = 1f;
while (t != null)
{
if (hasCanvasGroup)
{
break;
}
t.GetComponents(components);
for (int i = 0; i < components.Count; i++)
{
// 拿到最近邻的alpha
var group = components[i] as CanvasGroup;
if (group == null || !group.enabled)
{
continue;
}
alpha = group.alpha;
hasCanvasGroup = true;
break;
}
t = t.parent;
}
return alpha == 0;
}
判断grapha是否包含目标中心点
// 如果aabb包围盒不包含点击点则剔除
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, eventPosition, Camera.main, graphic.raycastPadding))
{
continue;
}
判断是否忽略canvasRender渲染
// 如果忽略canvasRender的渲染,或graphic深度不正常(graphic.depth == graphic.canvasRenderer.absoluteDepth)则剔除
if (graphic.canvasRenderer.cull || graphic.depth == -1)
{
continue;
}
判断材质是否透明
// 如果材质透明则剔除
if (graphic.color.a == 0f)
{
continue;
}
判断Canvs上的点击点是否在屏幕范围内
考虑多显示器的情况,获得鼠标在某个显示器上真正的坐标,判断鼠标有没有超出该显示器的长宽范围
/// <summary>
/// 判断Graphic是否在屏幕范围内,仅讨论canvas renderMode是Camera的情况
/// </summary>
/// <param name="graphic"></param>
/// <param name="eventCamera"></param>
/// <returns></returns>
private bool InScreen(Canvas canvas, PointerEventData eventData, Camera eventCamera)
{
// 该情况过于复杂,大概说下流程
// 1. 首先调用Display.RelativeMouseAt(eventData.position)方法来获取事件相对于屏幕的返回
// 2. 如果返回的位置不是零向量(Vector3.zero),则表示事件确实发生在某显示器上
// 3. 返回的事件位置(eventPosition)的z会作为显示器索引
// 4. 如果返回零向量,则意为多显示器系统在该平台上不受支持,在这种情况下,会假定认为是事件在当前显示器上发生
// 5. 最终根据显示器索引获得显示器分辨率(Display.displays[index])
// 6. 判断鼠标位置是否有超出屏幕分辨率
int displayIndex = canvas.targetDisplay;
var eventPosition = Display.RelativeMouseAt(eventData.position);
if (eventPosition != Vector3.zero)
{
int eventDisplayIndex = (int) eventPosition.z;
if (eventDisplayIndex != displayIndex)
{
return false;
}
}
else
{
eventPosition = eventData.position;
}
Vector2 pos;
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || eventCamera == null)
{
float w = Screen.width;
float h = Screen.height;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
w = Display.displays[displayIndex].systemWidth;
h = Display.displays[displayIndex].systemHeight;
}
pos = new Vector2(eventPosition.x / w, eventPosition.y / h);
}
else
{
pos = eventCamera.ScreenToViewportPoint(eventPosition);
}
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
{
return false;
}
return true;
}
参考资料
https://blog.csdn.net/u013477973/article/details/89343777
https://www.cnblogs.com/gwen-/p/16976033.html