NGUI 中 Panel 切换 Camera 之后丢失尺寸大小的问题

最近在项目中碰到一个比较诡异的问题,让自己着实头疼了一阵子。

具体问题是这样的:

游戏中使用 PoolManager 将使用过的所有界面 Panel 进行了缓存,在需要的时候从 Pool 中 Spawn。

而为了避免每个 Pool 中所有的 Prefab 实例在切换场景时被自动 Destory 掉,我选择了将这部分通用的弹出式的 UI 所储存的 Pool 设置为 dontDestroyOnLoad,这样一来这些界面就会一直存在内存里头,省去了来回内存释放又再次请求的过程,只需要每次从 Pool 里头 Spawn 就可以了。

当 PanelA 在 Scene_1 中显示过一次,那么 PanelA 就已经缓存到 UIPool 中了,当我们切换到 Scene_2 时,UIPool 并不会自动 Destory 掉。那么这个时候如果我们需要显示 PanelA,我们只需要调用 SpawnPool 的 Spawn 方法就可以直接把 PanelA 显示出来就好了。问题来了,这个时候显示的 PanelA 的尺寸完全不对,原本应该与屏幕宽度完全匹配的 PanelA 变成了宽度只有 2 个像素了。

我们这么辛苦地搞了一个自己认为可以节省内存重复申请和释放过程的机制,肯定不能因为碰到这个问题就放弃了对吧。那就开始找问题吧,那么为什么这个 PanelA 的宽度会最终变成了 2 呢?

首先,我们先来看看这个 Panel 的 GameObject 层级图:

UIPanel

|– UISprite(使用 Anchors 进行背景图尺寸和位置的设置)

|– UISprite

通过 Unity3D Editor 中,我们发现通过 Anchors 来进行尺寸和位置设置的背景 UISprite 的尺寸和位置相对于 UIPanel 这个根节点确实没有出错,Left Anchor 对应 UIPanel 的 Left 相对距离为 0,Right Anchor 对应 UIPanel 的 Right 相对距离为 0,Top Anchor 对应 UIPanel 的 Bottom 的相对距离为 100,Bottom Anchor 对应 UIPanel 的 Bottom 相对距离为 0。也就是说背景 UISprite 的高度还是正确的,但是由于整个 UIPanel 的尺寸出错了,所以导致了背景 UISprite 的尺寸和相对位置出现了错误。

那么怎么解决这个问题呢?首先我们可以确认 UISprite 是通过 Anchors 来设置尺寸和位置,我们可以在 NGUI 中找到对应的代码,这段代码在 UIWidget.cs 文件中的 OnAnchor() 方法中,这个方法中会获取 Anchor 设置中相对物体的边界信息,在我们这个案例中这个相对物体就是 UIPanel 了。UIPanel.cs 中通过 GetSides() 方法来获取 UIPanel 的尺寸和位置信息,而这个最终的计算会依赖于 UIPanel 所处 Layer 的 Camera。

这下问题就明朗了,因为我们将 UIPanel 在场景 A 中创建并显示出来,之后当 UIPanel 不再显示的时候将其缓存起来了,但是当我们再次从其他场景切换到场景 A 中时,我们又再次通过 Spawn 方法将 UIPanel 显示出来。此时 UIPanel 依然会调用其继承自 UIRect 的 GetSlides 方法来获取边界信息。

public virtual Vector3[] GetSides (Transform relativeTo)
{
if (anchorCamera != null) return mCam.GetSides(cameraRayDistance, relativeTo);

Vector3 pos = cachedTransform.position;
for (int i = 0; i < 4; ++i)
mSides[i] = pos;

if (relativeTo != null)
{
for (int i = 0; i < 4; ++i)
mSides[i] = relativeTo.InverseTransformPoint(mSides[i]);
}
return mSides;
}

而此时 anchorCamera 已经为空了,因为我们切换场景的时候,UIPanel 首次 Spawn 出来时自动选择的 UI 层的 Camera 对象已经被 Destroy 了, 而现在我们重新将 UIPanel 显示出来的时候,UIPanel 并不会自动重新选择当前场景中 UI 层的 Camera 对象,所以这里就需要我们手动来设置一下 UIPanel 的 Camera。

仔细看了一下 UIPanel 初始化的时候是如何设置 Anchors 的,然后发现了有ResetAnchors、UpdateAnchors 和ResetAndUpdateAnchors 和 ResetAndUpdateAnchors 就是我们想要找的货了。也就是说我们在当场景切换成功之后,应该手动将 SpawnPool 中缓存的 Panels 对象调用一遍 ResetAndUpdateAnchors 方法,这样一来就不会再出现因为丢失 Camera 对象,而导致 UIPanel 尺寸不正确的问题了。我们只需要在 OnLevelWasLoaded 的回调函数中这些处理一下就好了:

void OnLevelWasLoaded (int level)
{
Debug.Log ("OnLevelWasLoaded: " + level);
foreach (KeyValuePair<string, PrefabPool> pair in _uiSpawnPool.prefabPools) {
PrefabPool pool = pair.Value;
foreach (Transform xform in pool.spawned) {
UIPanel panel = xform.GetComponent ();
if (panel != null) {
panel.ResetAndUpdateAnchors ();
}
}
foreach (Transform xform in pool.despawned) {
UIPanel panel = xform.GetComponent ();
if (panel != null) {
panel.ResetAndUpdateAnchors ();
}
}
}
}