Unity3D对象池的设计

文章目录

      • 为什么要使用对象池
      • 对象池的分类
      • 对象池存在的问题
      • 对象池的适用范围
        • 对象池适用于以下情况
        • 对象池不适用于以下情况
      • 对象池的设计
      • 具体实现
      • 使用例
        • 原本的旧脚本
        • 使用对象池改造后的脚本
      • 修订

为什么要使用对象池

绝大部分游戏需要涉及到同一个预制体的反复生成和销毁,比如

  • 枪战游戏在发射时需要生成子弹对象,而子弹击中敌人或者离开视线范围需要销毁子弹对象
  • 消消乐游戏消除时需要销毁被消除的对象,同时上方会生成新的对象掉落补充空位
  • 横板跑酷类游戏需要从右端不断的生成金币,被玩家采集或者到左端需要销毁金币

如果使用Instantiate(GameObject)生成对象,需要从硬盘或缓存中拷贝对应的预制体到内存中。如果使用Destroy(GameObject)销毁对象,同样需要清空对应的内存区域,这些操作会增加CPU,内存和硬盘的消耗,使游戏性能降低。

实际上,如果使用上述方式管理对象,每个对象只会被使用一次。如果能够重复使用这些对象,就能降低生成和销毁的次数,进而减少性能消耗。

对象池就是基于这个思想的设计。如果要生成对象,不使用Instantiate(GameObject)直接生成,而是从对象池中复用对应的对象,对象池为空时再生成新对象。如果要销毁对象,不使用Destroy(GameObject)直接销毁,而是将对象放进对象池中,对象池满了再销毁旧对象。

对象池的分类

一般来说,对象池有通用池专用池两种类型

  • 通用池的设计理念类似缓存,会把所有标记销毁的对象放到同一个池子里。当需要从对象池生成时,按照从旧到新的顺序遍历,直到找到合适的对象。如果对象池满了,再销毁时间最早的对象。
  • 专用池只负责管理其中一种对象,由于每个对象都是相同的,生成和销毁时不需要考虑时间的先后顺序,只需要对第一个元素(或最后一个元素)操作即可。

两种方式各有优劣,通用池需要单独设计查找和缓存算法,性能上略低于专用池,但扩展容易,适合需要长时间运营和更新的游戏。

专用池不需要设计查找和缓存算法,设计起来比较简单,但每一个对象都要设置专用的对象池,扩展比较麻烦,适合独立游戏或玩法固定的游戏。

对象池存在的问题

  1. 读脏数据的问题
    从对象池生成的对象可能可能会保持上一次使用的状态使其和Instantiate(GameObject)生成的对象相比存在差异。
  2. 初始化的问题
    上述第一个问题的衍生,如果对象挂有脚本,从对象池生成将不会执行Start()函数从而产生一些未初始化的问题
  3. 对象池的大小和预缓存
    如果对象池大小不合适,反而会对性能造成影响。
    如果对象池太小,对象池很容易空或者满,导致依然需要生成和销毁对象。
    如果对象池太大,对象池本身也会影响性能。
    另外,如果游戏刚开始对象池是空的,则游戏起始也需要预先生成,影响刚开始的性能,不过当对象池有足够的对象时问题就能得到缓解。如果对开始时的性能也很在意,可以预先在对象池中生成需要调用的对象,然后再开始游戏。
  4. 对象生成的速度
    如果对象生成的太快,超过了销毁的速度,则对象池容易空,如果对象生成的太慢,超过销毁的速度,则对象池容易满,这两种情况都会使对象池形同虚设。

对象池的适用范围

对象池适用于以下情况

  • 需要反复生成的和销毁的预制体
  • 每个副本的行为应该相近
  • 场景几乎不会影响到预制体的行为
  • 生成和销毁的速度基本上一致

对象池不适用于以下情况

  • 只生成不销毁的预制体
  • 副本行为差异较大
  • 预制体会因为场景发生变化
  • 生成速度和销毁速度差异较大

对象池的设计

这里以专用池为例子,以简述对象池的需求。

在场景中建立空物体,并挂载对象池类,其中这个空物体的子级作为对象池。

对象池类需要有以下功能:

  1. 在游戏开始时预先生成对象,禁用后作为对象池的子物体
  2. 当需要生成时:
    如果对象池为空,则使用Instantiate(GameObject)生成新对象
    如果对象池不为空,则启用第一个子物体,变换到对应位置和方向,解除父子关系,并调用所挂载脚本的初始化函数。对象池长度减一;
  3. 当需要销毁时:
    如果对象池满了,则销毁第一个子物体,禁用物体后将该物体的父级设置为对象池空物体
    如果对象池没满则没有上述的销毁过程

初始化函数可以使用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日第一次修订,简化了初始化的过程

你可能感兴趣的:(Unity笔记,unity,c#,游戏引擎)