Оптимизация кода в Unity3d

» опубликован
Дисклеймер
Внимание, данный пост предназначен для начинающих программистов, которые начинают или уже работают в среде разработки Unity3d.
Примеры ниже - наиболее приемлемый вариант написания наиболее хорошего кода, надеюсь, что советчики, которые посчитают, что
код в данном посте для них ужасен или не лучше - не горите, а лучше попытайтесь максимально объективно разъяснить свою позицию.
Все ниже показанные примеры были протестированы и внизу вы сможете увидеть результат работы примеров без приёмов оптимизации
и с их приёмом.

Примеры оптимизации вашего кода

Циклы и условия

Как вы думаете, возможно ли оптимизировать несколько вложенных условий, по типу этого:
private void OnTriggerEnter(Collider other)
    {
        if(other.tag == "Player" && Input.GetKeyDown(KeyCode.Space)) //Если в триггер вошёл игрок и нажал на кнопку
        {
            if(isGround == true && manager != null) // И если булева переменная активирована и объект manager инициализирован
            {
               
            }
        }
    }
Это грубый пример того, с чем в процессе разработки игры вы сможете столкнуться. Согласитесь, это не совсем удобно, особенно,
если все условия записать в одном условном операторе, при условии, что в примере указана статичная кнопка нажатия Space.
А если необходимо изменить кнопку? Это другой вопрос, но сейчас я предлагаю заменить этот код на такой:
private readonly string playerTag = "Player";

     private void OnTriggerEnter(Collider other)
    {
       if(other.gameObject.CompareTag(playerTag) && isGround && CheckObject(manager))
        {
            if(IsPressedKey(KeyCode.Space))
            {
             ...
            }
        }
    }

    private bool IsPressedKey(KeyCode key) { return Input.GetKeyDown(key); }

    private bool CheckObject(GameObject other)
    {
        if (other == null) return false;
        else return true;
    }
Как можно видеть, здесь я вынес тэг в переменную и сразу её инициализировал. Таким образом, в отдельном классе или структуре,
вы можете составить список всех необходимых тэгов и обращаться всякий раз, где и когда это необходимо. Проверку на null'ое значение
я делаю в методе CheckObject(GameObject other) с одним входным параметром, которым является GameObject, который мы и будем проверять.
Это как минимум удобно, так как вам не придётся создавать множество методов на проверку различных объектов.
Далее, метод IsPressedKey(KeyCode key) позволяет нам проверять, нажата ли клавиша key, который мы указываем в качестве аргумента.
Опять же, это удобно, так как если будет необходимо хранить список "забинденых" кнопок, вы легко сможете делать их обработку в подобных
методах.
Далее о циклах. можно ли считать такой цикл правильным?
 for(int index = 0; index < names.Count; index++)
        {
            if(isActive)
            {

            }
        }
Конечно же нет. Во-первых, во многих примерах создания циклов, почему-то у многих в моду вошло использовать такой тип
данных как int. Конечно, очевидно мы не сможем использовать string или float для итератора. Однако, я хотел бы напомнить, что
существуют и другие целочисленные типы данных, которые в свою очередь имеют числовой диапазон либо меньше int, либо больше.
Так, я подвожу к тому, что если в вашем проекте где-то используется цикл примерно с десятью итерациями, то на мой взгляд, вы можете
спокойно, а главное логично использовать byte. Конечно, он имеет достаточно малый диапазон и не может уходить в минус, но если ваш
цикл не нуждается в этом, то смело используйте наиболее подходящий целочисленный тип данных.
Во-вторых, как вы могли заметить, в данном цикле присутствует условие, однако, я хочу напомнить, что такое цикл и почему это - плохой подход.
Данное условие будет проверятся каждую итерацию, что очень, ну уж очень затратно, особенно если аналогичного кода достаточно в проекте.
Вынесем это условие
if (isActive)
        {
            for (byte index = 0; index < names.Count; index++)
            {
				Debug.LogFormat("Iteration: {0}", index);
            }
        }
Теперь же, если вынести этот блок кода в какой нибудь метод, то условие будет проверятся один раз. Безусловно, бывают ситуации, когда
в условии необходимо оперировать с нашим итератором. Очевидно, что в таком случае условие не выйдет за рамки цикла, но в таком случае
мой совет состоит в том, чтобы вы запускали этот цикл, а лучше вызывали метод с этим циклом, когда это действительно необходимо.

Немного о вызове методов

Теперь же, хочу показать вот такой блок кода:
public Text outputHealth;

    public int health { get; private set; }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space)) health += 20;

        OutputHealth();
    }

    private void OutputHealth()
    {
        outputHealth.text = "Health: " + health;
    }
В этом ещё одном грубом примере при нажатии на кнопку Space мы прибавляем здоровье нашему игроку и выводим в текст текущее
здоровье. Как показал профайлер (скриншоты будут в самом финале) в отличии от наиболее хорошего варианта записи этого кода, GC Alloc и Time ms достаточно отличаются значениями. GC Alloc - это память, которая выделяется на один кадр, а Time ms - время рендера. У данного примера:
  • GC Alloc: (96-100) B
  • Time MS: 0.02
В наиболее приемлемом варианте же:
  • GC Alloc: 76 B
  • Time MS: 0.01
Немного лучше, собственно вот сам пример:
public Text outputHealth;
    private int health;

    public void DoubleHealth(int count)
    {
        health += count;
        OutputHealth();
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space)) DoubleHealth(20);
    }

    private void OutputHealth()
    {
        outputHealth.text = "Health: " + health.ToString();
    }
