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 это выглядит вот так (просто чтобы вы полностью поняли идею):

4d97cffff7c74240aa33c0b106cd2f20.PNG

Я люблю держать все аккуратно по полочкам, поэтому у меня даже два отдельных менеджера (для пуль, и для различных эффектов).

Сначала объясняю план действий, потом смотрим на коде:

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% всей сцены (всё кроме кораблей) создается через пулинг объектов, вы получите слегка более длительную загрузку уровня, но при этом во время геймплея никаких стопоров не будет.

c8500010217445948371f113637ebe1f.gif

P.S.: Данную технику не обязательно использовать если у вас неторопливая игра, в которой не много повторяющихся объектов, однако эту технику нужно абсолютно точно использовать если вы собрались делать раннер или бодрый шутер. В общем все, как всегда — всё хорошо в меру.

P.P.S.: Спасибо за внимание! Есть вопросы? Пишите в комменты — отвечу обязательно.

© Habrahabr.ru