Все знают что операции создания и удаления объектов не дешевые. Например создавать каждый раз пулю и уничтожать, довольно накладно для тех же мобильных устройств. Может стоит не уничтожать пулю, а скрывать ее. Вот решил поделится своей реализацией Pool Manager.

Структуры


Для начала, нужно создать интерфейс:
public interface IPoolObject<T>
{
    T Group { get; }
    void Create();
    void OnPush();
    void FailedPush();   
}

Для чего нужен именно интерфейс? Универсально, пусть пул не знает с какими объектами работает, он только знает, что у них есть особые методы и свойства.
T — в этом случае идентификатор группы. Пример далее.
Метод Create() — будет играть роль псевдо-конструктора. Ведь когда вы достанете объект из пула, его состояние будет не определено, что может пагубно отразится на дальнейшем его использовании.
Метод OnPush() — будет играть роль псевдо-декструктора. Когда объект попадает в пул, возможно нужно что-то сделать, например выключить какой-то связанный партикл или еще что-то.
Метод FailedPush() — Будет вызван у объекта, в случае не возможности попадания в пул. Например пул заполнен. И что-то бы объект не остался бесхозный, возможно потребуется его уничтожение.

Теперь сам Pool Manager
using System.Collections.Generic;
using System;

public class PoolManager<K, V> where V :IPoolObject<K>
{
    public virtual int MaxInstances { get; protected set; }
    public virtual int InctanceCount { get { return objects.Count; } }
    public virtual int CacheCount { get { return cache.Count; } }

    public delegate bool Compare<T>(T value) where T: V;

    protected Dictionary<K, List<V>> objects;
    protected Dictionary<Type, List<V>> cache;

    public PoolManager(int maxInstance) 
    {
        MaxInstances = maxInstance;
        objects = new Dictionary<K, List<V>>();
        cache = new Dictionary<Type, List<V>>();
    }

    public virtual bool CanPush()
    {
        return InctanceCount + 1 < MaxInstances;
    }

    public virtual bool Push(K groupKey, V value)
    {
        bool result = false;
        if (CanPush())
        {
            value.OnPush();
            if (!objects.ContainsKey(groupKey))
            {
                objects.Add(groupKey, new List<V>());
            }
            objects[groupKey].Add(value);
            Type type = value.GetType();
            if (!cache.ContainsKey(type))
            {
                cache.Add(type, new List<V>());
            }
            cache[type].Add(value);
        }
        else
        {
            value.FailedPush();
        }
        return result;
    }

    public virtual T Pop<T>(K groupKey) where T : V
    {
        T result = default(T);
        if (Contains(groupKey) && objects[groupKey].Count > 0)
        { 
            for (int i = 0; i < objects[groupKey].Count; i++)
            {
                if (objects[groupKey][i] is T)
                {
                    result = (T)objects[groupKey][i];
                    Type type = result.GetType();
                    RemoveObject(groupKey, i);
                    RemoveFromCache(result, type);
                    result.Create();
                    break;
                }
            }
        }
        return result;
    }

    public virtual T Pop<T>() where T: V
    {
        T result = default(T);
        Type type = typeof(T);
        if (ValidateForPop(type))
        {
            for (int i = 0; i < cache[type].Count; i++)
            {
                result = (T)cache[type][i];
                if (result != null && objects.ContainsKey(result.Group))
                {
                    objects[result.Group].Remove(result);
                    RemoveFromCache(result, type);
                    result.Create();
                    break;
                }
                
            }
        }
        return result;
    }

    public virtual T Pop<T>(Compare<T> comparer) where T : V
    {
        T result = default(T);
        Type type = typeof(T);
        if (ValidateForPop(type))
        {
            for(int i = 0; i < cache[type].Count; i++) 
            {
                T value = (T)cache[type][i];
                if (comparer(value))
                {
                    objects[value.Group].Remove(value);
                    RemoveFromCache(result, type);
                    result = value;
                    result.Create();
                    break;
                }
              
            }
        }
        return result;
    }

  
    public virtual bool Contains(K groupKey)
    {
        return objects.ContainsKey(groupKey);
    }

    public virtual void Clear()
    {
        objects.Clear();
    }

