Unity — Концептуальные идеи и подсказки для новичков игродева. Мощная оптимизация 2D проекта
Ссылка на первую статью из этой серии.
Быстрое вступление
Я долго думал какую тему выбрать на этот раз, и решил, что расскажу про некоторые фишки, которые помогут оптимизировать вашу игру. Особенно это будет актуально для новичков, потому что чаще всего первая игра оказывается игрой для смартфонов и планшетов. А на мобилках, сколько бы там ядер не оказывалось — 6 или 8, игры все ещё очень несбалансированные в плане потребления ресурсов. Код и его идея, которые будут приводится в этой статье являются немного более сложными для понимания чем те две строчки кода, которые я приводил в своей предыдущей публикации. Хотя, я не правильно выразился — код легко понять — но порог вхождения в это понимание будет чуть выше чем легко (для новичков конечно), придется посидеть минут 5.
Введение в идею
Как вы создаете объекты в Unity из префабов? Только Instantiate и никак иначе — другой функции там просто не существует.
Для тех кто не помнит или еще не знает, что такое Instansiate (), маленькая справка — *аргументы инстанции*. Вот вы их создаете (объекты), и создаете, иногда удаляете, потом опять создаете, и так в течении всего уровня. Это бьёт по оптимизации и существенно — почему? Потому что так написано на всех форумах Unity, а иногда проглядывается и в документации, а документацию надо слушаться. Как Instansiate бьет по про производительности?
Вот например такой код: Instansiate(bullet, transform.position, tranform.rotation);
Что происходит? Берется префаб, который вы положите в переменную bullet, в недрах вашего устройства ищется свободная память, память выделяется под размер оригинального префаба, туда записывается голая копия вашего префаба с его компонентами, потом все параметры компонентов оригинального префаба копируются в ваш новый клон, и он возвращается на сцену по заданной позиции и под заданным углом.
Наверное, это долго, да. Представьте, что вы делаете раннер(где все объекты повторяются через каждые пять внутри-игровых метров) или просто шутер — катастрофа, если хотя-бы каждую секунду будет происходить инструкция, которую я описал выше. Что же делать? Использовать pooling объектов.
P.S.: Есть видео на оф. сайте Unity про то, что такое object pooling и как это осуществить. Можете посмотреть и его, если вы с английским языком на ты. Но я приведу ту же идею, но с более прозрачной реализацией этого пулинга (однако, она будет более емкой в плане строчек кода).
Поехали!
Как и предыдущий раз я буду демонстрировать все на своем нынешнем проекте. Признаться честно я изначально не делал object pooling а вместо этого просто создавал объекты во время игрового процесса. И да в какой-то момент, когда на экране появлялось большое количество космических корабликов и все они начинали стрелять, начали появляться скачки фпс, а это очень раздражает. Пришлось переделывать.
Повторюсь что данная реализация будет более громоздкой чем от Unity (они используют экземпляры одного класса, что-бы для отдельного объекта (например, космического корабля) создавать пулл необходимых ему объектов ( например, пули). Я же пошел по другому пути и создал отдельный, так сказать скрипт-менеджер пуллинга объектов.
То-есть у меня в редакторе Unity это выглядит вот так (просто чтобы вы полностью поняли идею):
Я люблю держать все аккуратно по полочкам, поэтому у меня даже два отдельных менеджера (для пуль, и для различных эффектов).
Сначала объясняю план действий, потом смотрим на коде:
1) В своем скрипте который будет работать с объектами пулинга, создаете List (объектов конечно);
2) При старте игры сразу создаете в цикле нужное кол-во объектов для пулинга. Причем в цикле на каждой итерации после создания одного экземпляра объекта отключаете его и добавляете в ваш созданный до этого List;
3) Переходите в скрипт объекта, который будет активировать ваши объекты в листе (например в скрипт, который управляет корабликом). Создаете связь с объектом который является вашим менеджером пулов( Или сделайте класс с пулами статичным, чтобы обращаться напрямую). И в нужный момент(например стрельба) вы в своей корутине(обычно корутины используются для осуществления стрельбы) вызываете функцию которая будет активировать ваш объект для пула с нужными параметрами. Все.
Теперь то же самое, но на практике. Представлен код менеджера пулов(только для одного объекта RedMissle).
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class BulletsContainer : MonoBehaviour {
List<GameObject> missleRed=new List<GameObject>();
// В переменную RedMissle в редакторе закидываете ваш префаб
public GameObject RedMissle;
// Переменная missleCount будет контролировать сколько максимально на экране может быть объектов
public int missleCount;
void Awake()
{
// То же самое, что я описал во втором пункте теоретического разбора выше
for(int i=0;i<missleCount;b++)
{
GameObject temp=(GameObject)Instantiate(RedMissle);
temp.SetActive(false);
missleRed.Add(temp);
}
}
public void LaunchEnemyRedMissle(GameObject Caller)
{
if (missleRed != null)
{
for (int i=0; i<missleCount; i++)
{
// Проверяем неактивен-ли объект из листа в нашей сцене
// Если активен, то идем по индексу в списке дальше, чтобы
// найти неактивный и активировать его
if (!missleRed [i].activeInHierarchy)
{
// главное строчка тут - это " missleRed [i].SetActive (true); " - она обязательна
// остальные строчки могут быть другими в зависимости от вашей игры и того,
// что вы пуллите, поэтому можете не обращать внимание на них. Но ниже
// я объясню про каждую строку,что и для чего, просто чтобы вы лучше поняли
// как можно эту функцию расширять
missleRed [i].SetActive (true);
missleRed [i].transform.eulerAngles =Caller.transform.eulerAngles;
missleRed [i].transform.position = Caller.transform.position;
missleRed [i].GetComponent<DisableObject> ().StartCoroutine ("AblePause");
break;
// скобочки я поставил в ряд, просто чтобы не разводить много пустого места
}}}}}
Теперь по поводу дополнительных строчек.
1) "missleRed [i].transform.eulerAngles =Caller.transform.eulerAngles;"
— Как вы видите я принимаю в аргументе функции объект Caller, думаю все могут догадаться, что это объект из которого была вызвана эта функция. Скорее всего вам придется часто так делать, чтобы активировать объект из пула в позиции вызывающего объекта, как это сделано в этой следующей строке "missleRed [i].transform.position = Caller.transform.position;".
2) " missleRed [i].GetComponent ().StartCoroutine («AblePause»);"
— так вам тоже придется часто делать. Дело в том, что каждый раз когда вы активируете объект из пула вы должны его деактивировать через какое-то время ( вы можете поставить много условий деактивации, например когда пуля сталкивается с кораблем игрока, она осуществляет, что-то типо:" this.gameobject.SetActive(false); "). Поэтому у меня на каждом объекте пула навешен скрипт который деактивирует его через определенное время. И еще раз : Не забывайте деактивировать объекты пула, иначе, в какой-то момент все ваши объекты окажутся активными и цикл их активации просто не будет выполняться!
Тогда код в кораблике (или любом вашем объекте, который вызывает функцию активации пула) будет таким:
//....Какой-нибудь код .....
// устанавливаете связь с объектом, который отвечает за пулинг, как вам больше нравится
// у меня например так:
EnemiesContainer = GameObject.FindGameObjectWithTag ("ShipsGenerator");
BulletsSystem = EnemiesContainer.GetComponent<BulletsContainer> ();
//....Какой-нибудь код .....
IEnumerator StartAttack()
{
float randomTime = Random.Range (cooldownMin, cooldownMax + 1);
yield return new WaitForSeconds (randomTime);
// тут мы вызываем нашу функцию активации объекта в пуле, аргументом передаем себя
BulletsSystem.LaunchEnemyRedMissle(this.gameObject);
}
//....Какой-нибудь код .....
Заключение
Вот пример результата — большое количество объектов на сцене, практически 80% всей сцены (всё кроме кораблей) создается через пулинг объектов, вы получите слегка более длительную загрузку уровня, но при этом во время геймплея никаких стопоров не будет.
P.S.: Данную технику не обязательно использовать если у вас неторопливая игра, в которой не много повторяющихся объектов, однако эту технику нужно абсолютно точно использовать если вы собрались делать раннер или бодрый шутер. В общем все, как всегда — всё хорошо в меру.
P.P.S.: Спасибо за внимание! Есть вопросы? Пишите в комменты — отвечу обязательно.