游戏可谓是吃CPU和GPU的大户,虽然我等做的是个极小的游戏,但凡事从小做起,那怕只有那么一点优化的空间,也要把它榨出来,才是我们的追求。
进入正题:
性能优化之对象池
我们知道游戏里有很多东西是需要动态创建的,像,子弹,敌人,塔等物体。如果东西用完了怎么办,销毁呗,Destroy(Object)多简单。但我们要知道实例化(Instantiate)物体的执行过程是比较慢的。同时大量的Instantiate和Destroy对应到底层就是大量的申请,释放内存,结果就是造成了大量的内存碎片。想想在这个塔防游戏里满天飞的子弹导弹,一秒要创建几十次,一秒后又得把他删除,CPU不难受就怪了
既然不要把物体Destory,那我们如何通过另一种方式“销毁”呢,其实我们销毁的本质不过是让玩家看不到嘛,那方式就多了去了,常用的还是gameObject.SetActive(false),
还有其实最快的是直接移出相机外,这也是一种“销毁”嘛, 我用的还是设置状态,因为设置的时候Unity有回调函数的执行有时候比较方便
/// <summary>
/// 激活时调用
/// </summary>
private void OnEnable()
{
}
/// <summary>
/// 关闭时调用
/// </summary>
private void OnDisable()
{
}
OnEnable();和OnDisable();会在每次激活或关闭后都执行
OK,这一大堆动态创建的物体总算不用从内存中消失了,那我们当然要对这群东西进行管理了, 对象池就这么来了,顾名思义,把对象都放到一个池子里,需要时把它初始化一下就拿出来用。
首先我们得有个总的对象池
里面有各种具体的池,如:某种子弹的池子,某个怪的池子。
我们将这些池子以字典形式存储
using System.Collections.Generic;
using UnityEngine;
//总的对象池,里面对不同的池进行管理
public class ObjectPool : Singleton<ObjectPool>
{
//资源所在目录
public string ResourceDir = "";
//字典形式存储
Dictionary<string, SubPool> m_pools = new Dictionary<string, SubPool>();
//取对象
public GameObject Spawn(string name)
{
if (!m_pools.ContainsKey(name))
RegisterNew(name);
SubPool pool = m_pools[name];
return pool.Spawn();
}
//回收对象
public void Unspawn(GameObject go)
{
SubPool pool = null;
foreach (SubPool p in m_pools.Values)
{
if (p.Contains(go))
{
pool = p;
break;
}
}
pool.Unspawn(go);
}
//回收所有对象
public void UnspawnAll()
{
foreach (SubPool p in m_pools.Values)
p.UnspawnAll();
}
//创建新子池子
void RegisterNew(string name)
{
//预设路径
string path = "";
if (string.IsNullOrEmpty(ResourceDir.Trim()))
path = name;
else
path = ResourceDir + "/" + name;
//加载预设
GameObject prefab = Resources.Load<GameObject>(path);
//创建子对象池
SubPool pool = new SubPool(transform, prefab);
m_pools.Add(pool.Name, pool);
}
}
接下来就是具体的池子了,里面存的就是我们动态创建的某种物体
using System.Collections.Generic;
using UnityEngine;
public class SubPool
{
Transform m_parent;
//预设
GameObject m_prefab;
//集合
List<GameObject> m_objects = new List<GameObject>();
//名字标识
public string Name
{
get { return m_prefab.name; }
}
//构造
public SubPool(Transform parent, GameObject prefab)
{
this.m_parent = parent;
this.m_prefab = prefab;
}
//取对象
public GameObject Spawn()
{
GameObject go = null;
foreach (GameObject obj in m_objects)
{
if (obj==null)
{
m_objects.Remove(obj);
}
else if (!obj.activeSelf)
{
go = obj;
break;
}
}
if (go == null)
{
go = UnityEngine.Object.Instantiate(m_prefab, m_parent, true);
m_objects.Add(go);
}
go.SetActive(true);
go.SendMessage("OnSpawn", SendMessageOptions.DontRequireReceiver);
return go;
}
//回收对象
public void Unspawn(GameObject go)
{
if(Contains(go))
{
go.SendMessage("OnUnspawn", SendMessageOptions.DontRequireReceiver);
go.SetActive(false);
}
}
//回收该池子的所有对象
public void UnspawnAll()
{
foreach(GameObject item in m_objects)
{
if (item==null)
{
m_objects.Remove(item);
}
else if (item.activeSelf)
{
Unspawn(item);
}
}
}
//是否包含对象
public bool Contains(GameObject go)
{
return m_objects.Contains(go);
}
}
对象池就这么写完了,当然为了有一个更好的规范,我们还可加上一个接口,让物体用接口后受对象池的管理
public interface IReusable
{
//当取出时调用
void OnSpawn();
//当回收时调用
void OnUnspawn();
}
来写个流程图更好的展示对象池是怎么工作的
直观感受下
在发射导弹前对象池有很多状态为false的物体
发射导弹的时候,池子里的导弹对象被拿出来用,状态激活
导弹爆炸,回收对象,状态设为false
同时可以看到爆炸的粒子效果也是对象池里的物体,在此时被激活
(橙色圈里的Bomb为爆炸粒子效果)
下一次发射同样利用这些对象
看到没,对资源的循环利用体现到了极致啊,这样的激活-关闭的模式极大提高了流畅度,同时避免了内存碎片。
原理与机制大概就是这样了,在unity里一定要注意,场景退出的时候,它会把场景里的物体统统销毁,这肯定造成大问题,我们就把这些对象放在game物体下,game是一个空物体,它上面挂载了整个游戏的管理脚本,并且设置为退出场景不销毁。这样他的子物体在退出场景的时候就不会销毁了,而是发送一个退出场景的消息,对象池收到后会自动地关闭已经激活的物体。