    protected virtual bool ValidateForPop(Type type)
    {
        return cache.ContainsKey(type) && cache[type].Count > 0;
    }

    protected virtual void RemoveObject(K groupKey, int idx)
    {
        if (idx >= 0 && idx < objects[groupKey].Count)
        {
            objects[groupKey].RemoveAt(idx);
            if (objects[groupKey].Count == 0)
            {
                objects.Remove(groupKey);
            }
        }
    }

    protected void RemoveFromCache(V value, Type type)
    {
        if (cache.ContainsKey(type))
        {
            cache[type].Remove(value);
            if (cache[type].Count == 0)
            {
                cache.Remove(type);
            }
        }
    }
}

На что стоит обратить внимание.
MaxInstances — поле максимального количества pool объектов. В случае, если не возможно поместить в пул очередной объект.
protected Dictionary<K, List> objects -Контейнер представлен в виде группа — список. При попадании в пул, он отправляется в соответствующую группу. Когда он потребуется, по группе пул вернет первый соответствующий объект. protected Dictionary<Type, List> cache — Кеш объектов по типу. Нужен, исключительно, для красивого метода Pop().
Метод Pop(Compare comparer) — возможно потребуется, чтобы достать объект по условию.

Общий Pool готов. Теперь нужно сделать вариацию для Unity3d. Приступим

using UnityEngine;
using System.Collections;

public class UnityPoolManager : MonoBehaviour
{
    public static UnityPoolManager Instance {get; protected set;}

    public int maxInstanceCount = 128;

    protected PoolManager<string, UnityPoolObject> poolManager; 


    protected virtual void Awake()
    {
        Instance = this;
        poolManager = new PoolManager<string, UnityPoolObject>(maxInstanceCount);
    }


    public virtual bool CanPush()
    {
        return poolManager.CanPush();
    }

    public virtual bool Push(string groupKey, UnityPoolObject poolObject)
    {
        return poolManager.Push(groupKey, poolObject);
    }

    public virtual T PopOrCreate<T>(T prefab) where T : UnityPoolObject
    {
        return PopOrCreate(prefab, Vector3.zero, Quaternion.identity);
    }

    public virtual T PopOrCreate<T>(T prefab, Vector3 position, Quaternion rotation) where T : UnityPoolObject
    {
        T result = poolManager.Pop<T>(prefab.Group);
        if (result == null)
        {
            result = CreateObject<T>(prefab, position, rotation);
        }
        else
        {
            result.SetTransform(position, rotation);
        }
        return result;
    }

    public virtual UnityPoolObject Pop(string groupKey)
    {
        return poolManager.Pop<UnityPoolObject>(groupKey);
    }

    public virtual T Pop<T>() where T : UnityPoolObject
    {
        return poolManager.Pop<T>();
    }

    public virtual T Pop<T>(PoolManager<string, UnityPoolObject>.Compare<T> comparer) where T : UnityPoolObject
    {
        return poolManager.Pop<T>(comparer);
    }

    public virtual T Pop<T>(string groupKey) where T : UnityPoolObject
    {
        return poolManager.Pop<T>(groupKey);
    }

    public virtual bool Contains(string groupKey)
    {
        return poolManager.Contains(groupKey);
    }

    public virtual void Clear()
    {
        poolManager.Clear();
    }

    protected virtual T CreateObject<T>(T prefab, Vector3 position, Quaternion rotation) where T : UnityPoolObject
    {
        GameObject go = Instantiate(prefab.gameObject, position, rotation) as GameObject;
        T result = go.GetComponent<T>();
        result.name = prefab.name;
        return result;
    }
}

По сути это просто обвертка над первым пулом и Сингелтон. Можно было завернуть иначе. Но получилось бы что-то вроде UnityPoolManager.Instance.Pool.Pop(), а дополнительно создать только пару методов специально для юнити. Но это на усмотрение читателя. Кода получится меньше но появится дополнительный Pool.

PopOrCreate() — нам понадобится вот этот метод для создания объектов.
Push() — у самих пул объектов или Push в менеджере.

Теперь нам понадобится сам GameObject

using UnityEngine;
using System.Collections;

