聊聊Unity3d动态加载场景物件那些事儿。

众所周知,在策划或美术设计完游戏场景地图后,一个场景中可能会存在成千上万个小的物件,比如石头,木箱子,油桶,栅栏等等等等,这些物件并不是游戏中的道具,仅仅只是为了点缀场景,让画面更丰富,当然也可以被打碎之类的。那么基于手机平台内存等限制,如果我们在场景加载时就把所有的物件都加载进去,就会导致几个问题:1.游戏场景进入过慢,因为要加载完所有物件,2.游戏手机可能内存不够,直接崩溃,没办法,你加载的物件实在是太多了。而且更重要的是我们没必要都加载进去这些物件,因为某些物件可能玩家从始至终都没有遇到过,即使你加载了也不会被看到,无端浪费效率而已。基于此,我们需要设计一个根据中心点(一般是玩家位置)来加载与卸载一定范围内物件的功能,这就是我们要说的动态加载场景物件。功能需要做到,在玩家移动到过程中,不断加载半径r之内的物件,不断卸载半径r2之内的物件,以此来剔除无用物件所占的空间,节省内存,优化游戏。

功能的设计有部分参考:http://blog.csdn.net/qinyuanpei/article/details/46499779,以示感谢。

要做到动态加载物件,我们需要先做到几件事:

1.保存原始场景中的物件信息到xml。

2.玩家移动过程中根据位置与xml保存的信息比对,找到需要加载的物件与需要卸载的物件,do it。

3.由于频繁加载与卸载物件会造成GC卡顿,我们需要用到缓冲池来保存一定数量的物件以循环利用,当达到最大缓冲数量时才进行卸载。

在开始实现之前,我先加入几张图片用来示例场景的一些信息。

Unity3d 动态加载场景物件与缓存池的使用-LMLPHP

图1  原始场景信息

图1的场景就是美术设计好地形后的静态物件信息,该场景不会在游戏运行时加载,仅用于设计场景。1处DGOM是用来管理所有物件的父节点,类似2处的cubes是管理相同类型物件的一个父节点,该结点没有预制体,仅仅是一个空的对象,3处就是具体的物件信息,在该功能设计中具体的物件需要有对应的prefab文件,但是prefab只取到第二层,即就是Model下的Cube不再细化到prefab级别,4处表示一些没有父节点的物件

Unity3d 动态加载场景物件与缓存池的使用-LMLPHP

图2  游戏场景信息

图2就是运行游戏是要加载的场景,此场景有一个DGOM对象用以跟图1中的DGOM对应,也就是后续动态加载物件的父节点

Unity3d 动态加载场景物件与缓存池的使用-LMLPHP

图3  预制体信息

图3表示场景中物件所对应的prefab路径信息,文件夹名字与图1中的层级对应

Unity3d 动态加载场景物件与缓存池的使用-LMLPHP

图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 = ;
Transform childTrans = null;
for (idx = ; 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

抛砖引玉,欢迎探讨。

05-03 23:20