好久没写博客了,变懒了,最近想写的东西不多,这次来说说对象池吧。
对象池就是缓存池,提前把对象创建出来加入缓存池,需要的时候直接取出来用,删除的时候把物体放入到缓存池而不是真的删除,通过内存来换效率,减少画面的卡顿。
对象池是属于项目的底层模块,会被很多地方调用。所以一定得好用(我们得站在使用者的角度上来看),参数应该尽量少,简单明了。接下来我们一步步来制作:
1·对象池类肯定是个单例这个没得说了:
public class GameObjectsPool
{
GameObject ObjParent = null;
private static readonly GameObjectsPool _instance = new GameObjectsPool();
public static GameObjectsPool instance
{
get
{
return _instance;
}
}
2·我们希望对象池可以进行分类,就像垃圾的分类回收一样,这样我们可以轻松的对某类对象进行处理、统计,比如删除某类型的所有对象,在获取对象时也只是从其分类下去找,简单、高效,所以我们得有这个一个容器:
private Dictionary<string, List<GameObject>> _dictGameObjects = new Dictionary<string, List<GameObject>>();
3·定义好了数据容器,那我们就可以来写创建接口了。因为对象分类,所以肯定要传入类型信息,对象要加载进来,需要相应地址。所以我们的入参有类型和路径就够了:
private GameObject CreateObject(string strType, string strPath)
{
if (_dictGameObjects.ContainsKey(strType)
&& _dictGameObjects[strType].Count > 0)
{
GameObject Obj = _dictGameObjects[strType][0];
_dictGameObjects[strType].RemoveAt(0);
return Obj;
}
return LoadPrefab(strType, strPath);
}
private GameObject LoadPrefab(string strType, string strPath)
{
GameObject obj = Resources.Load(strPath) as GameObject;
if (null == obj)
{
return null;
}
GameObject InsObj = GameObject.Instantiate(obj);
if (null == InsObj)
{
return null;
}
GlObjInfo comObjInfo = InsObj.AddComponent<GlObjInfo>();
if (null != comObjInfo)
{
comObjInfo.strTypeName = strType;
}
return InsObj;
}
函数开头先判断,如果缓存池里面有相应的对象,那么直接返回List中的第一个元素,如果已无多余的对象,那么根据路径加载一个新的对象。先忽视这个ObjInfo,马上就会提到它。
4·有了创建自然要有删除,因为对象必须要有类型信,所以我们的接口一般来说是长这样的:
public void Destroy(string strType, GameObject desGameObject)
但是这样我觉得还是不够完美,因为这个接口跟Unity自带的Destroy入参不一致,第一次看到这个接口的时候,使用者可能不太明白这个strType是什么东西。然后多了个入参使用起来也更不方便。所以在每个对象创建的时候都给其挂上了GlObjInfo的脚本,用来存储对象的类型信息:
public void Destroy(GameObject desGameObject)
{
if (null == desGameObject)
{
return;
}
GlObjInfo comObjInfo = desGameObject.GetComponent<GlObjInfo>();
if (null == comObjInfo)
{
return;
}
string strType = comObjInfo.strTypeName;
if (!_dictGameObjects.ContainsKey(strType))
{
_dictGameObjects.Add(strType, new List<GameObject>());
}
if (_dictGameObjects[strType].Contains(desGameObject)) //重复删除的情况
{
return;
}
if (null == ObjParent) //需要把删除的GameObject挂载到某个节点下方
{
ObjParent = new GameObject("GameObjetsPoolNode");
GameObject.DontDestroyOnLoad(ObjParent);
}
desGameObject.transform.SetParent(ObjParent.transform);
desGameObject.SetActive(false);
_dictGameObjects[strType].Add(desGameObject); //加入容器
}
删除的时候通过获取GlObjInfo的组件获取其类型信息,代码都有些注释,相信大家都能看得懂。
写到这里,对象池主要的功能创建、删除都有了,其他的只要在这基础上增加就行了。但是仍然感觉不太好,因为CreateObject接口的第二个参数是个地址,每次调用都得找到对应的Resource相对地址,那肯定很不爽,所以这也是上面的接口为Private的原因,我们应该在上面再封装一层,让使用者能够更简单的使用。我的方法是这样的,为每个类型的资源单独写一个Create接口,只用传入相应的枚举,利用枚举的Attribute信息来得知资源的加载路径,以UI为例:
public class GameObjPoolType
{
public enum UI
{
[Description("UIPrefab/Dlg_A")]
DLG_A,
}
}
public GameObject CreateObjByUIType(GameObjPoolType.UI eUIType)
{
string strInfo = eUIType.ToString();
return this.CreateObject(strInfo, strEditorUIPath + GetEnumDescription(eUIType.GetType().GetField(strInfo)));
}
private string GetEnumDescription(FieldInfo objFieldInfo)
{
if (null == objFieldInfo)
{
return string.Empty;
}
object[] objs = objFieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (objs == null || objs.Length == 0)
{
return string.Empty;
}
DescriptionAttribute da = objs[0] as DescriptionAttribute;
if (null == da)
{
return string.Empty;
}
return da.Description;
}
这样,我们就把可以把传入strType和strPath的CreateObject函数完全消化在类内部,对外只暴露一个GameObjPoolType下的UI枚举就好了,对象的创建和销毁也变得非常简单:
GameObject objTest = GameObjectsPool.instance.CreateObjByUIType(GameObjPoolType.UI.DLG_A);
GameObjectsPool.instance.Destroy(objTest);
至于其他例如预加载等锦上添花功能,只要在这个基础上继续编写就好了,非常简单。