你使用对象池了吗

C#

浏览数:74

2019-8-31

AD:资源代下载服务

什么是对象池?
对象池,简单的说就是一种为了避免重复创建,删除对象的解决方案。它可以通过复用游戏对象,一定程度上提高游戏性能,避免内存消耗,特别针对任何频繁创建删除对象的情景。这符合了性能优化中对脚本优化的理念。是居家旅行…哦不…游戏开发必备技能。
对象池的实现原理?
创建复用对象的集合,通过查找和设置集合元素的active状态,达到和创建、删除一样的视觉效果。

这里先通过一个射击子弹的简单例子,从不用对象池,到给子弹创建对象池,去繁就简的一步步呈现对象池使用过程,深化理解。

不用对象池的小白脚本

Control.cs

...
public class Control:MonoBehaviour{
     public  GameObject bulletPrb;
     void Update(){
          if(Input.GetMouseButton(0))
                  Fire();
  }
     void Fire(){
      Instantiate(bulletPrb);
  }
}

Bullet.cs

...
public class Bullet:Monobehaviour{
     void Start(){
      Invoke("Destroy",2);
  }
     void Destroy(){
      Destroy(gameObject);  
  }
}

这里很容易的脑补出Hierarchry下,bullet对象一直在创建,销毁,创建,销毁。。。

使用对象池的一阶脚本
Control.cs

...
using System.Collections.Generic;

public class Control:MonoBehaviour{
     public  GameObject bulletPrb;

     public int poolingCapacity = 20;
     public List<GameObject> pool;
   
     void Start(){                 //创建对象池,并将对象设为不可见
          pool = new List<GameObject>()
          for(int i=0 ; i<poolingCapacity;i++)
                { 
                    GameObject bullet = Instantiate(bulletPrb);
                    pool.Add(bullet); 
                    pool[i].SetActive(false);
                }         
     }
     void Update(){
          if(Input.GetMouseButton(0))
                  Fire();
     }
     void Fire(){               
         //Instantiate(bulletPrb,transform.position,Quternion.identity);

         for(int i = 0 ; i < pool.Count; i++)
         {        //自动搜索对象池中失活的对象并激活显示
                 if ( !pool[i].activeinHierarchy )
                     {
                          pool[i].transform.position = transform.position;
                          pool[i].transform.rotation = transform.rotation;
                          pool[i].SetActive(true);
                          break;
                     }
         }
  }
}

Bullet.cs

...
public class Bullet:Monobehaviour{
     void OnEnable(){
      Invoke("Destroy",2);
  }
     void Destroy(){
       //Destory(gameObject);
       gameObject.SetActive(false);    //只要隐藏掉就好,对象还在对象池中
  }
     void OnDisable{
        CancelInvoke();
  }
}

这样对象池的功能算是实现了。然而它是被构建在控制脚本中的,这样显然不行。这就像编程小白把所有语句写在主函数中一样,这不符合面向对象的思想!因此,需要第三步

使用对象池的二阶脚本
新建一个ObjectBulletPool.cs 并挂载到新建的空物体ObjectPooler上

 ...
using System.Collections.Generic
public class ObjectPool: MonoBehaviour{
        public static ObjectPool current;

        public GameObject objectPrb;          //将被放入对象池中的预制体
        public int poolCapacity = 20;
        public List<GameObject> pool;
        public bool willgrow = true;           //池子容量不够时,是否自动扩展池子

        void Awake(){
            current = this;
     }
         void Start(){
             for(int i = 0 ; i<poolCapacity ; i++)
              {
                  GameObject obj = Instantiate(objectPrb);
                  pool.Add(obj);
                  obj.SetActive(false);
              }
     }
         public GameObject GetPooledObject(){
            for( int i = 0 ; i<pool.Count ; i++)    //遍历对象池,将未激活的对象传递出去
            {
                if ( ! pool[i].activeInHierarchy )
                        return pool[i];
            }
            if  ( willgrow )  //当池子中所有对象都激活了,但是还想激活显示对象时,扩展池子
            {
                GameObject obj = Instantiate(objectPrb);
                pool.Add(obj);
                obj.SetActive(false);
                return obj;
            }
           return null;
     } 
}

简化 Control.cs

...
using System.Collections.Generic;

public class Control:MonoBehaviour{
/*
     public  GameObject bulletPrb;
     public int poolingCapacity = 20;
     public List<GameObject> pool;
   
     void Start(){                 //创建对象池,并将对象设为不可见
          pool = new List<GameObject>()
           for(int i=0 ; i<poolingCapacity;i++)
               { 
                    GameObject bullet = Instantiate(bulletPrb);
                   pool.Add(bullet); 
                 pool[i].SetActive(false);
             }         
     }
*/
     void Update(){
          if(Input.GetMouseButton(0))
                  Fire();
     }
     void Fire(){               
         //Instantiate(bulletPrb,transform.position,Quternion.identity);
        /*
         for(int i = 0 ; i < pool.Count; i++)
         {        //自动搜索对象池中失活的对象并激活显示
                 if ( !pool[i].activeinHierarchy )
                     {
                          pool[i].transform.position = transform.position;
                          pool[i].transform.rotation = transform.rotation;
                          pool[i].SetActive(true);
                          break;
                     }
          }
        */
            //获取未激活对象,激活显示
          GameObject obj = ObjectPool.current.GetPooledObject();  
          if (obj == null) return;
          obj.transform.position = transform.position;
          obj.transform.rotation = transform.rotation;
          obj.SetActive(true);
  }
}

不用改 Bullet.cs

...
public class Bullet:Monobehaviour{
     void OnEnable(){
      Invoke("Destroy",2);
  }
     void Release(){
       //Destory(gameObject);
       gameObject.SetActive(false);    //只要隐藏掉就好,对象“回到”对象池中
  }
     void OnDisable{
        CancelInvoke();
  }
}

多说一句,如果你能发射不同类型的子弹怎么办?
解决思路:将子弹类型bulletPrb对象改成数组,在GetPooledObject()函数中传入子弹类型的参数,然后遍历数组匹配子弹类型,return出去。

小结:
这个对象池其实是固定的,一旦创建就不用删除,这样便于复用创建好的对象。使用对象其实就是在对象池中搜索没有active的对象并激活获取到它好进行操作。释放对象就是让他失活,好被以后搜索复用。
Ok,看到这里,对象池的基本用法就掌握了,在实际项目中需求会更加多变,但是万变不离本质。高阶运用也是从低阶进化的!

高阶运用请看这位老兄的文章:
unity中的通用对象池

作者:JervieQin