聊聊Unity3d動態加載場景物件那些事兒。
眾所周知,在策划或美術設計完游戲場景地圖后,一個場景中可能會存在成千上萬個小的物件,比如石頭,木箱子,油桶,柵欄等等等等,這些物件並不是游戲中的道具,僅僅只是為了點綴場景,讓畫面更豐富,當然也可以被打碎之類的。那么基於手機平台內存等限制,如果我們在場景加載時就把所有的物件都加載進去,就會導致幾個問題:1.游戲場景進入過慢,因為要加載完所有物件,2.游戲手機可能內存不夠,直接崩潰,沒辦法,你加載的物件實在是太多了。而且更重要的是我們沒必要都加載進去這些物件,因為某些物件可能玩家從始至終都沒有遇到過,即使你加載了也不會被看到,無端浪費效率而已。基於此,我們需要設計一個根據中心點(一般是玩家位置)來加載與卸載一定范圍內物件的功能,這就是我們要說的動態加載場景物件。功能需要做到,在玩家移動到過程中,不斷加載半徑r之內的物件,不斷卸載半徑r2之內的物件,以此來剔除無用物件所占的空間,節省內存,優化游戲。
功能的設計有部分參考:http://blog.csdn.net/qinyuanpei/article/details/46499779,以示感謝。
要做到動態加載物件,我們需要先做到幾件事:
1.保存原始場景中的物件信息到xml。
2.玩家移動過程中根據位置與xml保存的信息比對,找到需要加載的物件與需要卸載的物件,do it。
3.由於頻繁加載與卸載物件會造成GC卡頓,我們需要用到緩沖池來保存一定數量的物件以循環利用,當達到最大緩沖數量時才進行卸載。
在開始實現之前,我先加入幾張圖片用來示例場景的一些信息。
圖1 原始場景信息
圖1的場景就是美術設計好地形后的靜態物件信息,該場景不會在游戲運行時加載,僅用於設計場景。1處DGOM是用來管理所有物件的父節點,類似2處的cubes是管理相同類型物件的一個父節點,該結點沒有預制體,僅僅是一個空的對象,3處就是具體的物件信息,在該功能設計中具體的物件需要有對應的prefab文件,但是prefab只取到第二層,即就是Model下的Cube不再細化到prefab級別,4處表示一些沒有父節點的物件
圖2 游戲場景信息
圖2就是運行游戲是要加載的場景,此場景有一個DGOM對象用以跟圖1中的DGOM對應,也就是后續動態加載物件的父節點
圖3 預制體信息
圖3表示場景中物件所對應的prefab路徑信息,文件夾名字與圖1中的層級對應
圖4 游戲場景展示(請保持低調圍觀)
舞台已經搭建完成,我們開始吧。。。
1.保存原始場景中的物件信息到xml
這里我們使用C#中的XmlDocument與XmlElement將場景物件信息進行保存,需要保存的主要屬性包括:名字,prefab路徑,tag,layer,位置,旋轉,縮放,如果是子節點還需包括父節點名稱Parent。場景中物件的信息只遍歷到DGOM開始的下面兩層,再深的子物體當作是跟父物體一起的預制,用以簡化場景復雜度,當然可以自己酌情修改。另外場景中的物件名一定要與prefab名稱相對應,Unity拖拽prefab到場景后會自動在物件后添加xxx (1)這樣的序號,需要刪除。類似圖1和圖3的對應關系。
代碼如下:

