38

[Туториал] Простой и удобный пул объектов для Unity3D

Урок рассчитан на новичков, у профи я догадываюсь уже написаны свои супер пулы на все случаи жизни =)


Итак, вкратце - зачем же нужен пул и что это такое?
Пул нужен для оптимизации в тех играх, где часто создаются и уничтожаются какие-нибудь объекты. Например, пули или враги.

При создании нового объекта, программа пробегается по оперативной памяти, выискивая свободное местечко куда бы этот объект запихнуть. Это долго. А потом надо еще записать в найденное место байты этого объекта, запустить конструктор, всякие события в движке, происходящие при создании объекта, и так далее. Всё это жрёт большое количество процессорного времени и может приводить к тормозам. Особенно на слабых компьютерах и на телефонах.

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

Вот так выглядит сборщик мусора в мобильной стрелялке без пулов:

Так чем же нам поможет пул?
Очень просто - вместо уничтожения, объекты будут только деактивироваться, и записываться в пул, оставаясь таким образом в оперативной памяти. А вместо создания новых объектов, будут браться уже созданные объекты из пула, и только активироваться!

Дальше опишу свою реализацию пула на юнити, универсальную и простую в использовании:

Для начала инициализация:

Метод PoolManager.init() надо будет дёрнуть в начале игры, передав в параметр трансформ того геймобжекта, который точно не будет уничтожаться. В этот геймобжект будут класться все деактивированные объекты, чтобы случайно не удалить их по ходу игры, вызвав Destroy() изначального родительского геймобжекта =)

Про словарь (Dictionary) будет дальше.

Функция получения объекта из пула:

В параметр PoolManager.getGameObjectFromPool() надо передать префаб, по которому создаётся объект.

Все операции доставания из пула или укладывания в него происходят по имени префаба.

Строчка if(!poolsDictionary.ContainsKey(prefab.name)) проверяет, был ли уже в пуле объект от этого префаба, и если нет, то далее создаётся хранилище (LinkedList) для объектов которые создаются по этому префабу.


Строчка  if (poolsDictionary[prefab.name].Count > 0) проверяет, есть ли сейчас в хранилище деактивированные объекты от этого префаба. Если да, то объект берётся из хранилища, делается активным (SetActive()) и возвращается как результат работы функции.

Если же хранилище пустует, то создаётся новый объект через GameObject.Instantiate().
Ему присваивается имя, префаба, которое потом будет использоваться для возвращения в пул, и функция возвращает новый объект.

Функция возврата объекта в пул:

Тут всё просто, берётся имя объекта который мы хотим вернуть в пул, по этому имени в словаре находится хранилище, объект добавляется туда, переносится в безопасный parent и деактивируется.

Всё, пул готов!

Реализация очень простая, но у неё есть пара ограничений:
1) Нельзя делать префабы с одинаковыми названиями, лежащие в разных папках. В этом пуле они перемешаются потому что тут всё играется от названия префаба.
2) И соответственно нельзя динамически менять имя пулируемого геймобжекта, потому что оно должно быть как у префаба.

Но благодаря этим небольшим ограничениям, не надо возиться со всякими дополнительными компонентами типа IPoolable и т.д., мой пул очень прост в использовании. Просто вместо GameObject.Instantiate(prefab) вызываешь PoolManager.getFromPool(prefab), А вместо GameObject.Destroy(gameObject) PoolManager.putToPool(gameObject)

p.s. И конечно надо не забыть после взятия из пула проставить правильного родителя если это нужно, и прочие переменные вроде hp и так далее вернуть к исходным значениям.

Правила сообщества

ОБЩИЕ ПРАВИЛА:

- Уважайте чужой труд и используйте конструктивную критику

- Не занимайтесь саморекламой, пишите качественные и интересные посты

- Никакой политики


СТОИТ ПУБЛИКОВАТЬ:

- Посты о Вашей игре с историей её разработки и описанием полученного опыта

- Обучающие материалы, туториалы

- Интервью с опытными разработчиками

- Анонсы бесплатных мероприятий для разработчиков и истории их посещения;
- Ваши работы, если Вы художник/композитор и хотите поделиться ими на безвозмездной основе

НЕ СТОИТ ПУБЛИКОВАТЬ:

- Посты, содержащие только вопрос или просьбу помочь
- Посты, содержащие только идею игры

- Посты, единственная цель которых - набор команды для разработки игры

- Посты, не относящиеся к тематике сообщества

Подобные посты по решению администрации могут быть перемещены из сообщества в общую ленту.

ЗАПРЕЩЕНО:

- Публиковать бессодержательные посты с рекламой Вашего проекта (см. следующий пункт), а также все прочие посты, содержащие рекламу/рекламные интеграции

- Выдавать чужой труд за свой

