[Из песочницы] Простой пул объектов в Unity3D

В процессе разработки я столкнулся с необходимостью создания пула объектов. Прочитав эту и другие статьи, решил написать для своих нужд пул попроще с доступом к объекту по строке (названию префаба).

Итак, начнем. Пул состоит из четырех скриптов. Состояние вкл/выкл на объекте в пуле определяется его свойством Unity activeInHierarchy, чтобы не городить дополнительных переменных.

1. Pool Object


Компонент Pool Object должен находиться на любом объекте, используемом в пуле. Его основное предназначение — вернуть объект обратно в пул.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[AddComponentMenu("Pool/PoolObject")]
public class PoolObject : MonoBehaviour {

        #region Interface
        public void ReturnToPool () {
                gameObject.SetActive (false);
        }
        #endregion
}


У класса один-единственный метод. На самом деле можно было бы обойтись и без него, но таким образом мы разделяем объекты, предназначенные для пула и нет (уничтожаемые обычным способом).

2. Object Pooling


Идем дальше. Класс Object Pooling — собственно сам пул, который выдает свободные объекты по требованию и создает новые при нехватке.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[AddComponentMenu("Pool/ObjectPooling")]
public class ObjectPooling {

        #region Data
        List objects;
        Transform objectsParent;
        #endregion


Здесь objects — все объекты, содержащиеся в пуле, objectsParent используется только как их родитель в иерархии на сцене (чтобы не было простыни объектов).

Добавление происходит с помощью метода AddObject, принимающего образец, который нужно добавить и родителя в иерархии на сцене.

void AddObject(PoolObject sample, Transform objects_parent) {
                GameObject temp = GameObject.Instantiate(sample.gameObject);
                temp.name = sample.name;
                temp.transform.SetParent (objects_parent);
                objects.Add(temp.GetComponent ());
                if (temp.GetComponent ())
                        temp.GetComponent ().StartPlayback ();
                temp.SetActive(false);
        }


Создается Gameobject temp, ему присваивается имя образца, после чего он добавляется в наш List. Затем объект выключается до тех пор, пока его не «потребуют» снаружи.

Отдельно о строках:

             if (temp.GetComponent ())
                        temp.GetComponent ().StartPlayback ();


Введены они были, т.к. при создании объекта без них аниматор не стартовал (update не успевало вызваться). В итоге, когда при старте сцены создавалось, например 100 пуль и сразу выключалось, при запросе 50 пуль, аниматор стартовал у всех одновременно и начинались проседания фпс (на многих объектах постоянно проигрывается анимация). Если в проекте не предполагается использование большого числа объектов с аниматорами, данный код не нужен.

Рассмотрим инициализацию:

