Главная » Статьи » Программирование » Unity

Создание универсального пула объектов для Unity (попытка 2)

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

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

  • Первый список содержит все элементы, которые пул создавал, за исключением удаленных со сцены. Данный список представлен с помощью класса List.
  • Второй - элементы, которые в данный момент пул может использовать, так как они доступны для повторного использования, например, умерший противник попадает в этот список и спавнер может его заново использовать. Не забудьте написать метод для сброса характеристик объекта, чтобы, например, умерший противник не появился снова с нулевым здоровьем (я такие ошибки почему-то иногда делаю, скорее всего из-за невнимательно, кажется это наследственное). Данный список представлен с помощью класса Queue. Queue, как и List является оберткой для коллекции, но в отличии от List извлекать объекты из Queue можно только сначала списка, а добавлять только в конец. Таким образом Queue является вариантом очереди первым пришел, первым ушел. По данным с форумов, где я искал самый лучший вариант для коллекции доступных объектов этот оказался наиболее быстрым.
  • Третий - поступающие задачи. Каждая задача содержит набор действий, которые будут выполнены для выбранного из списка доступного объекта. Например, среди действий может быть обнуление характеристик, расчет позиции спавна. Также обязательно в список действий должно быть включено gameobject.setActive(true), так как система пула не включает объект после того как он был использован.

Как все работает на практике. В родительский скрипт поступает задача на обработку через одну из следующих функций:

   public void AddTask(Action<PoolNodeByAlexScript> action, int index = 0)
    {
        pools[index].tasksSpawn.Enqueue(action);
    }

    public void AddTask(Action<PoolNodeByAlexScript> action, string name)
    {
        AddTask(action, pools.FindIndex(x => x.prefab.name == name));
    }

В родительском пуле есть несколько очередей для различных типов объектов. Например тут может очередь спавна противников первого уровня и очередь ракет, которые они выпускают и так далее. После поступления задачи при следующем Update будет опрос каждой очереди на наличие заданий и если задание есть оно будет выполнено. Исключение из правил может быть, в своем скрипте я сделал параметр, который отвечает за это исключение, если этот параметр положительный, то в случае переполнения очереди существующих объектов в пуле (да тут есть максимальный размер пула, это сделано для того, чтобы из-за какого-нибудь косяка в игре не появилось 100000 и более объектов из-за которых все повиснет + для контроля каких-либо событий) задание не будет удалено из очереди, если оно не будет выполнено во время текущего опроса, оно уйдет в конец очереди заданий.
При использовании объекта вызывается метод.

    /// <summary>
    /// Выполняет действия по использованию объекта пула, после выполнения данной функции статус объекта = занят.
    /// </summary>
    /// <param name="task">Задача</param>
    public void SetProperties(System.Action<PoolNodeByAlexScript> task)
    {
        task(this);
    }


Тут сразу важное замечание. В скриптах я только первое время предусматривал возможность вызова другого компонента, который находится на объекте, но потом я просто стал использовать наследование PoolNodeByAlexScript, который находится на клоне, чтобы избегать применения GetComponent, что дает возможность в некоторых случаях выполнять операции спавна не в основном потоке приложения.
 

Ниже полный код родительского скрипта и скрипта клона:

PoolByAlexScript.cs

using LibByAlex.Collection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

