Все знают что операции создания и удаления объектов не дешевые. Например создавать каждый раз пулю и уничтожать, довольно накладно для тех же мобильных устройств. Может стоит не уничтожать пулю, а скрывать ее. Вот решил поделится своей реализацией Pool Manager.
Для начала, нужно создать интерфейс:
Для чего нужен именно интерфейс? Универсально, пусть пул не знает с какими объектами работает, он только знает, что у них есть особые методы и свойства.
T — в этом случае идентификатор группы. Пример далее.
Метод Create() — будет играть роль псевдо-конструктора. Ведь когда вы достанете объект из пула, его состояние будет не определено, что может пагубно отразится на дальнейшем его использовании.
Метод OnPush() — будет играть роль псевдо-декструктора. Когда объект попадает в пул, возможно нужно что-то сделать, например выключить какой-то связанный партикл или еще что-то.
Метод FailedPush() — Будет вызван у объекта, в случае не возможности попадания в пул. Например пул заполнен. И что-то бы объект не остался бесхозный, возможно потребуется его уничтожение.
Теперь сам Pool Manager
На что стоит обратить внимание.
MaxInstances — поле максимального количества pool объектов. В случае, если не возможно поместить в пул очередной объект.
protected Dictionary<K, List> objects -Контейнер представлен в виде группа — список. При попадании в пул, он отправляется в соответствующую группу. Когда он потребуется, по группе пул вернет первый соответствующий объект. protected Dictionary<Type, List> cache — Кеш объектов по типу. Нужен, исключительно, для красивого метода Pop().
Метод Pop(Compare comparer) — возможно потребуется, чтобы достать объект по условию.
Общий Pool готов. Теперь нужно сделать вариацию для Unity3d. Приступим
По сути это просто обвертка над первым пулом и Сингелтон. Можно было завернуть иначе. Но получилось бы что-то вроде UnityPoolManager.Instance.Pool.Pop(), а дополнительно создать только пару методов специально для юнити. Но это на усмотрение читателя. Кода получится меньше но появится дополнительный Pool.
PopOrCreate() — нам понадобится вот этот метод для создания объектов.
Push() — у самих пул объектов или Push в менеджере.
Теперь нам понадобится сам GameObject
Все объекты будем наследовать от него, и возможно переопределять методы Create и OnPush.
Теперь перейдем к использованию. На примере пули следов и UI List item.
Надеюсь данный пример поможет вам в написании своих проектов на Unity3d. Отдельное спасибо @ lexxpavlov который подсказал, что нужно описать детальней чем просто интерфейс.
Структуры
Для начала, нужно создать интерфейс:
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 который подсказал, что нужно описать детальней чем просто интерфейс.
lexxpavlov
Довольно обычная вещь, без изюминки. Таких пулов довольно много. Чем ваш реализация отличается от других пулов? Зачем вы решили делать свой велосипед?
Лично я из бесплатных пользовался двумя — ObjectPool и PoolBoss. Первый попроще, второй понавороченнее. Оба могут работать через методы расширений — вызываются прямо у GameObject или Transform. PoolBoss дополнительно имеет настройки в редакторе — довольно удобно.
Хм… Сейчас зашёл на страницу PoolBoss-а в AssetStore — он теперь не бесплатный… Вышла новая версия, Unity 5.0 ready
derek_streyt Автор
Туториал. Как то написал, использую до сих пор. Нужен был универсальный.
ForhaxeD
Это не туториал, это код с минимальными комментариями.
devcor
Два чая господину выше. Это не туториал. В туториалах люди чуть ли не каждую строчку объясняют, и не кусками кода, а помаленьку «пишут» вместе с читателями. А тут просто гигантские шматы кода с минимумом комментариев.
Поставил бы минус, если б мог.
derek_streyt Автор
На ключевых моментах сделаны пометки
иначе как бы это выглядело на примере одной функции. Специально писал код так, чтобы можно было просто его читать. Иначе вышло бы что-то вроде.
Как видно я просто перевел конструкции языка и методы на русский язык. По поводу комментариев, рекомендую почитать книгу «Стив Макконнелл — Совершенный код». А конкретно почему комментарии не всегда хорошо и что-то такое само-документирующий код. Встречал код людей которые правят код, но не правят комментарии. Вот это действительный корень зла. Напиши нормально, что делает метод в его заголовке. Напиши смысл переменной не жалея слов. В этом и есть вся красота интерфейсного программирования.
lexxpavlov
Дело не в комментариях к коду, а в объяснениях, зачем было выбрано именно такое решение, и почему так. Тогда получился бы туториал с хорошей заявкой на обучение новичков.
Что такое if или foreach знает каждый программист, который учится больше нескольких недель. А вот на этих вопросах можно было бы и остановиться подробнее:
— для чего был создан интерфейс IPoolObject
— почему был выбран стек (псевдостек) для хранения объектов в пуле
— для чего были выбраны именно такие методы в классах (имя метода OnPush в интерфейсе, на мой взгляд, отражает технические особенности пула, а не назначение этого метода)
— как организован доступ к пулу (через статическое поле Instance, такой полу-синглтон. Кстати, зачем дублировать в менеджере все методы самого пула? ведь вместо Instance = this можно было написать Instance = poolManager)
— что такое yield?
— в самом начале вы указали, что ваш пул можно использовать в Photon Server. Другие пулы нельзя в нём использовать? Что вы сделали, чтобы можно было. И вообще — что такое Photon?
Если бы в статье были ответы на эти и подобные вопросы, тогда получился бы отличный туториал, который оценили бы гораздо выше. А сейчас получилась статья в стиле «смотрите как я умею!», на что я и ответил в первом комменте.
derek_streyt Автор
Да согласен, интерфейс нужно детальней описать и примеры тоже. Photon Server это сетевое решение от Exit Games аналог SmartFoxServer только тот на Java, а этот на C#. По поводу Instance, нужен именно доступ к юнити расширению PopOrCreate, поэтому и продублировано. Было бы множественное наследование)
lexxpavlov
Я-то знаю, что такое Photon (хотя и лично с ним не работал), но раз в статье вы сказали о нём, то хорошо было бы об этом рассказать подробнее.
Эти вопросы я написал не для того, чтобы вы мне ответили тут, а чтобы показать, о чём было бы хорошо рассказать в статье, чтобы статья получилась цельная и полезная. Предлагаю вам в следующей статье больше внимания уделить не только сути (коду), а хорошему описанию, с обоснованием принятых решений и обзором тонких нюансов.