Пул объектов и фабрика в Unity. От теории к практике

от автора

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

Введение

Начнем с фабрики. В программировании фабрика — один из архитектурных паттернов в разработке, который отделяет логику создания объекта от остальной бизнес-логики.
В чем плюс фабрики?

  • Код становится более независимы (SRP);

  • Легкость в переписывании. Вытекает из предыдущего пункта, теперь, если мы захотим изменить настройки или порядок создания объекта, нам не нужно бегать по всему коду, мы можем просто зайти в один класс и внести в него изменения.

С фабрикой разобрались, теперь поговорим про пул объектов (Object pool). Еще один паттерн в проектировании вашего кода, который очень популярен в разработке игр. Использование пула объектов предлагает вам не создавать и удалять объекты заново, а переиспользовать их.
Благодаря этому раскрывается главный плюс пулов — скорость. Операции Instantiate и Destroy достаточно «дорогие» (по времени) в юнити, поэтому их частое использование приводит к снижению FPS.

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


К практике

Шаг 1

Создаем папку и класс GameObjectPool — универсальное название в котором мы и скрестим создание объектов и управление ими

Шаг 2

Когда мы говорим про пул объектов, то представляем что-то обобщенное, универсальное. В разработке игр, мы захотим создавать разные пулы для разных типов объектов. Например, пули, враги, сундуки и тд. У каждого такого объекта — свой класс (MonoBehaviour) поэтому и пулы должны быть разными. Чтобы не дублировать код, одинаковыми реализациями пулов, сделаем наш пул универсальным — добавим дженериков.

public class GameObjectPool<T> where T: MonoBehaviour {  } 

Теперь мы сможем создавать новые пулы с разными типами данных

Шаг 3

Идем дальше. Наш пул, при его инициализации должен получить префаб, из которого он будет создавать новые объекты, количество изначально создаваемых объектов и родителя (сделаем его по умолчанию = null, чтобы родителя можно было не указывать — нужно, если ваши объекты будут присваиваться к разным родителям).

public GameObjectPool(T prefab, int initialCount, Transform parent = null) {       _prefab = prefab;       _parent = parent;          for (int i = 0; i < initialCount; i++) {           Create();       } } 

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

Шаг 4

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

private Stack<T> _elements = new(); 

И реализуем сам метод:

private void Create() {       var element = Object.Instantiate(_prefab, _parent, false);          element.gameObject.SetActive(false);       _elements.Push(element);   } 

Мы не хотим, чтобы методом можно было пользоваться извне, поэтому делаем его приватным.
В самом методе — мы создаем новый объект, после этого сразу его выключаем и добавляем объект в наш созданный стек.
Таким образом, после создания нового пула у нас создается начальное кол-во объектов, которыми можно будет пользоваться.

Шаг 5

Следующим шагом сделаем метод, который будет возвращать объекты в пул.
Для этого напишем следующий метод:

public void Release(T element) {       element.gameObject.SetActive(false);       _elements.Push(element);   } 

Метод получается очень простой: выключаем элемент, на случай, если мы забыли сделать это перед тем, как решили вернуть элемент в пул, и возвращаем элемент в стек.

Шаг 6

Ну и переходим к самому интересному: метод получения объекта. Что нам важно здесь учесть? Как будет вести себя наш пул, когда стек окажется пустым. В нашем случае пул будет расширяться на один элемент и продолжать работу.
(Я видел различные варианты, когда пул выдает ошибку / возвращает false / расширяется не на один элемент а в N раз, но этот функционал, в случае чего вы сможете реализовать сами).

public T Get() {       if (_elements.Count == 0) {           Create();       }       return _elements.Pop();   } 

Получаем еще один простенький метод, который возвращает нам элемент. Его немного можно оптимизировать и вместо вызова метода Create просто создавать объект. Тогда нам не придется сначала класть, а потом доставать объект из стека.


Итоги

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

P.S. Существует еще реализация пула, в который передаются функции, выполняющие действия над объектом сразу после его создания, перед его выдачей и перед возвращением в пул. Я считаю, что это уже смешение логики и нагромождение. Легче и понятнее выполнять эти методы там, где вы обращаетесь к пулу.

P.P.S. Спасибо, что дочитали эту статью, вы можете задать мне вопросы в моем ТГ канале.


ссылка на оригинал статьи https://habr.com/ru/articles/848194/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *