Pull to refresh

Pool объектов для Unity3d

Reading time 6 min
Views 34K
Все знают что операции создания и удаления объектов не дешевые. Например создавать каждый раз пулю и уничтожать, довольно накладно для тех же мобильных устройств. Может стоит не уничтожать пулю, а скрывать ее. Вот решил поделится своей реализацией 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 который подсказал, что нужно описать детальней чем просто интерфейс.
Tags:
Hubs:
+7
Comments 8
Comments Comments 8

Articles