     public void Initialize (int count, PoolObject sample, Transform objects_parent) {
                objects = new List (); //инициализируем List
                objectsParent = objects_parent; //инициализируем локальную переменную для последующего использования
                for (int i=0; i


Второй метод данного класса — GetObject (), возвращающий Gameobject:

     public PoolObject GetObject () {
                for (int i=0; i


Логика проста — проходимся по листу, если какой-то из объектов в пуле выключен (т.е. свободен) — возвращаем его, иначе добавляем новый.

3. PoolManager


Следующий класс PoolManager управляет пулами различных объектов. Класс статический для упрощения доступа к объектам, т.е. не нужно создавать синглтоны, инстансы и прочее.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public static class PoolManager{
        private static PoolPart[] pools;
        private static GameObject objectsParent;

        [System.Serializable]
        public struct PoolPart {
                public string name; //имя префаба
                public PoolObject prefab; //сам префаб, как образец
                public int count; //количество объектов при инициализации пула
                public ObjectPooling ferula; //сам пул
        }


Вся информация хранится в структуре PoolPart.

Инициализация производится массивом этих структур: (ferula, возможно не совсем удачное название, но позволяет не запутаться в куче pool-ов):

     public static void Initialize(PoolPart[] newPools) {
                pools = newPools; //заполняем информацию
                objectsParent = new GameObject ();
                objectsParent.name = "Pool"; //создаем на сцене объект Pool, чтобы не заслонять иерархию
                for (int i=0; i


Второй метод данного статического класса — GetObject, аналог стандартного Instantiate, но по имени объекта. Он проверяет все существующие пулы, и если находит правильный — дергает его метод GetObject () у класса ObjectPooling:

     public static GameObject GetObject (string name, Vector3 position, Quaternion rotation) {
                GameObject result = null;
                if (pools != null) {
                        for (int i = 0; i < pools.Length; i++) {
                                if (string.Compare (pools [i].name, name) == 0) { //если имя совпало с именем префаба пула
                                        result = pools[i].ferula.GetObject ().gameObject; //дергаем объект из пула
                                        result.transform.position = position;
                                        result.transform.rotation = rotation; 
                                        result.SetActive (true); //выставляем координаты и активируем
                                        return result;
                                }
                        }
                } 
                return result; //если такого объекта нет в пулах, вернет null
        }


4. PoolSetup


Однако необходимо редактировать объекты, предназначенные для использования в пуле, и их количество, в инспекторе Unity. Для этого придется написать класс-обертку, наследника MonoBehaviour, вешающегося на объекты:

using UnityEngine;
using System.Collections;

[AddComponentMenu("Pool/PoolSetup")]
public class PoolSetup : MonoBehaviour {//обертка для управления статическим классом PoolManager
        
        #region Unity scene settings
        [SerializeField] private PoolManager.PoolPart[] pools; //структуры, где пользователь задает префаб для использования в пуле и инициализируемое количество 
        #endregion

        #region Methods
        void OnValidate() {
                for (int i = 0; i < pools.Length; i++) {
                        pools[i].name = pools[i].prefab.name; //присваиваем имена заранее, до инициализации
                }
        }

        void Awake() {
                Initialize ();
        }

        void Initialize () {
                PoolManager.Initialize(pools); //инициализируем менеджер пулов
        }
        #endregion

}


Данный класс должен быть один на сцене, в противном случае один из них затрет пулы другого.

Использование
Теперь мы можем вызывать объекты из пула так:

Gameobject bullet = PoolManager.GetObject (bulletPrefab.name, shotPoint.position, myTransform.rotation);


Возвращаем:

GetComponent.ReturnToPool ();


В результате пул работает и им достаточно просто пользоваться. Пара скринов:

Управление в редакторе:

8352bb8d153d48369fd1d803c125105c.png

Спавн пуль и кораблей:

69552593c1db4aac98b120bea127476a.png

Послесловие


Разумеется, у данной реализации множество недостатков. Перечислю основные:

1) Доступ по строке можно заменить доступом по, например, целочисленному ключу-идентификатору, что ускорило бы работу;
2) Нет обработки ошибок и исключений (методы просто вернут null), практически нет проверок;
3) Необходимость наличия на сцене по сути синглтона PoolSetup, хотя на него никто и не ссылается.

Полный код


PoolObject
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[AddComponentMenu("Pool/PoolObject")]
public class PoolObject : MonoBehaviour {
        #region Interface
        public void ReturnToPool () {
                gameObject.SetActive (false);
        }
        #endregion
} 


Object Pooling
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[AddComponentMenu("Pool/ObjectPooling")]
public class ObjectPooling {

        #region Data
        List objects;
        Transform objectsParent;
        #endregion
                
        #region Interface
        public void Initialize (int count, PoolObject sample, Transform objects_parent) {
                objects = new List ();
                objectsParent = objects_parent;
                for (int i=0; i ());
                if (temp.GetComponent ())
                        temp.GetComponent ().StartPlayback ();
                temp.SetActive(false);
        }
        #endregion

} 


PoolManager
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public static class PoolManager{
        private static PoolPart[] pools;
        private static GameObject objectsParent;

        [System.Serializable]
        public struct PoolPart {
                public string name;
                public PoolObject prefab;
                public int count;
                public ObjectPooling ferula;
        }

        public static void Initialize(PoolPart[] newPools) {
                pools = newPools;
                objectsParent = new GameObject ();
                objectsParent.name = "Pool";
                for (int i=0; i


PoolSetup
using UnityEngine;
using System.Collections;

[AddComponentMenu("Pool/PoolSetup")]
public class PoolSetup : MonoBehaviour {//обертка для управления статическим классом PoolManager
        
        #region Unity scene settings
        [SerializeField] private PoolManager.PoolPart[] pools;
        #endregion

        #region Methods
        void OnValidate() {
                for (int i = 0; i < pools.Length; i++) {
                        pools[i].name = pools[i].prefab.name;
                }
        }

        void Awake() {
                Initialize ();
        }

        void Initialize () {
                PoolManager.Initialize(pools);
        }
        #endregion
}


© Habrahabr.ru