using System.Xml; using UnityEditor; using UnityEngine; namespace LightFramework { /// <summary> /// 將場景資源信息導出到xml文件 /// 注意:場景中物件的名稱必須與prefab的名稱一致,不可修改,否則加載失敗 /// 由於prefab拖動到場景后,相同名稱的物件會被自動在末尾添加xxx (1),需刪除" (1)"部分 /// ref: http://blog.csdn.net/qinyuanpei/article/details/46499779 /// </summary> public class CreateSceneGameObjectsXmlTool { [MenuItem("ExportSceneToXml/Export")] static void ExportGameObjectsToXml() { string scenePath = UnityEngine.SceneManagement.SceneManager.GetActiveScene().path; string sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; string savePath = EditorUtility.SaveFilePanel("將當前場景導出為Xml", "", sceneName, "xml"); GameObject _dgom = GameObject.Find("DGOM");//場景資源父節點 if (null == _dgom) return; //創建Xml文件 XmlDocument xmlDoc = new XmlDocument(); //創建根節點 XmlElement scene = xmlDoc.CreateElement("Scene"); scene.SetAttribute("Name", sceneName); scene.SetAttribute("Asset", scenePath); xmlDoc.AppendChild(scene); //Object.FindObjectsOfType()返回所有active的物體;Resources.FindObjectsOfTypeAll()返回包括非active的所有物體 foreach (GameObject go in Object.FindObjectsOfType(typeof(GameObject))) //Resources.FindObjectsOfTypeAll(typeof(GameObject)) { //僅導出場景中的父物體 if (null != go.transform.parent && go.transform.parent.gameObject == _dgom) { //創建每個物體Node XmlElement gameObject = getGameObjectNode(xmlDoc, go); //添加物體到根節點 scene.AppendChild(gameObject); } } xmlDoc.Save(savePath); } static XmlElement getGameObjectNode(XmlDocument xmlDoc, GameObject go, string parentName = null) { //創建每個物體,該物體必須存在對應的prefab文件 XmlElement gameObject = null; if(string.IsNullOrEmpty(parentName)) { gameObject = xmlDoc.CreateElement("GameObject"); gameObject.SetAttribute("Name", go.name); gameObject.SetAttribute("Asset", "Prefabs/DGOM/" + go.name/* + ".prefab"*/);//物件所對應prefab路徑 } else { gameObject = xmlDoc.CreateElement("ChildObject"); gameObject.SetAttribute("Name", go.name); gameObject.SetAttribute("Asset", "Prefabs/DGOM/" + parentName + "/" + go.name/* + ".prefab"*/);//物件所對應prefab路徑 gameObject.SetAttribute("Parent", parentName); } //創建Transform XmlElement position = xmlDoc.CreateElement("Position"); position.SetAttribute("x", go.transform.position.x.ToString()); position.SetAttribute("y", go.transform.position.y.ToString()); position.SetAttribute("z", go.transform.position.z.ToString()); gameObject.AppendChild(position); //創建Rotation XmlElement rotation = xmlDoc.CreateElement("Rotation"); rotation.SetAttribute("x", go.transform.eulerAngles.x.ToString()); rotation.SetAttribute("y", go.transform.eulerAngles.y.ToString()); rotation.SetAttribute("z", go.transform.eulerAngles.z.ToString()); gameObject.AppendChild(rotation); //創建Scale XmlElement scale = xmlDoc.CreateElement("Scale"); scale.SetAttribute("x", go.transform.localScale.x.ToString()); scale.SetAttribute("y", go.transform.localScale.y.ToString()); scale.SetAttribute("z", go.transform.localScale.z.ToString()); gameObject.AppendChild(scale); //創建Tag XmlElement tag = xmlDoc.CreateElement("Tag"); tag.SetAttribute("tag", go.tag); gameObject.AppendChild(tag); //創建Layer XmlElement layer = xmlDoc.CreateElement("Layer"); layer.SetAttribute("layer", LayerMask.LayerToName(go.layer)); gameObject.AppendChild(layer); //場景物件子節點只遍歷到第二層 if (string.IsNullOrEmpty(parentName)) { int childCount = go.transform.childCount; int idx = 0; Transform childTrans = null; for (idx = 0; idx < childCount; ++idx) { childTrans = go.transform.GetChild(idx); XmlElement childObject = getGameObjectNode(xmlDoc, childTrans.gameObject, go.name); gameObject.AppendChild(childObject); } } return gameObject; } } }
保存完成的xml文件如下:

<Scene Name="TestSource" Asset="Assets/Test/Scenes/TestSource.unity"> <GameObject Name="Capsule" Asset="Prefabs/DGOM/Capsule"> <Transform x="-1.25" y="-2.06" z="3.244" /> <Rotation x="0" y="0" z="1.344026E-06" /> <Scale x="1" y="1" z="1" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </GameObject> <GameObject Name="Sphere" Asset="Prefabs/DGOM/Sphere"> <Transform x="-0.58" y="2.53" z="0" /> <Rotation x="358.4108" y="2.544795" z="301.9647" /> <Scale x="0.6404072" y="0.8599776" z="0.9996151" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </GameObject> <GameObject Name="capsules" Asset="Prefabs/DGOM/capsules"> <Transform x="1" y="0" z="0" /> <Rotation x="0" y="0" z="0" /> <Scale x="1" y="1" z="1" /> <Tag tag="Untagged" /> <Layer layer="Default" /> <ChildObject Name="Capsule" Asset="Prefabs/DGOM/capsules/Capsule" Parent="capsules"> <Transform x="1" y="0" z="2.12" /> <Rotation x="0" y="0" z="1.344026E-06" /> <Scale x="1" y="1" z="1" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </ChildObject> <ChildObject Name="Capsule" Asset="Prefabs/DGOM/capsules/Capsule" Parent="capsules"> <Transform x="-3.19" y="0" z="-0.09" /> <Rotation x="0" y="0" z="1.344026E-06" /> <Scale x="1" y="1" z="1" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </ChildObject> <ChildObject Name="Capsule" Asset="Prefabs/DGOM/capsules/Capsule" Parent="capsules"> <Transform x="3.401" y="0" z="2.12" /> <Rotation x="0" y="0" z="1.344026E-06" /> <Scale x="1" y="1" z="1" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </ChildObject> </GameObject> <GameObject Name="models" Asset="Prefabs/DGOM/models"> <Transform x="1" y="0" z="0" /> <Rotation x="0" y="0" z="0" /> <Scale x="1" y="1" z="1" /> <Tag tag="Untagged" /> <Layer layer="Default" /> <ChildObject Name="Model" Asset="Prefabs/DGOM/models/Model" Parent="models"> <Transform x="-0.13" y="1.76" z="-3.91" /> <Rotation x="0" y="0" z="0" /> <Scale x="1" y="1" z="1" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </ChildObject> <ChildObject Name="Model" Asset="Prefabs/DGOM/models/Model" Parent="models"> <Transform x="-2.26" y="3.36" z="-2.79" /> <Rotation x="0" y="0" z="0" /> <Scale x="1" y="1" z="1" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </ChildObject> </GameObject> <GameObject Name="cubes" Asset="Prefabs/DGOM/cubes"> <Transform x="1" y="0" z="0" /> <Rotation x="0" y="0" z="0" /> <Scale x="1" y="1" z="1" /> <Tag tag="Untagged" /> <Layer layer="Default" /> <ChildObject Name="Cube" Asset="Prefabs/DGOM/cubes/Cube" Parent="cubes"> <Transform x="3.56" y="1.6" z="-4.66" /> <Rotation x="16.77504" y="331.1571" z="297.6572" /> <Scale x="0.6404073" y="0.9012424" z="0.9583509" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </ChildObject> <ChildObject Name="Cube" Asset="Prefabs/DGOM/cubes/Cube" Parent="cubes"> <Transform x="3.36" y="-1.08" z="-2.21" /> <Rotation x="16.77504" y="331.1571" z="297.6572" /> <Scale x="0.6404103" y="0.9012403" z="0.9583499" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </ChildObject> </GameObject> <GameObject Name="spheres" Asset="Prefabs/DGOM/spheres"> <Transform x="1" y="0" z="0" /> <Rotation x="0" y="0" z="0" /> <Scale x="1" y="1" z="1" /> <Tag tag="Untagged" /> <Layer layer="Default" /> <ChildObject Name="Sphere" Asset="Prefabs/DGOM/spheres/Sphere" Parent="spheres"> <Transform x="0.74" y="-0.16" z="-5.85" /> <Rotation x="358.4108" y="2.544795" z="301.9647" /> <Scale x="0.64041" y="0.8599803" z="0.99962" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </ChildObject> <ChildObject Name="Sphere" Asset="Prefabs/DGOM/spheres/Sphere" Parent="spheres"> <Transform x="-2.98" y="-3.88" z="0.28" /> <Rotation x="358.4108" y="2.544795" z="301.9647" /> <Scale x="0.64041" y="0.8599803" z="0.99962" /> <Tag tag="Untagged" /> <Layer layer="Default" /> </ChildObject> </GameObject> </Scene>
2.玩家移動過程中根據位置與xml保存的信息比對,找到需要加載的物件與需要卸載的物件,do it。
在將信息保存到TestSource.xml后,我們將該文件放置到StreamingAssets文件夾下用於在游戲啟動時加載。將所有物件的信息保存到一個Dictionary中,Dictionary的key值為一個自增的id,從1開始累加,value值為包含了物件各個屬性的一個對象。需要定義一個枚舉用以標識當前物件的加載狀態,如WillLoad,Loaded等。游戲運行時需要每幀調用函數計算出距離玩家為圓心半徑為r的范圍內所有需要加載對象個數,進行加載,但是如果個數較多同時加載會卡頓,所以要分幀處理,限制每幀加載的最大個數,卸載同理,這里有個設置,如果計算需要加載的對象信息判斷出其在將要卸載的物件列表中,那么只需要將其從將要刪除列表移出即可,然后改變器狀態,不再需要重復加載,這個設計好處是避免玩家在一個小范圍進出導致不斷加載與卸載物件,浪費效率。
當然還需要提供另外一個接口,用戶輸入一個點,我們將其范圍內的所有物件都加載完成后再通知用戶,這是為了處理斷線重連進入時,讓玩家進入場景后可以看到一個真實的場景而非逐漸加載的場景,更符合現實。
代碼如下:

using System.Collections; using System.Collections.Generic; using UnityEngine; namespace LightFramework.Utils { public enum LoadState { None, WillLoad, Loaded, WillDetroy, Detroyed } public class GameObjectTransInfo { public int id; public string name; public string parentname; public string prefabname; public string tag; public string layer; public Vector3 position; public Vector3 rotation; public Vector3 scale; public LoadState state { get; set; } public GameObject go { get; set; } public GameObject parentGo { get; set; } public GameObjectTransInfo(int _id, string _name, string _parentname, string _prefabname, string _tag, string _layer, Vector3 _pos, Vector3 _rot, Vector3 _scale) { this.id = _id; this.name = _name; this.parentname = _parentname; this.prefabname = _prefabname; this.tag = _tag; this.layer = _layer; this.position = _pos; this.rotation = _rot; this.scale = _scale; this.state = LoadState.None; this.go = null; this.parentGo = null; } } /// <summary> /// 動態加載場景物件管理 /// </summary> public class DynamicLoadSceneObjUtil : Singleton<DynamicLoadSceneObjUtil> { public GameObject DGOM { get; set; } /// <summary> /// 加載\卸載對象判定中心 /// </summary> public Vector3 centerPos { get; set; } /// <summary> /// 距centerPos小於等於該值的物件進行加載 /// </summary> public int loadRadius { get; set; } /// <summary> /// 距centerPos大於該值的物件進行卸載 /// </summary> public int destroyRadius { get; set; } /// <summary> /// 每幀加載數量 /// </summary> public int loadNumPerFrame { get; set; } /// <summary> /// 每幀卸載數量 /// </summary> public int destroyNumPerFrame { get; set; } public bool isManual { get; set; } private Dictionary<int, GameObjectTransInfo> allObjsDic = new Dictionary<int, GameObjectTransInfo>(); private Dictionary<int, GameObjectTransInfo> alreadyLoadObjsDic = new Dictionary<int, GameObjectTransInfo>(); private Queue<GameObjectTransInfo> willLoadObjInfoQueue = new Queue<GameObjectTransInfo>(); private List<GameObjectTransInfo> willDestroyObjsInfoList = new List<GameObjectTransInfo>(); private int id = 0;//物件的唯一id,遞增 public override void onInit() { centerPos = Vector3.zero; loadRadius = 5; destroyRadius = 5; loadNumPerFrame = 1; destroyNumPerFrame = 1; isManual = false; DGOM = GameObject.Find("DGOM");//場景資源父節點 //GameObjectPoolUtil.instance.cacheMaxNum = 2; } public IEnumerator LoadDynamicScene(string sceneXml, System.Action callback) { string mainXml = UtilPath.WWWStreamingAssetPath + "/" + sceneXml; WWW www = new WWW(mainXml); yield return www; XmlCfgBase configDoc = new XmlCfgBase(); configDoc.parseXml(www.text); if (configDoc.mXmlConfig.Tag == "Scene") { ArrayList nodes = new ArrayList(); UtilXml.getXmlChildList(configDoc.mXmlConfig, "GameObject", ref nodes); getObjInfo(nodes); } callback.Invoke(); } public void getObjInfo(ArrayList nodes) { string tag = ""; string layer = ""; //定義物體位置、旋轉和縮放 Vector3 position = Vector3.zero; Vector3 rotation = Vector3.zero; Vector3 scale = Vector3.zero; //遍歷每一個物體 foreach (System.Security.SecurityElement xe1 in nodes) { //遍歷每一個物體的屬性節點 foreach (System.Security.SecurityElement xe2 in xe1.Children) { //根據節點名稱為相應的變量賦值 if (xe2.Tag == "Position") { float x = 0f; UtilXml.getXmlAttrFloat(xe2, "x", ref x); float y = 0f; UtilXml.getXmlAttrFloat(xe2, "y", ref y); float z = 0f; UtilXml.getXmlAttrFloat(xe2, "z", ref z); position = new Vector3(x, y, z); } else if (xe2.Tag == "Rotation") { float x = 0f; UtilXml.getXmlAttrFloat(xe2, "x", ref x); float y = 0f; UtilXml.getXmlAttrFloat(xe2, "y", ref y); float z = 0f; UtilXml.getXmlAttrFloat(xe2, "z", ref z); rotation = new Vector3(x, y, z); } else if (xe2.Tag == "Scale") { float x = 0f; UtilXml.getXmlAttrFloat(xe2, "x", ref x); float y = 0f; UtilXml.getXmlAttrFloat(xe2, "y", ref y); float z = 0f; UtilXml.getXmlAttrFloat(xe2, "z", ref z); scale = new Vector3(x, y, z); } else if (xe2.Tag == "Tag") { UtilXml.getXmlAttrStr(xe2, "tag", ref tag); } else if (xe2.Tag == "Layer") { UtilXml.getXmlAttrStr(xe2, "layer", ref layer); } else if (xe2.Tag == "TODO") { //and so on ... } } string name = ""; UtilXml.getXmlAttrStr(xe1, "Name", ref name); string parentname = ""; UtilXml.getXmlAttrStr(xe1, "Parent", ref parentname); string prefabName = ""; UtilXml.getXmlAttrStr(xe1, "Asset", ref prefabName); ++id; allObjsDic.Add(id, new GameObjectTransInfo(id, name, parentname, prefabName, tag, layer, position, rotation, scale)); //子節點信息 ArrayList childnodes = new ArrayList(); UtilXml.getXmlChildList(xe1, "ChildObject", ref childnodes); if(childnodes.Count > 0) { getObjInfo(childnodes); } } } public void GetWillLoadObjsInfo() { foreach (var item in allObjsDic) { var objinfo = item.Value; var offset = objinfo.position - centerPos; var sqrLen = offset.sqrMagnitude; if (sqrLen <= loadRadius * loadRadius) { if(objinfo.state == LoadState.None || objinfo.state == LoadState.WillDetroy || objinfo.state == LoadState.Detroyed) { if(willDestroyObjsInfoList.Contains(objinfo)) { int index = willDestroyObjsInfoList.IndexOf(objinfo); objinfo.state = LoadState.Loaded; alreadyLoadObjsDic.Add(item.Key, objinfo); willDestroyObjsInfoList.RemoveAt(index); } else { objinfo.state = LoadState.WillLoad; willLoadObjInfoQueue.Enqueue(objinfo); } } } } } public void _LoadPrefabs() { int curNum = 0; while(willLoadObjInfoQueue.Count > 0 && curNum < loadNumPerFrame) { var item = willLoadObjInfoQueue.Peek(); if(string.IsNullOrEmpty(item.parentname)) { willLoadObjInfoQueue.Dequeue(); } else { var parentgo = allObjsDic[item.id].parentGo; if (null == parentgo) { parentgo = GameObject.Find(item.parentname); } if (null == parentgo) { //父節點還未生成,該子節點暫不加載 continue; } else { willLoadObjInfoQueue.Dequeue(); } } //var item = willLoadObjInfoQueue.Dequeue(); GameObject go = GameObjectPoolUtil.instance.getObj(item.prefabname, null, item.tag, item.layer); if(go) { OnGetGoFromPool(item.id, go); } else { var prefab = GameObjectPoolUtil.instance.getPrefab(item.prefabname); if(prefab) { go = GameObjectPoolUtil.instance.getObj(item.prefabname, prefab, item.tag, item.layer); OnGetGoFromPool(item.id, go); } else { //資源加載 ResourceManager.GetResource(item.id, item.prefabname, OnInstantiateObj, OnPrefabLoadFailed); } } ++curNum; } } private void OnInstantiateObj(int id, object content) { GameObject _prefab = content as GameObject; GameObjectPoolUtil.instance.cachePrefab(allObjsDic[id].prefabname, _prefab); GameObject go = GameObjectPoolUtil.instance.getObj(allObjsDic[id].prefabname, _prefab, allObjsDic[id].tag, allObjsDic[id].layer); OnGetGoFromPool(id, go); } private void OnPrefabLoadFailed(int id) { //沒有預制體,說明是父節點,直接創建且不需要刪除 GameObject go = new GameObject(allObjsDic[id].name); OnGetGoFromPool(id, go, false); } private void OnGetGoFromPool(int id, GameObject go, bool isNeedDestroy = true) { if(string.IsNullOrEmpty(allObjsDic[id].parentname)) { if (allObjsDic[id].parentGo) { } else { go.transform.SetParent(this.DGOM.transform, false); allObjsDic[id].parentGo = this.DGOM; } } else { if(allObjsDic[id].parentGo) { } else { var parent = GameObject.Find(allObjsDic[id].parentname); go.transform.SetParent(parent.transform, false); allObjsDic[id].parentGo = parent; } } go.transform.position = allObjsDic[id].position; go.transform.rotation = Quaternion.Euler(allObjsDic[id].rotation); go.transform.localScale = allObjsDic[id].scale; allObjsDic[id].state = LoadState.Loaded; allObjsDic[id].go = go; allObjsDic[id].go.name = allObjsDic[id].name; if(isNeedDestroy) { alreadyLoadObjsDic.Add(id, allObjsDic[id]); } } public void SetWillDestroyObjsInfo() { foreach (var item in alreadyLoadObjsDic.Values) { var offset = item.position - centerPos; var sqrLen = offset.sqrMagnitude; if (sqrLen > destroyRadius * destroyRadius && allObjsDic[item.id].state != LoadState.WillDetroy) { allObjsDic[item.id].state = LoadState.WillDetroy; willDestroyObjsInfoList.Add(allObjsDic[item.id]); } } } public void _DestroyObj() { int curNum = 0; while (willDestroyObjsInfoList.Count > 0 && curNum < destroyNumPerFrame) { var item = willDestroyObjsInfoList[0]; allObjsDic[item.id].state = LoadState.Detroyed; GameObjectPoolUtil.instance.cacheObj(allObjsDic[item.id].prefabname, allObjsDic[item.id].go); allObjsDic[item.id].go = null; alreadyLoadObjsDic.Remove(item.id); willDestroyObjsInfoList.RemoveAt(0); ++curNum; } } public void dynamicLoadAndDestroyObj() { if(!isManual) { //加載 GetWillLoadObjsInfo(); _LoadPrefabs(); //卸載 SetWillDestroyObjsInfo(); _DestroyObj(); } } /// <summary> /// 加載指定中心范圍內的所有物件,加載完成后回調 /// </summary> /// <param name="_centerPos"></param> /// <param name="callback"></param> public IEnumerator manualDynamicLoadObj(Vector3 _centerPos, System.Action callback) { centerPos = _centerPos; //加載 GetWillLoadObjsInfo(); while (willLoadObjInfoQueue.Count > 0) { _LoadPrefabs(); yield return null; } callback.Invoke(); } } }
3.由於頻繁加載與卸載物件會造成GC卡頓,我們需要用到緩沖池來保存一定數量的物件以循環利用,當達到最大緩沖數量時才進行卸載。
由於需要根據玩家位置不斷的加載與卸載物件,所以要頻繁的Instantiate與Destroy一個物件,但是頻繁的調用這兩個函數,效率很低,所以我們需要犧牲一些內存,通過空間換時間的做法將不再需要的物件先不進行卸載,而是緩存下來,當下次需要生成同種類的物件時先去緩沖池查找,找到后直接更新屬性進行使用,找不到才生成一個物件。而緩存的物件我們是不能將其渲染出來的,但是Unity本身的SetAcite()與SetParent()調用效率很低,所以我們使用改變物件layer的方式進行隱藏物件,這樣只需要調整相機的culling mask就可以了,這樣隱藏物件時並不需要移動其位置與改變其可見性。當然每種物件我們也不能將其全部緩存下來,所以需要限制每種類型的最大緩存數量,緩存達到最大后,其他要緩存的物件則直接Destroy即可,當然每種物件的prefab文件也需要緩存下來,而不需要每次生成的時候都要load其prefab文件,提高效率。
代碼如下:

using System.Collections.Generic; using UnityEngine; namespace LightFramework.Utils { /// <summary> /// 緩存池 /// </summary> public class GameObjectPoolUtil : Singleton<GameObjectPoolUtil> { /// <summary> /// 對象池 /// int: prefab name hashcode /// queue: objQueue /// </summary> private Dictionary<int, Queue<GameObject>> pool; public int cacheMaxNum { get; set; } private string hideLayer = "CacheLayer";//隱藏對象直接設置層級,不移動對象,transform的SetParent()與SetAcite()調用很耗時 /// <summary> /// prefab緩存池 /// </summary> private Dictionary<int, GameObject> prefabPool; public override void onInit() { pool = new Dictionary<int, Queue<GameObject>>(); prefabPool = new Dictionary<int, GameObject>(); cacheMaxNum = 10; } public void cacheObj(string prefabName, GameObject _go) { int nameHashCode = prefabName.GetHashCode(); _go.setLayer(hideLayer, true); Queue<GameObject> queue = null; if (this.pool.TryGetValue(nameHashCode, out queue)) { if(null == queue) { queue = new Queue<GameObject>(); queue.Enqueue(_go); this.pool[nameHashCode] = queue; } else { if(queue.Count < cacheMaxNum) { queue.Enqueue(_go); } else { //超過最大緩存數量,直接刪除 GameObject.Destroy(_go); _go = null; } } } else { queue = new Queue<GameObject>(); queue.Enqueue(_go); this.pool.Add(nameHashCode, queue); } } public GameObject getObj(string prefabName, GameObject prefabGo, string tagName, string layerName) { GameObject go = null; int nameHashCode = prefabName.GetHashCode(); Queue<GameObject> queue = null; if (this.pool.TryGetValue(nameHashCode, out queue)) { if (null != queue && queue.Count > 0) { int len = queue.Count; go = queue.Dequeue(); } } if (null == go && null != prefabGo) { go = GameObject.Instantiate(prefabGo); } if(null != go) { //深度遍歷設置tag與layer go.setTag(tagName, true); go.setLayer(layerName, true); } return go; } public void clearObjsPool() { foreach(var item in pool) { foreach(var go in item.Value) { GameObject.Destroy(go); } item.Value.Clear(); } pool.Clear(); } public void cachePrefab(string prefabName, GameObject prefabGo) { int nameHashCode = prefabName.GetHashCode(); if(!this.prefabPool.ContainsKey(nameHashCode)) { this.prefabPool.Add(nameHashCode, prefabGo); } } public GameObject getPrefab(string prefabName) { int nameHashCode = prefabName.GetHashCode(); GameObject prefab = null; this.prefabPool.TryGetValue(nameHashCode, out prefab); return prefab; } public void clearPrefabsPool() { foreach (var item in prefabPool) { GameObject.Destroy(item.Value); } prefabPool.Clear(); } public void clear() { this.clearObjsPool(); this.clearPrefabsPool(); } } }
至此就可以在游戲中使用一個動態加載場景物件的功能了。需要注意的是上述代碼中有用到自己封裝的xml都寫工具類和資源加載類,所以這些代碼並不能在你的游戲中正確運行,需酌情修改。
完整demo路徑:https://gitee.com/planefight/LightFramework.git
拋磚引玉,歡迎探討。