为什么要使用对象池
绝大部分游戏需要涉及到同一个预制体的反复生成和销毁,比如
- 枪战游戏在发射时需要生成子弹对象,而子弹击中敌人或者离开视线范围需要销毁子弹对象
- 消消乐游戏消除时需要销毁被消除的对象,同时上方会生成新的对象掉落补充空位
- 横板跑酷类游戏需要从右端不断的生成金币,被玩家采集或者到左端需要销毁金币
如果使用Instantiate(GameObject)
生成对象,需要从硬盘或缓存中拷贝对应的预制体到内存中。如果使用Destroy(GameObject)
销毁对象,同样需要清空对应的内存区域,这些操作会增加CPU,内存和硬盘的消耗,使游戏性能降低。
实际上,如果使用上述方式管理对象,每个对象只会被使用一次。如果能够重复使用这些对象,就能降低生成和销毁的次数,进而减少性能消耗。
对象池就是基于这个思想的设计。如果要生成对象,不使用Instantiate(GameObject)
直接生成,而是从对象池中复用对应的对象,对象池为空时再生成新对象。如果要销毁对象,不使用Destroy(GameObject)
直接销毁,而是将对象放进对象池中,对象池满了再销毁旧对象。
对象池的分类
一般来说,对象池有通用池和专用池两种类型
- 通用池的设计理念类似缓存,会把所有标记销毁的对象放到同一个池子里。当需要从对象池生成时,按照从旧到新的顺序遍历,直到找到合适的对象。如果对象池满了,再销毁时间最早的对象。
- 专用池只负责管理其中一种对象,由于每个对象都是相同的,生成和销毁时不需要考虑时间的先后顺序,只需要对第一个元素(或最后一个元素)操作即可。
两种方式各有优劣,通用池需要单独设计查找和缓存算法,性能上略低于专用池,但扩展容易,适合需要长时间运营和更新的游戏。
专用池不需要设计查找和缓存算法,设计起来比较简单,但每一个对象都要设置专用的对象池,扩展比较麻烦,适合独立游戏或玩法固定的游戏。
对象池存在的问题
- 读脏数据的问题
从对象池生成的对象可能可能会保持上一次使用的状态使其和Instantiate(GameObject)
生成的对象相比存在差异。 - 初始化的问题
上述第一个问题的衍生,如果对象挂有脚本,从对象池生成将不会执行Start()
函数从而产生一些未初始化的问题 - 对象池的大小和预缓存
如果对象池大小不合适,反而会对性能造成影响。
如果对象池太小,对象池很容易空或者满,导致依然需要生成和销毁对象。
如果对象池太大,对象池本身也会影响性能。
另外,如果游戏刚开始对象池是空的,则游戏起始也需要预先生成,影响刚开始的性能,不过当对象池有足够的对象时问题就能得到缓解。如果对开始时的性能也很在意,可以预先在对象池中生成需要调用的对象,然后再开始游戏。 - 对象生成的速度
如果对象生成的太快,超过了销毁的速度,则对象池容易空,如果对象生成的太慢,超过销毁的速度,则对象池容易满,这两种情况都会使对象池形同虚设。
对象池的适用范围
对象池适用于以下情况
- 需要反复生成的和销毁的预制体
- 每个副本的行为应该相近
- 场景几乎不会影响到预制体的行为
- 生成和销毁的速度基本上一致
对象池不适用于以下情况
- 只生成不销毁的预制体
- 副本行为差异较大
- 预制体会因为场景发生变化
- 生成速度和销毁速度差异较大
对象池的设计
这里以专用池为例子,以简述对象池的需求。
在场景中建立空物体,并挂载对象池类,其中这个空物体的子级作为对象池。
对象池类需要有以下功能:
- 在游戏开始时预先生成对象,禁用后作为对象池的子物体
- 当需要生成时:
如果对象池为空,则使用Instantiate(GameObject)
生成新对象
如果对象池不为空,则启用第一个子物体,变换到对应位置和方向,解除父子关系,并调用所挂载脚本的初始化函数。对象池长度减一; - 当需要销毁时:
如果对象池满了,则销毁第一个子物体,禁用物体后将该物体的父级设置为对象池空物体
如果对象池没满则没有上述的销毁过程
初始化函数可以使用OnEnable实现,这个函数在物体启用时自动执行。
具体实现
对象池类,需要挂载在对应的对象池空物体上。
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
public int MaxSize = 128; //对象池的上限
public GameObject gameObjectType; //对象池用来管理的对象
public int CacheSize = 16; //对象池预先缓存的对象
private int size;
// 在Start函数中生成预缓存对象
void Start()
{
size = CacheSize;
if (CacheSize > MaxSize)
Debug.LogError(string.Format("缓存大小Cache{0}超出对象池大小MaxSize{1},请指定小于{1}的值!", CacheSize, MaxSize));
for (int i = 0; i < CacheSize; i++)
{
GameObject obj = Instantiate(gameObjectType);
obj.transform.parent = transform;
obj.SetActive(false);
}
}
public GameObject Instantiate(Vector3 position, Quaternion rotation)
{
GameObject obj0;
if (size == 0) //对象池没有对象,添加新对象
{
obj0 = Instantiate(gameObjectType, position, rotation);
}
else //对象池中有对象,把对象释放出来
{
obj0 = transform.GetChild(0).gameObject;
obj0.transform.position = position;
obj0.transform.rotation = rotation;
obj0.SetActive(true);
obj0.transform.parent = null;
size--;
}
return obj0;
}
public void Destroy(GameObject obj)
{
if (size == MaxSize)
{
Destroy((Object)transform.GetChild(0).gameObject);
size--;
}
obj.SetActive(false);
obj.transform.parent = transform;
size++;
}
}
使用例
使用起来非常简单,只需要给涉及生成和销毁的对象挂载对象池脚本,给需要使用对象池的对象所属脚本重写OnEnable()
,再替换原本的Instantiate(GameObject)
和Destroy(GameObject)
函数即可。
原本的旧脚本
挂在枪口上的脚本
using UnityEngine;
public class Gun: MonoBehaviour
{
public Transform bullet;
void Start(){}
void Update()
{
if (Input.GetKeyDown(KeyCode.Mouse0))
Instantiate(bullet,transform.position, transform.rotation);
}
}
挂在子弹预制体上的脚本
using UnityEngine;
public class Bullet: MonoBehaviour
{
public float speed;
public Transform gun;
void Start(){
gun = GameObject.Find("这里写枪的名字").transform;
}
void Update()
{
transform.Translate(Vector3.forward * speed * Time.deltaTime);
}
void FixedUpdate() //超出300米左右销毁,这里使用包围盒检测
{
if (new Bounds(gun.position, new Vector3(300, 300, 300)).Contains(transform.position))
return;
Destroy(gameObject);
}
private void OnTriggerEnter(Collider other)
{
if(other.tag == "Enemy")
{
Destroy(gameObject);
}
}
使用对象池改造后的脚本
挂在枪口上的脚本
using UnityEngine;
public class Gun: MonoBehaviour
{
public Transform bullet;
public ObjectPool pool;
void Start(){
//初始化操作
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Mouse0))
pool.Instantiate(transform.position, transform.rotation);
}
}
挂在子弹预制体上的脚本
using UnityEngine;
public class Bullet: MonoBehaviour
{
public float speed;
public Transform gun;
public ObjectPool pool;
void Start(){
//初始化操作
}
//重写OnEnabled()以实现对象池插入的初始化功能
void OnEnable()
{
Start();
}
void Update()
{
transform.Translate(Vector3.forward * speed * Time.deltaTime);
}
void FixedUpdate() //超出300米左右销毁,这里使用包围盒检测
{
if (new Bounds(gun.position, new Vector3(300, 300, 300)).Contains(transform.position))
return;
pool.Destroy(gameObject);
}
private void OnTriggerEnter(Collider other)
{
if(other.tag == "Enemy")
{
pool.Destroy(gameObject);
}
}
修订
2022-4-8日投稿
2022-4-12日第一次修订,简化了初始化的过程