Здесь же добавление здоровье происходит в отдельном методе и вызывается только при нажатии, а не каждый кадр.
Простая математика: Вариант 1: Вызов метода идёт ( > 100.000) кадров за весь игровой процесс
Вариант 2: Вызов метода идёт ( < 100.000) кадров за весь игровой процесс. Это, если разбирать конкретный случай
так как могут быть методы, которые необходимо часто вызывать, но наберут ли они они столько же, сколько вариант 1?
Главное помните, что любой блок кода можно поместить в метод и сделать его намного универсальнее.

Инициализация переменных

Куда же и без наших родных переменных... В процессе разработки какого либо проекта, в программном коде накапливаются
свыше десятков переменных. И их необходимо инициализировать. Это можно делать практически любым способом. Иногда, для инициализации
переменных в Unity3d используют метод Find() класса GameObject. Очень удобно, однако, если в вашей сцене располагается множество объектов,
то этот метод ужасен тем, что:
  • Во-первых, он перебирает каждый объект в сцене в поисках нужного, что может сказаться на оптимизации
  • Во-вторых, он, со временем, станет менее удобным, так как названия ваших объектов могут начать повторяться, либо метод внезапно не
найдёт ваш объект, потому что вы сделали его дочерним и теперь вы вынуждены указывать другой путь до него.
Особенно может сказаться на оптимизации, если вы получаете объект в акссесоре get, так как метод перебора объектов будет активировываться
более одного раза.
Сейчас я опишу пару способов наиболее приемлемой инициализации объектов.
Вариант 1 (Действует только для компонентов объекта, на котором весит скрипт)
Самый банальный способ - инициализация компонента объекта в методе Start().
private Color spriteColor;

private void Start()
{
spriteColor = gameObject.GetComponent<SpriteRenderer>().color;
}
С помощью метода GetComponent<T>(), мы инициализировали нашу переменную на всю оставшеюся игровую сессию.
Точно также можно сделать и в get аксессоре, но тогда доступ к компоненту или его свойству будет выполняться более одного раза.
Второй вариант состоит в инициализации через окно Inspector. Просто добавляем нашей переменной модификатор доступа public и настраиваем
её в редакторе. Это наименее затратный метод инициализации компонента.

Сериализация классов

Хочу теперь рассказать о сериализации классов. Само понятие сериализация означает что мы переводим наш класс в поток битов.
В Unity3d сериализация классов может использоваться для группировки данных сложного объекта. Пример ниже
Вот код:
public class ExampleCode : MonoBehaviour {

    public GraphicSettings quality;
    public AudioSettings sound;
    
}

[System.Serializable]
public class GraphicSettings
{
    public Toggle toggleVSync, toggleShadows;
	[HideInInspector]
    public bool isActiveVSync, isActiveShadows;

}
[System.Serializable]
public class AudioSettings
{
    public Toggle toggleVolume;
    public Slider sliderVolume;
	
	[HideInInspector]
    public float valueVolume;
	[HideInInspector]
    public bool isActiveVolume;
}
Вот как это выглядит в окне Inspector
Я специально скрыл более значимые типы переменных, так как в моём примере приведены классы, хранящие данные о игровых настройках,
которые будут меняться очевидно не в инспекторе.

Напоследок

Думаю уже давно для всех не секрет, что стандартные методы SendMessage() и BroadcastMessage() в Unity не рекомендуются к использованию
даже ими самими
Как мы видим, нам предлагают использовать рефлексию. Помимо этого, они отмечают, что в некоторых ситуациях, когда вы не знаете,
какой компонент хотите вызвать, нам предлагают в таком случае использовать события и делегаты. Что такой рефлексия, делегаты и события,
здесь я рассказывать конечно же не буду, просто тоже рекомендую отказаться от использования этих методов.
Кстати, вы знали, что Camera.main = GameObject.Find()? Поясню, когда вы не хотите инициализировать камеру, потому что есть такая
замечательная вещь, как Camera.main, то вы поступаете не очень правильно. Во-первых, у многих в привычку вошло, что при создании лучей
(Raycast) нужно использовать именно такое обращение к камере. Однако, Camera.main вызывает GameObject.Find(), просто ищет объект с
именем "Main Camera" и получает компонент через тот же GetComponent<T>(). Вот так. Поэтому подумайте, прежде обращаться к камере
именно этим способом, так как вы не только перебираете ВСЕ объекты сцены в поисках камеры, так ещё и делаете это КАЖДЫЙ кадр.
Что лучше? Инициализировать камеру в методе Start() один раз или в процессе всей игровой сессии свыше тысячи раз обращаться к камере?
Вообще, я не рекомендую также использовать и метод GameObject.Find(), как вы могли уже понять.
Кое-что ещё. Иногда нам необходимо что-то воспроизводить в методе Update() или его аналогах. Например метод Move(), когда мы кем-то управляем.
Так вот. Есть последняя на сегодня рекомендация - использовать метод OnBecameVisible() для таких случаев. Все действия в этом методе будут
вызываться только тогда, когда объект на котором вызван этот метод, будет в поле зрения камеры. Примечание: данный совет рекомендуется
только для тех случаев, когда объект требует выполнения какого-нибудь метода, но он может уйти из поля зрения. Это может относится к врагам.
Например, когда вы реализовывайте патрулирование для ИИ через навигацию. Смысл ходить ИИ в те моменты, когда вы их не видите?
На этом всё, спасибо за внимание, я надеюсь, что данный пост поможет хоть кому нибудь и он переосмыслит свой код или его фрагмент.
Тестирование вывода здоровья (Рис. 1 - До оптимизации, рис. 2 - после оптимизации)
Тестирование Raycast с десятью объектами на сцене (Рис. 1 - До оптимизации, рис. 2 - после оптимизации)
Опрос: Ну как тебе?

Всего проголосовали: 1

 

Просмотров: 370

Комментарии пока отсутcтвуют