Добавлен , опубликован
Дисклеймер
Внимание, данный пост предназначен для начинающих программистов, которые начинают или уже работают в среде разработки 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. 
Очень Полезно!
2. 
Можно было бы и больше -_-
3. 
Meh, я и так всё знаю!!
`
ОЖИДАНИЕ РЕКЛАМЫ...
2
9
5 лет назад
Отредактирован AsagiriGen
2
Глаз зацепился за это и решил разобраться.
Автор категорически не прав - использование типов short или byte для индексов в цикле только ухудшит ситуацию.
  1. По сравнению со случаем использования int-индекса в данном случае в IL-коде инкремента будет еще функция преобразования <результата сложения индекса и единицы в стеке> в тип Int16(short). И в коде ассемблера, в который JIT-компилятор преобразует IL-код, эта лишняя работа никуда не исчезнет, а будет выглядеть как что-то вроде movsx eax, ax. То есть мы экономим 8 или 16 бит, но имеем гарантированно дополнительную работу в цикле. Это просто дичайше не целесообразно.
  1. Только-только у нас в names по каким-то причинам (не обязательно нежданно, просто на определенном этапе оказалось, что нужно вмещать больше данных) разрастется до длинны, выходящие за ограничения byte или short как сразу все летит к чертовой матери и садись меняй типы в коде.
В целом байтодрочерство почти всегда не оправдано. Процессоры привыкли работать с 32-битными числами. Да и, думаю, не нужно объяснять, что, учитывая вышеописанные издержки, стараться сохранить несколько бит просто глупо. Тем более когда выравнивание данных может внезапно нивелировать драгоценные ваши старания по сохранению памяти..
2
30
5 лет назад
2
    private bool CheckObject(GameObject other)
    {
        if (other == null) return false;
        else return true;
    }
И тут я умер
return other != null для слабых
1
0
5 лет назад
1
После истории с "оптимизацией" цикла рассматривал статью, как список вредных советов.
В блоках кода, которые требовательны к производительности для целочисленных рассчетов всегда, всегда применяйте тип int или dword. Это универсальные типы, для х86-64 платформ, ибо все команды оперируют именно ими или удвоенными аналогами в случае 64битных систем. В следствии чего не происходит ненужных преобразований.
Со строками ваще фейл, команда string.concat быстрее работает и не создает лишнего мусора в памяти.
Статья новичка для хз кого,, полезности ноль.
Скажу новичкам пару советов: кэшируйте ссылки на компонентыних, если используется кодом чаще раза в секунду, у скриптов удаляйте пустые "магические" методы start, update. Если объектов со скриптами более 1к и, в них используется метод update, то делайте менеджеры для этих объектов и ими обновляйте состояние обьектов, а функцию update удаляйте. Так вы выйграете в несколько раз в скорости, пожертвовав отказоустойчивостью.
0
29
5 лет назад
0
Clamp, Любой код требует оптимизации, даже который учит этому)
Чтобы оставить комментарий, пожалуйста, войдите на сайт.