对象池是游戏开发中常用的优化方法。
解决问题:在某些类型的游戏,相同的对象会多次创建和销毁,这些对象的创建十分耗时,因而,我们会以一部分内存为代价,将这部分对象缓存起来,并不去销毁它,在需要创建时,从缓存中将先前创建好的对象取出来使用。
在Unity游戏开发中,创建GameObject是一个费时的过程,本文将针对GameObject类创建一个对象池。因为是一个非常常用的优化手段,因而,我们需要把其能够方便的移植到其他项目中,不妨放到一个单一的常用工具的文件夹中,然后可以很方便的导出为package,并在其他项目中导入引用。
public class PoolOfGameObjects
{ private List<GameObject> pool;//存放提前创建好的对象缓存区 private GameObject sample;//这个对象池缓存的对象样本 private int index;//未使用对象的位置坐标(比如10个对象,使用了3个,index就会变成3,即pool[3]是下一个可用得对象) }首先,我们需要去创建这个池,即将它的属性初始化:
public PoolOfGameObjects(GameObject sample)
{ this.sample = sample;//不管怎么说,你都先要给对象池一个对象做样本,不然池子里该放什么呢? pool = new List<GameObject> ();//把池子初始化,不然你提前创建好的对象要放哪里呢? index = -1;//现在池子里是空的,所以你读取pool[-1]只会得到错误。 }接下来我们复制一些样本的副本到池子里:
private void Add(int count=1)
{ for (int i = 0; i < count; i++) { GameObject copy = GameObject.Instantiate (this.sample); copy.SetActive (false);//还没使用的物体就让它安静躺着池子里,不然可能会被渲染出来。 pool.Add (copy); } }这个方法是私有的,我们的池子很智能,不需要外部给我们加水,当池子的对象被耗尽,池子会自己往里添加更多能用的对象。
接下来,我们应该在什么时候往池里加对象呢?也许你的游戏有一个漂亮的加载界面,你也许想在这时候给池子里加上差不多够用的对象备用,也许你没有那么多内存去存放多余的对象,你甚至不清楚你会用到几个,或者你并没有足够的加载时间,只想吝啬的需要一个时添加一个(在编程时,希望你能做一个吝啬鬼)。
不管你是前者还是后者,既然想让更多的项目能够复用,我们得能同时满足两者的需要,为了前者,我们需要另一个构建函数:
public PoolOfGameObjects(GameObject sample,int capacity)
{ this.sample = sample; pool = new List<GameObject> (); Add(capacity); index = 0; }在初始化池子的时候,我们就往里面创建了用户预计会使用到的对象副本个数。
现在,你有了一个池子,也许里面还有一些对象供你使用,现在来规定一下你能用这个池子来做什么,首先,这个池子里的对象是供我们拿出来使用的,并且如果你有良好的管理习惯,你应该认为,用完再放回原处是理所当然的,我们一定要吝啬,如果你随意丢弃,那么当你把池子里的对象用尽时,不得不再请求cpu再为你创建一个,每创建一个副本,池子的体积就会越来越大,你可能认为,我已经把GameObject清理掉了,但池子并不会知道你以及把它的管理对象清理了,它还是会为对象保留一个位置。所以我们需要两个基本方法,借用和归还。
首先是借用:
public GameObject Borrow()
{ if (index >= 0 && index < pool.Count)//index属性保存着可用对象的下标,我希望能连续的取对象,而不是每次都去循环一遍List,假如100个中用掉99个,得需要循环多少次哦 { pool[index].SetActive(true); return pool[index++];//借出一个后,浮标移动到仅靠着得下一位 } else { Add();//不够得时候,得为池子增加一个对象 if (index < 0) { index = 0;//这里用index++也可以 } return Borrow(); //刚刚没有借到,现在增加了一个对象,总可以借给我了 } }有时候你需要一次性借多个,你总不希望要来好多趟吧:
public GameObject[] Borrow(int count)
{ GameObject[] order = new GameObject[count]; for (int i = 0; i < count; i++)//不要介意这里使用的循环,假如你把这个循环放到里层,创建GameObject的时间复杂度是一样的,但会多调用几次上面的方法,这里我不想为这一点性能,多写一部分代码。 { order[i] = Borrow(); } return order; }使用完之后,我们还要好好的把副本还回去:
public bool Lend(GameObject gameobject)
{ for (int i = 0; i < index; i++)//只需要在已经借出的列表前部分进行比对 { if (pool[i].Equals(gameobject))//的确是这个池子里的借出去的对象 { pool[i].SetActive(false); pool.Insert(pool.Count, pool[i]);//将对象插入到最后面待之后继续使用 pool.Remove(pool[i]);//将原来的空出来的位置去掉 index--;//浮标向前一位 return true;//归还成功 } } return false;//归还不成功 }同样的,归还你也不想重复好几次吧:
public GameObject[] Lend(GameObject[] gameobects)
{ List<GameObject> notMatch = new List<GameObject>(); for (int i = 0; i < gameobects.Length; i++) { if (!Lend(gameobects[i])) { notMatch.Add(gameobects[i]); } } return notMatch.ToArray(); }归还多个的变数多一些,我们将不匹配的对象拒退给用户。
最后,我们还需要有清理对象池的方法,有时候我们没有把握好初始化池子的大小,或者一开始我们用了很多副本,但是之后我们需要的很少,将未使用的副本清理出去:
public void Clear()
{ if (index >= 0) { for (int i = pool.Count-1; i >=index; i--) { if (!pool[i].activeSelf)//以防我们删掉正在使用的对象,这本是没有必要的。经过测试,有没有这个判断不会造成误删,但是多一层保险,有时候未必是坏事 { GameObject.Destroy(pool[i]); } } pool.RemoveRange(index, pool.Count - index);//把池子的容量恢复到刚好的尺寸 } }当你不想要这个池子的时候,需要销毁它来释放更多的内存:
public void Destory(bool force)//false时,仅仅销毁池子和未使用的对象,已经使用的对象不会被销毁,但也无法再归还回来;true时,已经使用的对象也会被强制销毁掉。
{ int start; if (force) { start = 0; } else { start = index; } for (int i = pool.Count - 1; i >= start; i--) { if ((force) || (!pool[i].activeSelf)) { GameObject.Destroy(pool[i]); } } pool.Clear(); }以上,就是一个GameObject对象池的基本实现,放心大胆的部署在你的各个Unity应用中,也许你还会碰到各种池,比如常见的线程池,总体的思路都是如此,具体实现会略有不同,你还可以创建一个统一的接口,添加各有特色的池,让你的池系统更加完善