public class UnityPoolObject : MonoBehaviour, IPoolObject<string> 
{
    public virtual string Group { get {return name;} } // та самая группа
    public Transform MyTransform { get { return myTransform; } }

    protected Transform myTransform;

    protected virtual void Awake()
    {
        myTransform = transform;
    }

    public virtual void SetTransform(Vector3 position, Quaternion rotation)
    {
        myTransform.position = position;
        myTransform.rotation = rotation;
    }

    public virtual void Create() // конструктор для пула
    {
        gameObject.SetActive(true);
    }

    public virtual void OnPush() // деструктор для пула
    {
        gameObject.SetActive(false);
    }

    public virtual void Push() // вызов деструктора
    {
        UnityPoolManager.Instance.Push(Group, this);
    }

    public void FailedPush() // не возможно попасть в пул
    {
        Debug.Log("FailedPush"); // !!!
        Destroy(gameObject);
    }
}


Все объекты будем наследовать от него, и возможно переопределять методы Create и OnPush.

Теперь перейдем к использованию. На примере пули следов и UI List item.

public class Bullet : UnityPoolObject  // собственно наша пуля, опустим ее реализацию
{
...
}
// создание
 Bullet bullet = UnityPoolManager.Instance.PopOrCreate<Bullet>(bulletPrefab, bulletPoint.position, Quaternion.identity);
  bullet.Execute(sender, bulletPoint.position, CalculateTarget(target, accuracy01), damage, blockTime, range, bulletSpeed);
...
// уничтожение, точней возращаем в пул
// пуля летит какое-то время и уничтожается
timer-= Time.deltaTime;
 if (timer< 0)
            {
                Push(); 
            }


public class StepObject : UnityPoolObject // следы 
{
...
}

/// Создаем следы и запускаем плавный сброс альфа канала
 StepObject stepObject = UnityPoolManager.Instance.PopOrCreate<StepObject>(prefab, sender.position, sender.rotation);
            FXManager.Instance.InitDecal(null, stepObject.gameObject, hit, direction);
            stepObject.MyTransform.rotation *= rotation;
            StartCoroutine(ApplyDecalC(stepObject));
/// Детализация метода
protected virtual IEnumerator ApplyDecalC(StepObject decalObject)
    {
        yield return new WaitForSeconds(waitTime); // ждем какое-то время
        yield return StartCoroutine(FXManager.Instance.HideOjectC(decalObject.gameObject, hideTime)); // начинаем плавно уничтожать
        decalObject.Push(); // альфа в нуле, отправляем в пул
    }


public class ProfileListItem : UnityPoolObject 
{
...
}
 // УИ элемент - создание
 ProfileListItem profileItem = UnityPoolManager.Instance.PopOrCreate(prefab);

...
// различные манипуляции
 profileItem.profileId = profileId;
 list.AddItem(profileItem); // отправляем в список

// пример уничтожение, это очистка списка. Где для всех элементов вывозится Push
foreach(ProfileListItem item in list)
{
item.Push();
}


Надеюсь данный пример поможет вам в написании своих проектов на Unity3d. Отдельное спасибо @ lexxpavlov который подсказал, что нужно описать детальней чем просто интерфейс.

