在做项目遇到一个不规则按钮的实现需求。网上搜了一圈,并没有啥好方案,说的最多的就是拿到鼠标下的像素,如果是透明的就不作处理。这种方法很费,一开始也是按照这种方法来做的。
后来我想起unity新加入一个Polygon Collider 2D,可以相当好地把一个Sprite(Unity Native Sprite,不是NGUI的UISprite)周围包上一圈Collider,于是就尝试加上这个Collider然后做实验。Ngui 按钮消息的实现原理是将一个Collider绑到GameObject对象上之后,用Physics.RayCast来检测射线碰撞。可惜,至少到目前我用的NGUI版本(3.4.9)并没有任何对2D 的Collider的检测。开放源码的好处就在这,自己动手加入对2D的支持就OK了。
static public bool Raycast2D (Vector3 inPos, out RaycastHit2D hit)
{
for (int i = 0; i < list.size; ++i)
{
UICamera cam = list.buffer[i];
// Skip inactive scripts
if (!cam.enabled || !NGUITools.GetActive(cam.gameObject)) continue;
// Convert to view space
currentCamera = cam.cachedCamera;
Vector3 pos = currentCamera.ScreenToViewportPoint(inPos);
if (float.IsNaN(pos.x) || float.IsNaN(pos.y)) continue;
// If it's outside the camera's viewport, do nothing
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f) continue;
// Cast a ray into the screen
Ray ray = currentCamera.ScreenPointToRay(inPos);
// Raycast into the screen
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;
if (!Raycast(Input.mousePosition, out lastHit)) hoveredObject = fallThrough;
if (hoveredObject == null) Raycast2D(Input.mousePosition, out lastHit2D);
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));
后来我想起unity新加入一个Polygon Collider 2D,可以相当好地把一个Sprite(Unity Native Sprite,不是NGUI的UISprite)周围包上一圈Collider,于是就尝试加上这个Collider然后做实验。Ngui 按钮消息的实现原理是将一个Collider绑到GameObject对象上之后,用Physics.RayCast来检测射线碰撞。可惜,至少到目前我用的NGUI版本(3.4.9)并没有任何对2D 的Collider的检测。开放源码的好处就在这,自己动手加入对2D的支持就OK了。
UICamera中负责检测的模块是 Raycast(),有3处调用了Raycast(),分别是 ProcessMouse(),ProcessTouches(),ProcessFakeTouches()。Raycast()中使用Physics来进行检测,并不兼容任何Physics2D对象。我的解决方案是拷贝一份Raycast()变为Raycast2D(),并在其中使用Physics2D.Raycast()来替代原有的逻辑。然后在3处使用Raycast()的地方加入Raycast2D()。这种极为简单的处理可以满足对2D collider的检测,但是在消息排序上可能会有问题。仅仅抛砖引玉。
Raycast2D()static public bool Raycast2D (Vector3 inPos, out RaycastHit2D hit)
{
for (int i = 0; i < list.size; ++i)
{
UICamera cam = list.buffer[i];
// Skip inactive scripts
if (!cam.enabled || !NGUITools.GetActive(cam.gameObject)) continue;
// Convert to view space
currentCamera = cam.cachedCamera;
Vector3 pos = currentCamera.ScreenToViewportPoint(inPos);
if (float.IsNaN(pos.x) || float.IsNaN(pos.y)) continue;
// If it's outside the camera's viewport, do nothing
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f) continue;
// Cast a ray into the screen
Ray ray = currentCamera.ScreenPointToRay(inPos);
// Raycast into the screen
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;
}
然后在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;
同时在相应的地方加入以下: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 collider的功能加入进去,所以我这里就简单的复制一遍collider的逻辑,虽然不怎么漂亮,但是it works。