NGUI不规则按钮的实现
在项目开发中,我们遇到了实现不规则按钮的需求。在网上搜索后,发现并没有特别理想的解决方案。其中提及最多的方法是获取鼠标下方的像素,若为透明像素则不做处理。不过这种方法效率较低,起初我们也是采用该方法进行尝试。
后来,我想到Unity新加入了Polygon Collider 2D组件,它能够很好地为一个Sprite(这里指的是Unity原生的Sprite,而非NGUI的UISprite)包裹一圈碰撞体。于是,我尝试添加这个碰撞体并进行实验。
NGUI按钮消息的实现原理是将一个碰撞体绑定到GameObject对象上,然后使用Physics.RayCast来检测射线碰撞。然而,至少在我当前使用的NGUI版本(3.4.9)中,并没有对2D碰撞体的检测支持。好在NGUI是开源的,我们可以自己动手添加对2D碰撞体的支持。
具体实现步骤
1. 改造检测方法
UICamera中负责检测的模块是Raycast()方法,该方法在ProcessMouse()、ProcessTouches()和ProcessFakeTouches()这三个方法中被调用。Raycast()方法使用Physics进行检测,并不兼容任何Physics2D对象。
我的解决方案是复制一份Raycast()方法,将其命名为Raycast2D(),并在其中使用Physics2D.Raycast()来替代原有的逻辑。然后在上述三个调用Raycast()的地方添加对Raycast2D()的调用。这种简单的处理方式可以满足对2D碰撞体的检测需求,但在消息排序方面可能会存在问题,这里仅作为一种抛砖引玉的思路。
以下是Raycast2D()方法的代码实现:
static public bool Raycast2D (Vector3 inPos, out RaycastHit2D hit)
{
for (int i = 0; i < list.size; ++i)
{
UICamera cam = list.buffer[i];
// 跳过未激活的脚本
if (!cam.enabled || !NGUITools.GetActive(cam.gameObject)) continue;
// 转换为视口空间
currentCamera = cam.cachedCamera;
Vector3 pos = currentCamera.ScreenToViewportPoint(inPos);
if (float.IsNaN(pos.x) || float.IsNaN(pos.y)) continue;
// 如果在相机视口之外,则不做处理
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f) continue;
// 向屏幕发射射线
Ray ray = currentCamera.ScreenPointToRay(inPos);
// 射线投射到屏幕
int mask = currentCamera.cullingMask & (int)cam.eventReceiverMask;
float dist = (cam.rangeDistance > 0f) ? cam.rangeDistance : currentCamera.farClipPlane - currentCamera.nearClipPlane;
if (cam.eventType == EventType.World)
{
hit = Physics2D.Raycast(ray.origin, ray.direction, dist, mask);
if (hit.collider != null)
{
hoveredObject = hit.collider.gameObject;
return true;
}
continue;
}
else if (cam.eventType == EventType.UI)
{
RaycastHit2D[] hits = Physics2D.RaycastAll(ray.origin, ray.direction, dist, mask);
if (hits.Length > 1)
{
for (int b = 0; b < hits.Length; ++b)
{
GameObject go = hits[b].collider.gameObject;
UIWidget w = go.GetComponent<UIWidget>();
if (w != null)
{
if (!w.isVisible) continue;
if (w.hitCheck != null && !w.hitCheck(hits[b].point)) continue;
}
else
{
UIRect rect = NGUITools.FindInParents<UIRect>(go);
if (rect != null && rect.finalAlpha < 0.001f) continue;
}
mHit2D.depth = NGUITools.CalculateRaycastDepth(go);
if (mHit2D.depth != int.MaxValue)
{
mHit2D.hit2D = hits[b];
mHits2D.Add(mHit2D);
}
}
mHits2D.Sort(delegate(DepthEntry2D r1, DepthEntry2D r2) { return r2.depth.CompareTo(r1.depth); });
for (int b = 0; b < mHits2D.size; ++b)
{
#if UNITY_FLASH
if (IsVisible(mHits.buffer[b]))
#else
if (IsVisible(ref mHits2D.buffer[b]))
#endif
{
hit = mHits2D[b].hit2D;
hoveredObject = hit.collider.gameObject;
mHits2D.Clear();
return true;
}
}
mHits2D.Clear();
}
else if (hits.Length == 1)
{
Collider2D c = hits[0].collider;
UIWidget w = c.GetComponent<UIWidget>();
if (w != null)
{
if (!w.isVisible) continue;
if (w.hitCheck != null && !w.hitCheck(hits[0].point)) continue;
}
else
{
UIRect rect = NGUITools.FindInParents<UIRect>(c.gameObject);
if (rect != null && rect.finalAlpha < 0.001f) continue;
}
if (IsVisible(ref hits[0]))
{
hit = hits[0];
hoveredObject = hit.collider.gameObject;
return true;
}
}
continue;
}
}
hit = mEmpty2D;
return false;
}
2. 在处理方法中添加调用
在ProcessMouse()、ProcessTouches()和ProcessFakeTouches()方法中添加对Raycast2D()的调用:
if (!Raycast(Input.mousePosition, out lastHit)) hoveredObject = fallThrough;
if (hoveredObject == null) Raycast2D(Input.mousePosition, out lastHit2D);
if (hoveredObject == null) hoveredObject = genericEventHandler;
3. 添加相关变量和结构体
同时,在相应的位置添加以下代码:
static public RaycastHit2D lastHit2D;
static DepthEntry2D mHit2D = new DepthEntry2D();
static BetterList<DepthEntry2D> mHits2D = new BetterList<DepthEntry2D>();
static RaycastHit2D mEmpty2D = new RaycastHit2D();
struct DepthEntry2D
{
public int depth;
public RaycastHit2D hit2D;
}
static bool IsVisible(ref RaycastHit2D hit)
{
UIPanel panel = NGUITools.FindInParents<UIPanel>(hit.collider.gameObject);
if (panel == null || panel.IsVisible(hit.point))
{
return true;
}
return false;
}
static bool IsVisible(ref DepthEntry2D de)
{
UIPanel panel = NGUITools.FindInParents<UIPanel>(de.hit2D.collider.gameObject);
return (panel == null || panel.IsVisible(de.hit2D.point));
}
性能影响及说明
这种实现方式带来的性能损耗是每帧都需要多进行一次2D碰撞体的检测。由于个人精力有限,而且NGUI也在不断发展,说不定未来官方会将检测2D碰撞体的功能加入到NGUI中。因此,这里只是简单地复制了一份碰撞体检测的逻辑,虽然代码不够优雅,但确实能够实现预期的功能。