Как сделать пули в unity
2D игра на Unity. Подробное руководство. Часть 3
Снаряд
Сделайте масштаб таким (0,75, 0,75, 1) для лучшего отображения. Теперь, нам нужно установить новый параметр в «Инспекторе» (Inspector). для этого в «Box Collider 2D» поставьте галочку напротив свойства «IsTrigger». Триггер коллайдера создает событие при столкновении, но не используется при моделирования физики. Это значит, что выстрел пройдет сквозь объект при соприкосновении — никакого «реального» взаимодействия не будет. А вот у другого коллайдера это спровоцирует событие «OnTriggerEnter2D».
Та-дам! У нас появился выстрел. Теперь настало время немного поскриптить. Создайте скрипт, назвав его «ShotScript»:
Прикрепите «ShotScript» к спрайту. Также добавьте «MoveScript», т.к. ваши снимки будут двигаться. Теперь перетащите объект выстрел в панель «Проект» для создания Префаба. Он нам совсем скоро понадобится. Вы должны иметь следующую конфигурацию:
Если вы запустите игру с помощью кнопки «Play», вы увидите, что выстрел движется.
Столкновения и повреждения
Тем не менее, выстрел (пока) не наносит повреждений. Ничего удивительного, ведь мы не сделали скрипт обработки повреждений. Создадим его, назвав «HealthScript»:
Добавьте «HealthScript» на префаб спрута.
Внимание: Лучше всего поработать непосредственно с префабом. При этом каждый экземпляр врага, участвующий в сцене, будет модифицирован так, чтобы отражать префаб. В данном случае это особенно важно, потому что в нашей сцене будет много врагов. Если вы сосредоточили усилия на экземпляр игрового объекта вместо префаба, не волнуйтесь: нажав на кнопку «Применить» сверху вкладки «Инспектор», вы добавите эти изменения и в префаб.
Убедитесь, что выстрел и спрут находятся на одной линии, чтобы проверить столкновение. Напоминаю, что 2D движок ничего не знает про ось Z, поэтому ваши 2D коллайдеры всегда будут в той же плоскости. А теперь, запустите нашу сцену. Вы должны увидеть следующее:
Здоровье врага превосходит урон от выстрела, поэтому он выживет. Попробуйте изменить значение hp в «HealthScript» врага:
Стрельба
Удалите выстрел из сцены. Теперь, когда мы с ним закончили, ему нечего там делать. Нам нужен новый скрипт для стрельбы. Создайте его под именем «WeaponScript». Этот скрипт мы будем использовать везде (игроки, враги и т.д.) Его цель заключается в to instantiate снаряда перед игровым объектом, к которому он привязан. Вот полный код, больше, чем обычно. Объяснения ниже:
Прикрепите этот скрипт к игроку. Скрипт делится на три части:
Переменные во вкладке «Inspector»
Выберите игрока в сцене «Hierarchy». В компоненте «WeaponScript», вы можете увидеть свойство «Shot Prefab» со значением «None». Перетащите префаб «Shot» на это место:
Unity автоматически дополнит скрипт это информацией. Удобно, не так ли?
Переменная shootingRate имеет значение по умолчанию, установленное в коде. Мы не будем менять его на данный момент. Но вы можете начать игру и экспериментировать с ним, чтобы узнать на что она влияет.
Будьте осторожны: изменение значения переменной во вкладке «Инспектор» в Unity не приводит к сохранению этих значений в скрипте. Если добавите этот скрипт в другой объект, значение по умолчанию будет таким, которое написано в скрипте. Если же вы хотите сохранить отредактированные параметры, вы должны открыть свой редактор кода и записать эти значения там.
Оружие обладает определенной частотой выстрелов. Без этого параметра можно было бы выпускать неограниченное количество патронов в каждом кадре.
3. Публичный метод создания атаки
Главная цель этого скрипта – активироваться через другой скрипт. Поэтому для создания снаряда мы используем публичный метод.
Создав екземпляр снаряда, мы извлекаем скрипты объекта выстрела и оверрайдим некоторые переменные.
Давайте вернемся к нашему «PlayerScript».
В функции Update() добавьте этот кусочек кода:
На данном этапе неважно, поставите вы его перед или после движения.
Запустите игру с помощью кнопки «Play». Вот что вы должны получить:
Вражеский снаряд
Если вы так же ленивы, как я, продублируйте префаб «PlayerShot», переименуйте его в «EnemyShot1» и измените спрайт, как описано выше.
Для дублирования создайте экземпляр, перетащив его на сцену, переименовав созданный игровой объект и, наконец, сохранив его как `Prefab’.
Или можно просто продублировать Prefab напрямую внутри папки с помощью ярлыков cmd+D (OS X) или ctrl+D (для Windows).
Если вы не выбираете легких путей, вы можете создать новый спрайт с параметром rigibody, коллайдером с триггером и т.д.
Вот, что у вас должно получиться.
При нажатии «Play» произойдет выстрел, который потенциально может уничтожить врага. Это из-за свойств «ShotScript» (которые по умолчанию плохо совместимы с Poulpi).
Не изменяйте ничего. Помните наш «WeaponScript»? Он то и установит правильные значения.
У нас есть префаб «EnemyShot1». Удалите экземпляры со сцены, если они есть.
Прикрепите этот скрипт к осьминогу. У вас должно получиться следующее (заметьте, что частота стрельбы немного звеличилась до 0.75 ):
Попробуйте сыграть и посмотреть!
Итак, мы сделали то, что хотели и теперь и по нам тоже стреляют.
Если повернуть врага, вы можете сделать его стреляющим в его слева, но, хм. спрайт повернулся вверх ногами, а нам это не нужно.
Давайте исправим это недоразумение.
Стрельба в любом направлении.
«WeaponScript» был написан особым образом: вы можете выбрать направление стрельбы, просто вращая прикрепленный игровой объект. Мы уже видели это раньше, когда вращали спрайт врага. Суть в том, чтобы создать пустой игровой объект как ребенка префаба врага. Итак, нам нужно:
Если вы проделали это все на игровом объекте, а не на префабе, то не забудьте нажать на кнопку «Применить» для сохранения изменений. Вот, что у нас получилось:
However, we have a small change to make on the «EnemyScript» script.
В своем нынешнем состоянии вызов GetComponent () в «EnemyScript» возвращает null. В самом деле, «WeaponScript» больше не привязан к одному объекту игры.
Наконец, нужно обновить скорость выстрела путем настройки публичной пременной «MoveScript» из префаба «EnemyShot1». Скорость выстрела должна быть больше скорости движения спрута:
Мы сделали великого и ужасного осьминога. А давайте еще реализуем стрельбу в двух направлениях?
Стрельба а двух направлениях
Эта задача реализуеся всего в пару кликов. Для этого не нужны никакие скрипты:
Нанесение урона игроку
Просто добавьте «HealthScript» на игрока. Убедитесь, что сняли галку с поля «IsEnemy».
Запустите игру и почувствуйте разницу:
Бонус
Вот вам некоторые советы для улучшения аспекта стрельбы в вашей будущей игре. Вы можете пропустить эту часть, если вас не интересуют подробности, относящиеся к жанру шмапа.
Солкновения игрока с врагом
Массив снарядов
Когда вы играете, вы можете наблюдать ва вкладке «Иерархия» (Hierarchy), что игровые объекты создаются и удаляются только через 20 секунд (если они не сталкиваются с игроком или врагом).
Если ваша цель создание огневой завесы для которой требуется МНОГО пуль, эта техника вряд ли подойдет.
Один из способов увеличить количество пуль – использовать массив. По сути, это набор пуль ограниченного размера. Когда массив заполнен, удалите старый объект и замените его на новый.
Мы не будем использовать его здесь, но в этом нет ничего сложного. Мы использовали ту же технику для скрипта рисования.
Кроме того, можно сократить время жизни пули и тогда она исчезнет быстрее.
Имейте в виду, что использование метода Instantiate довольно затратное удовольствие. Применяйте его осторожно.
Поведение пули
В хорошем шутере должны быть запоминающиеся боевые сцены.
Некоторые библиотеки вроде BulletML значительно упрощают определение сложных и зрелищных bullet patterns.
Если вы хотите сделать полную версию игры в жанре Shoot’Em Up, ознакомьтесь с нашим плагином BulletML for Unity
Задержка выстрела
Добавьте несколько вооруженных противников в сцену и запустите игру. Вы увидете как синхронны все враги.
Скорость врагов также может определяться случайной величиной.
Еще раз, это зависит от вас. Все зависит исключительно от того, чего вы хотите достичь с вашим геймплеем.
В следующем уроке
Туториал 3: 2Д спейс шутер (новичок)
Вступление:
Я сейчас использую Юнити 5.5. Скриншоты были сделаны ещё на версии 5.3. Некоторые мелочи будут немножко отличаться, но не критично. Готовый проект для скачивания тоже был сделан на версии 5.5.
*возможно некотрые браузеры просто откроют картинку вместо скачивания, сделайте правый клик на картинку и выберите сохранить. Или правый клик на линке и выбрать сохранить цель.
1. Шаг
Нажмите теперь на Sprite Editor. Кликните на Slice. Проверьте что Type установлен на Automatic и нажмите кнопку Slice. Кликните на Apply и закройте редактор спрайтов.
2. Шаг
Сначала сделаем корабль для игрока. В Catch Game 2D мы использовали клавиатуру для движения. Теперь для движения будем использовать мышь. Наш корабль будет лететь к точке, где мы кликнули по экрану. Перетащите картинку корабля в сцену. Юнити автоматически создаст новый GameObject. Смените его имя на Player. Смените тэг на Player.
3. Шаг
Векторное вычитание:
* Часто использование этого вектора напрямую это не лучший вариант. Корабль будет лететь быстро при удалённом клике и медленно при близком. Так как вам нужно только направление и своя скорость, то используйте normalized вектор. У такого вектора сохраняется направление, но длина будет равна 1. Получить такой вектор из вашего вектора можно через функцию Vector3.Normalize. Потом вы просто умножите это вектор на скорость, что-бы получить нужную постоянную скорость. Но для туториала нам подходит и просто результат векторного вычитания.
Camera.ScreenToWorldPoint
using UnityEngine;
using System.Collections;
public class PlayerMove : MonoBehaviour <
//переменная для позиции клика
Vector3 clickPos;
//переменная для вектора движения
Vector3 move;
//переменная для скорости движения
public float speed = 1 ;
//переменная для ссылки на Rigidbody2D
Rigidbody2D rb;
//выполнится один раз при старте скрипта
void Start () <
//делаем ссылку на компонент Rigidbody2D
rb = GetComponent Rigidbody2D > ();
//что-бы корабль остался на месте и не полетел к точке (0,0,0)
clickPos = transform.position;
>
//выполняется через определенные периоды (0.02 по умолчанию). Используйте для вычислений физики
void FixedUpdate () <
//измените вектор движения
//z останется 0, что-бы корабль не двигался по z-оси
rb.velocity = new Vector2 (move.x, move.y) * speed;
>
>
4. Шаг
5. Шаг
using UnityEngine;
using System.Collections;
public class LaserShot : MonoBehaviour <
//переменная для линка на Rigidbody2D
Rigidbody2D rb;
//переменная величины силы
public float force;
6. Step
using UnityEngine;
using System.Collections;
public class PlayerShoot : MonoBehaviour <
//переменная для ссылки на префаб лазера
public GameObject laser;
//задержка между выстрелами
public float delayTime;
//бинарная переменная для проверки возможности стрельбы
bool canShoot = true ;
//корутина, такую функцию можно поставить на паузу
IEnumerator NoFire () <
//пауза этой функции, возврат через заданное время
yield return new WaitForSeconds (delayTime);
//влючить возможность стрельбы
canShoot = true ;
>
>
7. Шаг
8. Step
using UnityEngine;
using System.Collections;
//библиотека для управления сценами
using UnityEngine.SceneManagement;
public class AsteroidMove : MonoBehaviour <
//переменная скорости движения
public float speed;
//переменная для ссылки на Rigidbody2D
Rigidbody2D rb;
//исполнится если объект перестал отображаться на экране
void OnBecameInvisible () <
//уничтожить объект
Destroy (gameObject);
>
9. Шаг
using UnityEngine;
using System.Collections;
public class ObjectSpawner : MonoBehaviour <
//переменная для ссылки на позицию, которую будем использовать как правую границу спавна
public Transform RightPosition;
//задержка между спавном
public float spawnDelay;
//переменная для ссылки на объект для спавна
public GameObject Item;
//функция спавна
void Spawn () <
//случайная позиция между спавном и правой границей
Vector3 spawnPos = new Vector3 (Random.Range (transform.position.x, RightPosition.position.x), transform.position.y, 0 );
//спавн объекта
Instantiate (Item, spawnPos, transform.rotation);
>
>
10. Step
using UnityEngine;
using System.Collections;
public class HpController : MonoBehaviour <
//переменная для жизненных очков
public int hp;
using UnityEngine;
using System.Collections;
public class LaserShot : MonoBehaviour <
//переменная для ссылки на Rigidbody2D
Rigidbody2D rb;
//переменная для урона
public int damage;
//переменная величины силы
public float force;
11. Шаг
12. Step
using UnityEngine;
using System.Collections;
public class EnemyBullet : MonoBehaviour <
//переменная для ссылки на объект игрока
GameObject player;
//переменная для ссылки на Rigidbody2D
Rigidbody2D rb;
//переменная для величины силы
public float force;
//переменная для величины урона
public int damage;
//Выполняется если пуля вылетела за экран (out of screen)
void OnBecameInvisible () <
//уничтожить пулю
Destroy (gameObject);
>
>
12. Step
using UnityEngine;
using System.Collections;
public class EnemyShoot : MonoBehaviour <
//переменная для префаба пули
public GameObject bullet;
//задержка между выстрелами
public float fireDelay;
//переменная для ссылки на игрока
GameObject player;
//бинарная переменная для разрешения стрельбы
bool canShoot = true ;
//старт корутины
IEnumerator firePause () <
//пауза исполнения на указанное время
yield return new WaitForSeconds (fireDelay);
//возможность стрельы при следующей проверке
canShoot = true ;
>
>
13. Шаг
using UnityEngine;
using System.Collections;
public class ObjectSpawner : MonoBehaviour <
//переменная для ссылки на позицию, которую будем использовать как правую границу спавна
public Transform RightPosition;
//задержка между спавнами
public float spawnDelay;
//массив для объектов спавна
public GameObject [] Item;
14. Шаг
15. Шаг
using UnityEngine;
using System.Collections;
//библиотека для доступа к UI
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class PlayerHP : MonoBehaviour <
//переменная для ссылки на полосу здоровья
public GameObject HealthBar;
//переменная для ссылки на компонент Image
Image img;
//переменная для текущего здоровья
public int hp;
//максимальное здоровье, используется для вычисления соотношения в процентах
float maxHp;
//выполняется один раз
void Start () <
//ссылка на UI компонент Image от объекта HealthBar
img = HealthBar.GetComponent Image > ();
//инициализируем максимальное здоровье
maxHp = hp;
//изменяем процент заполнения полоски (пока заполнится на 100%)
img.fillAmount = hp / maxHp;
>
16. Шаг
17. Шаг
18. Шаг
18. Шаг
using UnityEngine;
using System.Collections;
public class EnemyBullet : MonoBehaviour <
//переменная для ссылки на объект игрока
GameObject player;
//переменная для ссылки на Rigidbody2D
Rigidbody2D rb;
//переменная для величины силы
public float force;
//переменная для величины урона
public int damage;
//переменная для звука стрельбы
public AudioClip BulletSound;
//Выполняется если пуля вылетела за экран (out of screen)
void OnBecameInvisible () <
//уничтожить пулю
Destroy (gameObject);
>
>
using UnityEngine;
using System.Collections;
public class LaserShot : MonoBehaviour <
//переменная для ссылки на Rigidbody2D
Rigidbody2D rb;
//переменная для урона
public int damage;
//переменная величины силы
public float force;
//переменная для звукового клипа
public AudioClip BulletSound;
19. Шаг
using UnityEngine;
using System.Collections;
public class HpController : MonoBehaviour <
//переменная для жизненных очков
public int hp;
//переменная для звкового клипа
public AudioClip ExplosionsSound;
20. Шаг
Это просто пример взрыва, тут границы это только ваша фантазия. Сейчас Particle System постоянно улучшается. На скринах ещё старая система.
Теперь нужен скрипт для уничтожения проигравших Explosion из сцены. Моздайте новый C# script и назовите его TimeDestroyer.
using UnityEngine;
using System.Collections;
public class TimeDestroyer : MonoBehaviour <
//переменная для длительности нахождения в сцене
public float timeToDestroy;
//выполнится один раз при старте скрипта
void Start () <
//унитожить объект через заданное время
Destroy (gameObject, timeToDestroy);
>
>
21. Step
using UnityEngine;
using System.Collections;
public class HpController : MonoBehaviour <
//переменная для жизненных очков
public int hp;
//переменная для звукового клипа
public AudioClip ExplosionsSound;
//переменная для взрыва
public GameObject Explosion;
22. Шаг
23. Шаг
Пулинг объектов в Unity 2021+
Разрабатывая игры, вы могли заметить, что создание порядка 100 экземпляров пуль в секунд.
A: уменьшить количество пуль до 20
B: реализовать свою собственную пулинговую систему
C: заплатить 50 долларов за пулинговую систему в Asset Store
D: использовать новый Pooling API Unity, представленный в 2021 году
(СМОТРИТЕ ВИДЕО В ОРИГИНАЛЕ СТАТЬИ)
В этой статье мы рассмотрим последний вариант.
Сегодня вы узнаете, как использовать новый Pooling API, представленный в 2021 году.
Начиная с Unity 2021, у вас есть доступ к широкому набору фич для работы с пулами, которые помогут вам разрабатывать высокопроизводительные проекты на Unity.
Готовы узнать о них побольше?
Когда вам нужен пул?
Начнем с самого главного вопроса: когда вам нужен пул?
Я задаю его, потому что пулы не должны быть вашим дежурным решением.
Пул объектов в Unity определенно имеет некоторые важные недостатки, которые приносят больше вреда, чем пользы, поэтому использовать его нужно с умом.
Но мы рассмотрим это позже.
Если вкратце, то вам стоит рассматривать возможность использования пулов, когда:
Вы часто аллоцируете и высвобождаете объекты, хранящиеся в куче (вместо их повторного использования). Это относится и к коллекциям C#.
Эти операции вызывают много аллокаций, следовательно вы сталкиваетесь с:
Избыточным расходом тактов процессора на операций создания и уничтожения (или new/dispose).
Преждевременной сборкой мусора, вызывающей фризы, которые ваши игроки не оценят.
Фрагментацией памяти, которая затрудняет поиск свободных смежных областей памяти.
Не кажется ли вам, что эти проблемы могут представлять для вас угрозу?
Но давайте продолжим.
Итак, что же такое это (объектный) пулинг в Unity в конце-то концов?
Теперь, когда вы понимаете, попали ли вы в беду (или все еще в безопасности), позвольте мне быстро объяснить, что такое пулинг.
Сущность может быть чем угодно: игровым объектом, инстансом префаба, словарем C# и т. д.
Позвольте мне продемонстрировать концепцию пулинга в контекст реального примера.
Допустим, вам нужно завтра утром пойти за продуктами.
Что вы обычно берете с собой, кроме кошелька и ключей?
Ну, можно взять многоразовые сумки. В конце концов, вам нужны какие-то контейнеры, чтобы донести продукты домой.
Итак, вы берете пустые многоразовые сумки, наполняете их продуктами и возвращаетесь домой.
Вернувшись домой, вы опустошаете свои сумки и кладете их обратно в ящик.
Многоразовые сумки — лучшая альтернатива, чем покупка (аллокация) пластиковых пакетов и их выбрасывание (высвобождение) каждый раз, когда вы идете за покупками.
Хорошо, вы идете к своему пулу сумок (например, ящик на кухне), берете несколько, используете их, вытаскиваете все из них и, наконец, возвращаете их обратно в пул.
Вот основные детали юзкейса пулинга:
Функции, которые вы выполняете над пулом и его элементами: Take (взять), Return (вернуть), Reset (сбросить).
В случае с шутером вы можете создавать и уничтожать пули каждый раз при выстреле… или вы можете заранее создать определенное количество, а затем повторно использовать их следующим образом:
Вы создаете тысячу пуль и помещаете их в пул.
Когда пуля попадает во что-то и исчезает, вы возвращает ее обратно в пул.
Таким образом вы экономите такты процессора, необходимые для создания и уничтожения этих префабов. Кроме того, вы уменьшаете нагрузку на сборщик мусора.
Теперь, прежде чем сразу нырнуть в пулинг, обратите внимание на несколько моментов.
Когда следует отказаться от использования пула?
У техники пулинга есть несколько (потенциальных) проблем:
Ваши элементы могут загрязняться. Поскольку они уже использовались в прошлом, вы могли оставить их в непригодном состоянии, например, пули с небольшими остатками красной краски на них. Это означает, что вам нужно потратить несколько тактов процессора, чтобы очистить свои элементы перед их использованием: операция reset.
Вы резервируете память, которая может вам так и не понадобиться. Если вы создаете пул с тысячами пуль, но все, что ваш игрок хотел сделать, это полюбоваться видами, то вы зря потратили память.
Это усложняет вашу кодовую базу. Вам необходимо управлять жизненным циклом своих пулов. Это не только увеличивает количество тактов процессора, но и увеличивает количество мозговых тактов из-за обработки большей кодовой базы.
Все, что вам нужно сделать, — это избегать пулов в тех случаях, когда вы не получите от них никакой выгоды.
Скажем, нет никакой необходимости пулить финального босса. В конце концов, он существует в единственном экземпляре.
Помните: самое главное — это частота ваших операций создания и уничтожения.
Если вы делаете их часто, рассматривайте возможность использования пулов. В противном случае даже не думайте об этом.
Позже мы рассмотрим больше проблем с пулами.
Теперь давайте посмотрим на наши доступные варианты для реализации пулов.
Пулы объектов в Unity 2021: ваши варианты
Если вы хотите добавить пул объектов в свой проект Unity, у вас есть три варианта:
Создать свою собственную систему
Купить стороннюю систему пулинга
Давайте рассмотрим их.
A) Создаем свою собственную систему пулинга
Один из вариантов — применить на практике свое мастерство.
Внедрение вашей собственной системы пулинга не выглядит слишком сложным, поскольку вам нужно всего лишь реализовать несколько операций:
Создать и удалить пул (Create & dispose)
Взять из пула (Take)
Вернуться в пул (Return)
Операции сброса (Reset)
Но это часто становится гораздо сложнее, когда вы начинаете думать о:
Управление памятью и структурах данных
Пользовательской аллокации/высвобождении объектов
Это уже больше похоже на головную боль? Чувствую, ваше лицо побледнело.
Предлагаю не изобретать велосипед (если только это не учебное упражнение).
Поскольку это уже решенная проблема, используйте что-то, что работает, чтобы вы могли сосредоточиться на своем проекте.
Сосредоточьтесь на том, чтобы доставить удовольствие своим игрокам. В любом случае они вам за это заплатят. Проверим второй вариант.
B) Сторонние системы пулинга объектов
Здесь вам всего лишь нужно выбирать одного из таких поставщиков, как:
The Unity Asset Store
Друг или член семьи
Давайте рассмотрим несколько примеров:
Pooling Toolkit
13Pixels Pooling
Pure Pool
Pro Pooling
Но прежде чем вы нажмете кнопку покупки… прочитайте немного дальше.
Сторонние инструменты могут творить чудеса и обладают множеством фич.
Но у них есть недостатки:
Вы полагаетесь на их поддержку в исправлении проблем и обновлении пакетов для более новых версий редактора.
Если у вас нет исходного кода, вы не сможете исправить проблемы самостоятельно.
Больше фич = сложнее код. Вам потребуется время, чтобы понять и поддерживать их систему.
Они могут быть достаточно дорогими (и по деньгам и по времени).
Вы, наверное, все это и так уже знали, но об этом всегда стоит упомянуть.
И в настоящее время осталось еще меньше причин для использования сторонних решений, поскольку Unity втихаря зарелизила новый API для пулинга в Unity 2021.
И это основная тема этой статьи.
C) Новый Pooling API от Unity
Начиная с версии 2021 года, Unity зарелизила несколько механизмов пулинга C#, которые помогут вам во множестве юзкейсов.
Эти новые пулы объектов напрямую интегрированы в движок Unity. Не требуется никаких дополнительных загрузок, и они поддерживается в актуальном состоянии при каждом обновлении Unity.
Огромный плюс — у вас есть доступ к их исходному коду.
И я должен отметить, что их реализации довольно просты. Это приятное вечернее чтиво.
Давайте посмотрим, как вы можете начать использовать Unity Pooling API прямо сегодня, чтобы снизить затраты на операции, о которых и вы, и я прекрасно знаем.
Как использовать новый Object Pooling API в Unity 2021
Первый шаг — убедиться, что вы используете Unity 2021+.
(Я имею в виду, вы можете просто скопировать и вставить код в любой из ваших старых проектов. но эй, я этого не говорил, если что)
После этого, это просто вопрос знания Unity Pooling API:
Различные контейнеры пулов
Я уже рассказывал вам несколько спойлеров о пулах. Но теперь давайте углубимся в них.
(СМОТРИТЕ ВИДЕО В ОРИГИНАЛЕ СТАТЬИ)
1. Построение вашего пула
Первая операция, которую вам нужно сделать, — это построить контейнер для пула по вашему выбору. Обычно это делается в одну строчку кода, так что здесь не беспокойтесь.
Параметры конструктора зависят от конкретного контейнера, который вы хотите использовать, но они очень похожи. Вот обычные параметры конструктора пула Unity:
Вызывается для создания нового экземпляра вашего объекта, например () => new GameObject(“Bullet”) or () => new Vector3(0,0,0)
Вызывается, когда вы берете экземпляр из пула, например, для активации игрового объекта.
Вызывается, когда вы возвращаете экземпляр в пул, например, чтобы очистить и деактивировать экземпляр.
Вызывается, когда пул уничтожает этот элемент, то есть когда он не помещается (превышает максимальный размер) или пул уничтожается.
True, если вы хотите, чтобы Unity проверяла, что этот элемент еще не был в пуле, когда вы пытаетесь его вернуть (только в редакторе).
Размер пула по умолчанию: начальный размер стека/списка, который будет содержать ваши элементы.
Размер пула: максимальное количество свободных элементов, которые находятся в пуле в любой момент времени. Если вы вернете предмет в заполненный пул, он будет уничтожен.
Вот как вы можете создать пул GameObjects :
Я оставил названия параметров для наглядности; не стесняйтесь пропускать их в вашем коде.
Хорошо, теперь у вас есть пул _GameObject_’ов.
Как нам им пользоваться?
2. Создание элементов пула
Первое, что Unity нужно знать, — это как создавать больше ваших _GameObject_’ов, когда вы запрашиваете больше, чем доступно.
Мы уже указали это в конструкторе, поскольку передали функцию createFunc в качестве первого параметра конструктору пула.
Каждый раз, когда вы захотите взять GameObject из пустого пула, Unity создаст его для вас и отдаст вам.
А как нам взять GameObject из пула?
3. Извлечение элемента из пула
Теперь вы можете использовать объект по своему усмотрению (в определенных рамках).
Когда вы закончите с ним, вам нужно вернуть его обратно в свой пул, чтобы вы могли использовать его позже.
4. Возврат элемента в пул
Итак, вы использовали свой элемент несколько минут, и теперь он вам больше не нужен. Что дальше?
Вот чего вы сейчас не делаете: вы не уничтожаете (destroy/dispose) его сами. Вместо этого вы возвращаете его в пул, чтобы пул мог правильно управлять своим жизненным циклом в соответствии с предоставленными вами функциями.
Как это сделать? Легко:
Проверит, есть ли достаточно места в своем внутреннем списке/стеке на основе MaxSize
Если есть достаточно свободного пространство в контейнере, он поместит туда объект.
А теперь об уничтожении элементов.
5. Уничтожение элемента из вашего пула
Всякий раз, когда вы утилизируете (dispose) свой пул, или в нем нет внутреннего пространства для хранения возвращаемых вами элементов, пул уничтожает эти элементы.
Эта функция может быть совершенно пустой или вызывать Destroy(myObject) , если мы говорим об объектах, управляемых Unity.
И, наконец, когда вы закончите работу с пулом, вы должны его утилизировать.
6. Очистка и утилизация вашего пула
Утилизация вашего пула — это высвобождение ресурсов, принадлежащих пулу. Часто внутри вашего пула есть стек или список, содержащий элементы, которые можно свободно из него брать. Что ж, вы избавляетесь от своего пула, вызывая:
Вот это собственно и есть вся функциональность пула. Но нам все еще не хватает одного важного момента.
Не все пулы созданы для одних и тех же юзкейсов
Давайте посмотрим, какие типы пулов предлагает Unity, чтобы удовлетворить ваши потребности.
Типы пулов в Unity 2021+
LinkedPool и ObjectPool
Первая группа пулов — это те, которые охватывают обычные объекты C# (95%+ элементов, которые вы, возможно, захотите поместить в пул).
Типичным вариантом использования пулов этого типа являются игровые объекты — независимо от того, созданы они из префабов или нет.
Разница между LinkedPool и ObjectPool заключается во внутренней структуре данных, которую Unity использует для хранения элементов, которые вы хотите поместить в пул.
ObjectPool просто использует Stack C#, который использует массив C# под капотом:
Будучи стеком, он содержит большой кусок непрерывной памяти.
Наихудший случай — наличие 0 элементов (длина = 0) в большом пуле (емкость = 100000). Там у вас будет большой кусок зарезервированной памяти, который вы не используете.
Изменение размера стека происходит, когда вы превышаете его емкость. И это дорого, так как вам нужно выделить больший кусок и скопировать элементы.
LinkedPool использует связанный список, который может улучшить управление памятью в зависимости от вашего юзкейса. Вот как выглядит эта структура данных:
Но это требует дополнительных затрат: вы тратите больше памяти на элемент и больше тактов процессора для управления этой структурой данных. В любом случае вы, вероятно, знаете разницу между массивами и связанными списками.
Итак, давайте поговорим о следующей категории классов пулов в Unity.
ListPool, DictionaryPool, HashSetPool, CollectionPool
Теперь мы поговорим о пулах коллекций C# в Unity.
Видите ли, при разработке игр вам, скорее всего, придется использовать списки, словари, хеш-множества и коллекции.
И достаточно часто вам нужно часто создавать/уничтожать эти коллекции.
Мы часто делаем это в структурах ИИ при выполнении определенных одноразовых действий или алгоритмов. Там нам часто требуются вспомогательные структуры данных для выполнения поиска, оценки и скоринга.
Вот в чем собственно дело.
Каждый раз, когда вы создаете и уничтожаете коллекции, вы оказываете давление на систему управления памятью. Это потому, что вы:
Аллоцируете и высвобождаете коллекцию плюс ее внутренние структуры данных.
Вы можете динамически изменять размер своих коллекций.
Когда вам понадобится список, вы можете просто взять его из пула, использовать и вернуть, когда закончите.
А вот другая конструкция, которая освобождает для вас пул коллекций:
Каждый раз, когда вы выходите за пределы этого using блока, Unity будет возвращать список в пул за вас.
Но вы должны быть осторожны с этими пулами для коллекций. Я говорю это, потому что внутри все эти пулы коллекций в Unity работают на основе статической переменной пула. Это означает вот что.
Использование этих пулов коллекций нарушит функцию, которая сокращает время итерации в редакторе: отключение перезагрузки домена. Если вы необдуманно используете такие статические пулы, объединенные элементы будут сохраняться на протяжении выполнений в редакторе. И это не очень весело.
Они, как и описывают их названия, являются пулами общих объектов. Но в них есть кое-что особенное.
GenericPool и UnsafeGenericPool
Итак, что такого особенного с этими пулами объектов?
Опять же, GenericPool и UnsafeGenericPool являются пулами статических объектов. Таким образом, их использование не позволит вам отключить перезагрузку домена, чтобы сократить время итерации редактора.
С другой стороны, вам не нужно беспокоиться о создании их для любого из ваших юзкейсов.
Вы просто используете их, когда и где бы (и кем бы) вы ни находились.
Вариант UnsafeGenericPool работает лучше за счет пропуска важной проверки: проверки уже возвращенного объекта. Видите ли, когда вы возвращаете объект в пул, возможно, вы уже возвращали его в прошлом (и не вынимали его из пула). Это может быть проще, чем вы думаете, особенно если вы используете статические пулы и одни и те же объекты используются в нескольких местах.
В этом случае элемент может дважды появиться во внутренней структуре данных пула. И угадайте, что происходит, когда вы берете два элемента?
Вы будете использовать один и тот же объект в двух разных местах, и естественно будете перезаписывать изменения в нем. Представьте, что вы использовали один и тот же игровой объект для двух разных игроков.
Подводя итоги различий:
GenericPool использует статический ObjectPool с collectionCheck = true
UnsafeGenericPool использует статический ObjectPool с collectionCheck = false
Хорошо, как вы видели, не все в пулах красиво и аккуратно. Но вотрем еще немного соли в рану.
Проблемы с пулами (почему вы не должны ими злоупотреблять)
Я мог бы написать как минимум 3 статьи, подробно описывающих неприятные проблемы, которые могут возникнуть с пулами.
Но вместо того, чтобы делать это, я просто обозначу их здесь, основываясь на отличном посте Джексона Данстана.
Вот некоторые из проблем, с которыми вы можете столкнуться при использовании пулов:
Вашим объектам требуется явный возврат. Если вы забудете вернуть объект пула, сборщик мусора должен будет выполнить ту работу, которой вы хотели избежать в первую очередь (в лучшем случае).
Вы должны сбросить состояние ваших объектов. Если вы не сбросите состояние своих объектов, старые данные будут попадать в экземпляры, которые вы получаете из пула. Объект больше не будет «свежим». Ваши пули могут содержать следы крови.
Если вы используете один и тот же объект из пула в разных местах, вы должны вернуть объект только тогда, когда все его пользователи закончат с ним. Вы не хотите возвращать объект несколько раз, так как это гарантированно вызовет у вас головную боль. Таким образом, вам потребуется какой-то ручной подсчет ссылок, дорогостоящие для процессора проверки или стратегия, чтобы убедиться, что есть только один владелец.
Управление памятью коллекций трудно. Вы можете пулить списки, хэш-множества, словари и тому подобное. Но делать предположения об их размерах сложно. Когда вы получаете список из пула, он может иметь размер 4, в то время как вам действительно нужен список размером 1000+. Вы бы принудительно изменили размер. Бывает и наоборот. Короче говоря, вы можете в конечном итоге потратить много памяти на поддержание жизни огромных коллекций, когда вам нужно всего несколько предметов для них.
По умолчанию пулы не являются потокобезопасными. А если вы добавите механизмы для поддержки потоковой безопасности, тогда вы добавите накладные расходы на процессор, которые могут больше не окупаться.
Хорошая пища для размышлений.
Так, что еще?
Пулы — отличные инструменты для снижения:
затрат производительности, связанных с распределением ресурсов в игровом процессе;
давления, которое вы оказываете на бедный сборщик мусора;
А с Unity 2021+ теперь стало проще, чем когда-либо, принять пул как образ жизни разработчика, поскольку теперь у нас есть встроенное pooling API.
Однако я объяснил темную сторону пулов. Сторона, которая может доставить вам массу боли во время разработки вашего проекта.
Пул — это еще один инструмент повышения производительности, который вы должны знать. И чем больше инструментов вы знаете, тем лучше.
Перевод материала подготовлен в рамках курса «Unity Game Developer. Professional». Если вам интересно узнать о курсе подробнее, приглашаем на день открытых дверей: на нем преподаватель расскажет о формате и особенностях обучения, о программе и выпускном проекте.