public class PoolByAlexScript : MonoBehaviour
{
    /// <summary>
    /// Данные о пуле (содержат префаб, размер пула, общую коллекцию объектов и коллекцию свободных объектов)
    /// </summary>
    [Serializable]
    public class PoolData
    {
        /// <summary>
        /// Максимальный размер пула
        /// </summary>
        [Tooltip("Максимальный размер пула")]
        public int poolMaxSize = 1;
        /// <summary>
        /// Префаб
        /// </summary>
        [Tooltip("Префаб")]
        public PoolNodeByAlexScript prefab = null;
        /// <summary>
        /// Основная коллекция пула
        /// </summary>
        [Tooltip("Основная коллекция пула")]
        public List<PoolNodeByAlexScript> collectionPool = new List<PoolNodeByAlexScript>();
        /// <summary>
        /// Коллекция свободных объектов пула
        /// </summary>
        [Tooltip("Коллкция свободных объектов пула")]
        public Queue<PoolNodeByAlexScript> collectionPoolFree = new Queue<PoolNodeByAlexScript>();
        /// <summary>
        /// Задачи на спавн
        /// </summary>
        [Tooltip("Задачи на спавн")]
        public Queue<Action<PoolNodeByAlexScript>> tasksSpawn = new Queue<Action<PoolNodeByAlexScript>>();

        public void ClearTime(float timerItemDestroy, float timeNow)
        {
            int n = collectionPoolFree.Count;
            for (int i=0;i<n;i++)
            {
                var item = collectionPoolFree.Dequeue();
                if (timeNow - item.lastFreeTime > timerItemDestroy)
                {
                    Destroy(item);
                }
                else
                {
                    collectionPoolFree.Enqueue(item);
                }
            }
        }

        public void ClearTime(float timerItemDestroy)
        {
            ClearTime(timerItemDestroy, Time.time);
        }

        public void SpawnDo(PoolByAlexScript poolByAlexScript)
        {
            if (tasksSpawn.Any())
            {
                if (collectionPoolFree.Any())
                {
                    PoolNodeByAlexScript item = collectionPoolFree.Dequeue();
                    SpawnTask(item);
                }
                else
                {
                    SpawnTaskNew(poolByAlexScript);
                }
            }
        }

        void SpawnTask(PoolNodeByAlexScript item)
        {
            var task = tasksSpawn.Dequeue();
            item.SetProperties(task);
        }

        void SpawnTaskNew(PoolByAlexScript poolByAlexScript)
        {
            if (collectionPool.Count < poolMaxSize)
            {
                var task = tasksSpawn.Dequeue();
                var item = Instantiate(prefab, poolByAlexScript.transform);
                item.Init(poolByAlexScript, this);
                item.SetProperties(task);
                collectionPool.Add(item);
            }
            else if (poolByAlexScript.isWaitFreeCellPool == false)
            {
                tasksSpawn.Dequeue();
            }
        }
    }

    /// <summary>
    /// Переключать свободный статус объекта очереди, если объект вкючается или выключается (подходит для простых игр)
    /// </summary>
    [Tooltip("Переключать свободный статус объекта очереди, если объект вкючается или выключается (подходит для простых игр)")]
    public bool enableDisableIsFree = false;
    /// <summary>
    /// Таймер отчистки очереди
    /// </summary>
    [Tooltip("Таймер отчистки очереди")]
    public float timerCleanCollection = 30f;
    /// <summary>
    /// Таймер уничтожения объекта
    /// </summary>
    [Tooltip("Таймер уничтожения объекта")]
    public float timerItemDestroy = 30f;
    /// <summary>
    /// При спавне если нет свободных ячеек в пуле ждать свободную ячейку или убрать из очереди ожидания спавна объект
    /// </summary>
    [Tooltip("При спавне если нет свободных ячеек в пуле ждать свободную ячейку или убрать из очереди ожидания спавна объект")]
    public bool isWaitFreeCellPool = true;
    /// <summary>
    /// Последнее время отчистки коллекции
    /// </summary>
    [Tooltip("Последнее время отчистки коллекции")]
    private float lastTimeCleanCollection;
    /// <summary>
    /// Массив действий выполняемых во время Update
    /// </summary>
    [Tooltip("Массив действий выполняемых во время Update")]
    private Action[] MassActionUpdate;
    /// <summary>
    /// Занята коллекция или нет
    /// </summary>
    [Tooltip("Занята коллекция или нет")]
    public bool collectionIsBusy = false;
    /// <summary>
    /// Массив пулов.
    /// </summary>
    [Tooltip("Массив пулов")]
    public PoolData[] pools;

    void Start()
    {
        lastTimeCleanCollection = Time.time;
        if (timerItemDestroy > 0 && timerCleanCollection > 0)
        {
            MassActionUpdate = new Action[] { UpdateCleanCollection, UpdateTasksSpawn };
        }
        else
        {
            MassActionUpdate = new Action[] { UpdateTasksSpawn };
        }
    }

    void Update()
    {
        foreach (var act in MassActionUpdate)
            act();
    }

    /// <summary>
    /// Операция по отчистке списка
    /// </summary>
    void UpdateCleanCollection()
    {
        if (Time.time - lastTimeCleanCollection > timerCleanCollection && collectionIsBusy == false)
        {
            collectionIsBusy = true;
            for (int i = 0; i < pools.Length; i++)
            {
                pools[i].ClearTime(timerItemDestroy);
            }
            collectionIsBusy = false;
            lastTimeCleanCollection = Time.time;
        }
    }

    /// <summary>
    /// Выполнение операций по спавну из всех очередей
    /// </summary>
    void UpdateTasksSpawn()
    {
        if (collectionIsBusy == false)
        {
            collectionIsBusy = true;
            int n = pools.Length;
            for (int i=0;i<pools.Length;i++)
            {
                pools[i].SpawnDo(this);
            }
            collectionIsBusy = false;
        }
    }

    public void AddTask(Action<PoolNodeByAlexScript> action, int index = 0)
    {
        pools[index].tasksSpawn.Enqueue(action);
    }

    public void AddTask(Action<PoolNodeByAlexScript> action, string name)
    {
        AddTask(action, pools.FindIndex(x => x.prefab.name == name));
    }
}

 

PoolNodeByAlexScript.cs

using UnityEngine;
using System.Collections;

public class PoolNodeByAlexScript : MonoBehaviour
{
    /// <summary>
    /// Указатель на пул
    /// </summary>
    [Tooltip("Указатель на пул")]
    public PoolByAlexScript poolByAlexScript;
    [HideInInspector]
    public PoolByAlexScript.PoolData poolData;
    /// <summary>
    /// Последений раз когда объект стал свободным
    /// </summary>
    [Tooltip("Последений раз когда объект стал свободным")]
    public float lastFreeTime;

    void Awake()
    {
        if (name.EndsWith("(Clone)"))
            name = name.Substring(0, name.Length - 7);
    }

    /// <summary>
    /// Первичная инициализация объекта
    /// </summary>
    /// <param name="poolByAlexScript">Указатель на родительский пулл</param>
    /// <param name="poolData">Указатель на класс данных очереди к которой относится объект</param>
    public void Init(PoolByAlexScript poolByAlexScript, PoolByAlexScript.PoolData poolData)
    {
        this.poolByAlexScript = poolByAlexScript;
        this.poolData = poolData;
    }

    /// <summary>
    /// Выполняет действия по использованию объекта пула, после выполнения данной функции статус объекта = занят.
    /// </summary>
    /// <param name="task">Задача</param>
    public void SetProperties(System.Action<PoolNodeByAlexScript> task)
    {
        task(this);
    }

    public void SetFree(bool value)
    {
        lastFreeTime = Time.time;
        if (value == true)
        {
            poolData.collectionPoolFree.Enqueue(this);
        }
    }

    public virtual void OnEnable()
    {
        if (poolByAlexScript.enableDisableIsFree)
            SetFree(false);
    }

    public virtual void OnDisable()
    {
        if (poolByAlexScript.enableDisableIsFree)
            SetFree(true);
    }

    public virtual void OnDestroy()
    {
        poolData.collectionPool.Remove(this);
    }
}

Категория: Unity | Добавил: Алексей (29.03.2018) | Автор: Фролов Алексей Алексеевич E W
Просмотров: 252 | Теги: unity3d, Pool, gameobject, Пул, Unity | Рейтинг: 0.0/0
Всего комментариев: 0
avatar