最近在项目中碰到一个比较诡异的问题,让自己着实头疼了一阵子。
具体问题是这样的:
游戏中使用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 (); } } } }