Комментарии (8)


  1. lexxpavlov
    10.04.2015 21:48
    +3

    Довольно обычная вещь, без изюминки. Таких пулов довольно много. Чем ваш реализация отличается от других пулов? Зачем вы решили делать свой велосипед?

    Лично я из бесплатных пользовался двумя — ObjectPool и PoolBoss. Первый попроще, второй понавороченнее. Оба могут работать через методы расширений — вызываются прямо у GameObject или Transform. PoolBoss дополнительно имеет настройки в редакторе — довольно удобно.

    Хм… Сейчас зашёл на страницу PoolBoss-а в AssetStore — он теперь не бесплатный… Вышла новая версия, Unity 5.0 ready


    1. derek_streyt Автор
      10.04.2015 22:08

      Туториал. Как то написал, использую до сих пор. Нужен был универсальный.


      1. ForhaxeD
        14.04.2015 08:31
        +1

        Это не туториал, это код с минимальными комментариями.


      1. devcor
        23.04.2015 00:37

        Два чая господину выше. Это не туториал. В туториалах люди чуть ли не каждую строчку объясняют, и не кусками кода, а помаленьку «пишут» вместе с читателями. А тут просто гигантские шматы кода с минимумом комментариев.
        Поставил бы минус, если б мог.


        1. derek_streyt Автор
          23.04.2015 07:25

          На ключевых моментах сделаны пометки
          иначе как бы это выглядело на примере одной функции. Специально писал код так, чтобы можно было просто его читать. Иначе вышло бы что-то вроде.

          public virtual T Pop<T>(K groupKey) where T : V // достаем из пула
              {
                  T result = default(T);// значение по умолчанию
                  if (Contains(groupKey) && objects[groupKey].Count > 0) // Если есть в контейнере и контейнер не пустой
                  { 
                      for (int i = 0; i < objects[groupKey].Count; i++) // перебираем контейнер
                      {
                          if (objects[groupKey][i] is T) // если тип соответствует
                          {
                              result = (T)objects[groupKey][i]; // присваиваем результату соответствие
                              Type type = result.GetType(); // получаем тип
                              RemoveObject(groupKey, i); // удалем из базы данных
                              RemoveFromCache(result, type); // удаляем из кеша
                              result.Create(); // вызываем псевдо-конструктор
                              break; // дальше что-то искать нет смысла, обрываем
                          }
                      }
                  }
                  return result; // получаем результат
              }
          

          Как видно я просто перевел конструкции языка и методы на русский язык. По поводу комментариев, рекомендую почитать книгу «Стив Макконнелл — Совершенный код». А конкретно почему комментарии не всегда хорошо и что-то такое само-документирующий код. Встречал код людей которые правят код, но не правят комментарии. Вот это действительный корень зла. Напиши нормально, что делает метод в его заголовке. Напиши смысл переменной не жалея слов. В этом и есть вся красота интерфейсного программирования.


          1. lexxpavlov
            23.04.2015 12:38

            Дело не в комментариях к коду, а в объяснениях, зачем было выбрано именно такое решение, и почему так. Тогда получился бы туториал с хорошей заявкой на обучение новичков.
            Что такое if или foreach знает каждый программист, который учится больше нескольких недель. А вот на этих вопросах можно было бы и остановиться подробнее:
            — для чего был создан интерфейс IPoolObject
            — почему был выбран стек (псевдостек) для хранения объектов в пуле
            — для чего были выбраны именно такие методы в классах (имя метода OnPush в интерфейсе, на мой взгляд, отражает технические особенности пула, а не назначение этого метода)
            — как организован доступ к пулу (через статическое поле Instance, такой полу-синглтон. Кстати, зачем дублировать в менеджере все методы самого пула? ведь вместо Instance = this можно было написать Instance = poolManager)
            — что такое yield?
            — в самом начале вы указали, что ваш пул можно использовать в Photon Server. Другие пулы нельзя в нём использовать? Что вы сделали, чтобы можно было. И вообще — что такое Photon?

            Если бы в статье были ответы на эти и подобные вопросы, тогда получился бы отличный туториал, который оценили бы гораздо выше. А сейчас получилась статья в стиле «смотрите как я умею!», на что я и ответил в первом комменте.


            1. derek_streyt Автор
              23.04.2015 14:11

              Да согласен, интерфейс нужно детальней описать и примеры тоже. Photon Server это сетевое решение от Exit Games аналог SmartFoxServer только тот на Java, а этот на C#. По поводу Instance, нужен именно доступ к юнити расширению PopOrCreate, поэтому и продублировано. Было бы множественное наследование)


              1. lexxpavlov
                23.04.2015 14:18

                Я-то знаю, что такое Photon (хотя и лично с ним не работал), но раз в статье вы сказали о нём, то хорошо было бы об этом рассказать подробнее.
                Эти вопросы я написал не для того, чтобы вы мне ответили тут, а чтобы показать, о чём было бы хорошо рассказать в статье, чтобы статья получилась цельная и полезная. Предлагаю вам в следующей статье больше внимания уделить не только сути (коду), а хорошему описанию, с обоснованием принятых решений и обзором тонких нюансов.