Подобные посты будут перемещены из сообщества в общую ленту, а их авторы по решению администрации могут быть внесены в игнор-лист сообщества.


О РАЗМЕЩЕНИИ ССЫЛОК:

Ссылка на сторонний ресурс, связанный с игрой, допускается только при следующих условиях:

- Пост должен быть содержательным и интересным для пользователей, нести пользу для сообщества

- Ссылка должна размещаться непосредственно в начале или конце поста и только один раз

- Cсылка размещается в формате: "Страница игры в Steam: URL"

0
Автор поста оценил этот комментарий
Спасибо за урок, чувак! Хотя я думаю Dictionary — не лучшее решение. С другой стороны — больше вариантов я пока не знаю. Я сейчас делаю несложную 2д игрушку, и производительность вроде-бы норм, но что-то подобное я стопудово сделаю. Спасибо еще раз!)
раскрыть ветку (1)
1
Автор поста оценил этот комментарий

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

показать ответы
0
Автор поста оценил этот комментарий

Что мешает добавлять компонент сразу после создания объекта? При этом все делается 2 строчками кода, не нужно ручками все делать)

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

А где гарантия что от этого оверхед будет меньше чем от поиска по словарю?)) GetComponent<> тоже весьма небыстрая штука.

показать ответы
0
DELETED
Автор поста оценил этот комментарий

Отмечу только, что это пул не будет работать на AOT-платформах, т.к. он использует generic of generics. Чтобы все заработало нужно создать отдельный пустой класс, унаследованный от LinkedList<GameObject> и использовать его в объявлении poolsDictionary;

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Ну, у меня в WebAsm работает, наверное юнитисты уже добавили поддержку вложенных дженериков в IL2CPP

0
Автор поста оценил этот комментарий

Окей, вчитался, был не очень прав, про ту часть с копированием. Остальные предложения актуальны, нужно сравнивать.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Да, пулы иногда заполняются перед стартом уровня на загрузчике. Например какие-то враги могут быть только на определенных уровнях, тогда при загрузке уровня их заливается штук сто в пул, а после прохождения вычищаются.
Что до ключа, его негде хранить. Нету в геймобжекте халявного поля не несущего логики, кроме name. А чтобы добавить поле, надо делать отдельный компонент, добавлять его во все префабы, что является гемором))

0
Автор поста оценил этот комментарий

Перемещение парента не особо затратно, как по мне. Лучше так, чем удалять-создавать объекты.

Вообще для таких дел Stack<T> используют. Почему его не применил?

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Не знал про эту структуру, я не так давно в C# =)
Спасибо.

0
Автор поста оценил этот комментарий
Спасибо за урок, чувак! Хотя я думаю Dictionary — не лучшее решение. С другой стороны — больше вариантов я пока не знаю. Я сейчас делаю несложную 2д игрушку, и производительность вроде-бы норм, но что-то подобное я стопудово сделаю. Спасибо еще раз!)
раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Есть другие варианты, но они завязаны на отдельный компонент, который добавляется ко всем пулируемым объектам. На мой вкус это неудобно. Да и если пулируемых префабов с десяток-другой вариантов, поиск по словарю не будет заметен.
А в отдельном компоненте можно уже что угодно хранить для поиска пула, да, индекс в массиве пулов например.
Пожалста =)

показать ответы
3
Автор поста оценил этот комментарий

Элементарная как кирпич, сойдет для того, чтобы грубо разбить окно, но полиция все равно среагирует.

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

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

Плюс словарь можно создать сразу, да и сразу указать "папку" на сцене где будут сохраняться все данные, т.е. функция Init лишняя.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Для простых игр  наверное и вообще вся эта возня с парентом лишняя. Я думаю перемещение объекта из трансформа в трансформ в юнити тоже не бесплатное, особенно если участвуют коллайдеры.
Мне это нужно было потому что у меня пулились объекты внутри других динамически создаваемых.)

показать ответы
1
Автор поста оценил этот комментарий

Получается, я один такой особенный, которому идея хранить маленькие короткоживущие объекты в словаре по текстовому ключу кажется не самой удачной даже за последние 5 минут?


Не то, чтобы я сильно против, но, имхо, убить производительность Юнити можно куда более изящно.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Моя игра с введением этого пула тормозить перестала.)
Оверхед от создания и удаления объектов гораздо больше, чем от поиска в словаре по текстовому ключу.
По крайней мере если позиций в словаре не много, как у меня.

показать ответы
3
Автор поста оценил этот комментарий

Мне всегда казалось, что в юнити, должен быть уже какой-нить встроенный менеджер пулов.

Что-то типо

Pool<T> newPool, со всей готовой логикой внутри.

Странно что такого нет, и приходится свои велосипеды ваять со словарями.

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

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

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества