Version: 5.3 (switch to 5.4b)
Оптимизации рендеринга
Legacy Topics

Оптимизация скриптов

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

Profiler - король (только для Unity Pro)

Не существует такой штуки, как список задач, по которому надо пройтись, чтобы ваш проект стал работать плавнее. Для оптимизации медленного проекта вам придётся профилировать определённые участки, занимающие слишком много времени при выполнении. Попытки оптимизировать без профилирования или без чёткого понимания результатов, выдаваемых профайлером, это как пытаться оптимизировать с завязанными глазами.

Internal mobile profiler

Вы можете использовать встроенный профайлер для выяснения процесса, тормозящего вашу игру, будь то физика, скрипты, или отрисовка, но вы не сможете взглянуть на определённые скрипты и методы для поиска точного источника проблемы. Однако, добавив в вашу игру переключатели для включения или отключения того или иного функционала, вы сможете отследить самые проблемные участки. Например, если вы удалите скрипт AI для персонажей противника и при этом частота кадров возрастёт вдвое, вы поймёте, что следует оптимизировать этот скрипт, или что-то из того, что он привносит в игру. Основная проблема в том, что вам может потребоваться много попыток, прежде чем вы сможете обнаружить проблему.

Для дополнительной информации по профилированию на мобильных устройствах прочтите раздел о профайлинге.

Оптимизирован по дизайну

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

Пулинг объектов

Во введении в методы оптимизированного скриптинга в качестве примера пересечения игрового процесса и хорошего дизайна кода мы привели пулинг объектов. Использование пулинга объектов для недолговечных объектов быстрее, чем их создание и уничтожение, т.к. пулинг упрощает процесс выделения памяти, исключает динамическое выделение памяти и сборку мусора (Garbage Collection или GC).

Выделение памяти

Простое объяснение понятия “автоматическое управление памятью”

Скрипты, которые вы пишете в Unity используют автоматическое управление памятью. А низкоуровневые языки, такие как C и C++, наоборот, используют ручное управление памятью - программист может напрямую считывать и записывать данные по указанным адресам памяти и он ответственен за удаление любого создаваемого им объекта. Например, если вы создаёте объекты в вашем C++, то после того как вы закончили с ними работу, вы обязаны вручную освободить выделенную для них память. В скриптовом же языке, достаточно написать objectReference = null;.

Важно: Почему может не уничтожаться переменная типа Game Object, например, GameObject myGameObject; или var myGameObject : GameObject;, когда я пишу myGameObject = null;?

  • Unity всё ещё ссылается на объект, т.к. Unity должна сохранять на него ссылку для отрисовки, обновления и т.д. Вызов Destroy(myGameObject); удаляет эту ссылку и сам объект.

Но если вы создадите объект, о котором Unity ничего не знает, например, экземпляр класса, который ни от чего не наследуется (большинство классов или “скриптовых компонентов” наследуются от MonoBehaviour) и затем установите вашей переменной со ссылкой значение null, то на самом деле объект будет потерян для скрипта и Unity, т.к. они не смогут получить к нему доступ и никогда снова его не увидят, но при этом он останется в памяти. Затем, через какое-то время отработает сборщик мусора и при этом удалит из памяти всё, на что нет ссылок. Он может это сделать, т.к. в недрах сборщика ведётся учёт количества ссылок на каждый блок памяти. Это одна из причин, по которым скриптовые языки медленней C++.

Как избежать выделения памяти

Память выделяется каждый раз, когда создаётся объект. Зачастую в коде вы создаёте объекты даже не подозревая об этом.

  • Debug.Log("boo" + "hoo"); создаёт объект.
    • Используйте System.String.Empty вместо "" при работе с большим количеством строк.
  • OnGUI (UnityGUI) работает медленно и его не следует использовать в случаях, когда важна производительность.
  • Различия между классом и структурой:

Классы - это объекты и ведут себя как ссылки. Если Foo - это класс и

  Foo foo = new Foo();
  MyFunction(foo); 

тогда MyFunction получит ссылку на оригинальный объект Foo, память для которого была выделена в куче. Любые изменения foo в MyFunction отразятся везде, где есть ссылки на foo.

Классы - это данные и ведут себя соответствующе. Если Foo - это структура и

  Foo foo = new Foo();
  MyFunction(foo); 

то MyFunction получит копию foo. Память для foo никогда не выделяется из кучи и никогда не подвергается сборке мусора. Если MyFunction изменяет свою копию foo, то это не влияет на остальные foo.

  • Объекты, предназначенные для длительного использования должны быть классами, а объекты для непродолжительного использования - структурами. Vector3 - вероятно самая известная структура. Если бы он был классом, всё работало бы значительно медленней.

Почему пулинг объектов быстрее

Дело в том, что частое использование Instantiate и Destroy подкидывает сборщику мусора прилично работы, что может привести к рывкам во время игры. Как рассказано на странице про автоматическое управление памятью, существуют другие способы обойти основные проблемы с производительностью, окружающие Instantiate и Destroy, такие как ручной запуск сборщика мусора пока на экране ничего не происходит, или очень частый его запуск для предотвращения накапливания большого количества работы для сборщика.

Другая причина в том, что иногда в память должны прогрузиться дополнительные вещи при создании первого экземпляра определённого префаба, либо в GPU должны загрузиться текстуры и меши. Это также может вызвать рывок, а при использовании пулинга объектов это произойдёт при загрузке уровня, а не во время игры.

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

Почему пулинг объектов может быть медленнее

Одна из причин в том, что создание пула уменьшает количество доступной для других целей памяти кучи. Так что, если вы продолжите выделять память в только что созданных пулах, вы можете вызывать слишком частое срабатывание сборщика мусора. Кроме того, каждая сборка мусора будет проходить медленней, т.к. затрачиваемое на сборку время увеличивается с ростом количества активных объектов. Исходя из этих соображений, должно быть очевидно, что при использовании слишком больших пулов или активных пулов с объектами, которые какое-то время не понадобятся, пострадает производительность. Более того, многие типы объектов не подходят для содержания в пулах. Например, в игре могут быть существующие определённое время магические эффекты, или враги, которые появляются в больших количествах, но уничтожаются постепенно по мере прохождения игры. В таких случаях ресурсоёмкость объектного пула значительно перевешивает выгоду от его использования, так что его лучше вообще не использовать.

Реализация

Вот простое сравнение скриптов простой пули с использованием Instantiate и с использованием пулинга объектов.

 // GunWithInstantiate.js                                                  // GunWithObjectPooling.js

 #pragma strict                                                            #pragma strict

 var prefab : ProjectileWithInstantiate;                                   var prefab : ProjectileWithObjectPooling;
                                                                           var maximumInstanceCount = 10;
 var power = 10.0;                                                         var power = 10.0;

                                                                           private var instances : ProjectileWithObjectPooling[];

                                                                           static var stackPosition = Vector3(-9999, -9999, -9999);

                                                                           function Start () {
                                                                               instances = new ProjectileWithObjectPooling[maximumInstanceCount];
                                                                               for(var i = 0; i < maximumInstanceCount; i++) {
                                                                                   // place the pile of unused objects somewhere far off the map
                                                                                   instances[i] = Instantiate(prefab, stackPosition, Quaternion.identity);
                                                                                   // disable by default, these objects are not active yet.
                                                                                   instances[i].enabled = false;
                                                                               }
                                                                           }

 function Update () {                                                      function Update () {
     if(Input.GetButtonDown("Fire1")) {                                        if(Input.GetButtonDown("Fire1")) {
         var instance : ProjectileWithInstantiate =                                var instance : ProjectileWithObjectPooling = GetNextAvailiableInstance();
             Instantiate(prefab, transform.position, transform.rotation);          if(instance != null) {
         instance.velocity = transform.forward * power;                                instance.Initialize(transform, power);
     }                                                                             }
 }                                                                             }
                                                                           }

                                                                           function GetNextAvailiableInstance () : ProjectileWithObjectPooling {
                                                                               for(var i = 0; i < maximumInstanceCount; i++) {
                                                                                   if(!instances[i].enabled) return instances[i];
                                                                               }
                                                                               return null;
                                                                           }




 // ProjectileWithInstantiate.js                                           // ProjectileWithObjectPooling.js

 #pragma strict                                                            #pragma strict

 var gravity = 10.0;                                                       var gravity = 10.0;
 var drag = 0.01;                                                          var drag = 0.01;
 var lifetime = 10.0;                                                      var lifetime = 10.0;

 var velocity : Vector3;                                                   var velocity : Vector3;

 private var timer = 0.0;                                                  private var timer = 0.0;

                                                                           function Initialize(parent : Transform, speed : float) {
                                                                               transform.position = parent.position;
                                                                               transform.rotation = parent.rotation;
                                                                               velocity = parent.forward * speed;
                                                                               timer = 0;
                                                                               enabled = true;
                                                                           }

 function Update () {                                                      function Update () {
     velocity -= velocity * drag * Time.deltaTime;                             velocity -= velocity * drag * Time.deltaTime;
     velocity -= Vector3.up * gravity * Time.deltaTime;                        velocity -= Vector3.up * gravity * Time.deltaTime;
     transform.position += velocity * Time.deltaTime;                          transform.position += velocity * Time.deltaTime;

     timer += Time.deltaTime;                                                  timer += Time.deltaTime;
     if(timer > lifetime) {                                                    if(timer > lifetime) {
                                                                                   transform.position = GunWithObjectPooling.stackPosition;
         Destroy(gameObject);                                                      enabled = false;
     }                                                                         }
 }                                                                         }


Конечно, для большой, сложной игры вам хотелось бы иметь универсальное решение, работающее для всех ваших префабов.

Другой пример: вечеринка монеток!

Для демонстрации того, как можно создать впечатляющий эффект с помощью скриптинга, компонентов Unity, таких как Particle System, и пользовательских шейдеров, без поблажек для слабого железа мобильных устройств, будет использован пример из раздела о методах скриптинга “Сотни вращающихся собираемых монет с динамическим освещением на экране в один момент времени”.

Представьте, что этот эффект существует в рамках 2D игры-скроллера, с тоннами монеток, которые падают, отскакивают и вращаются. Монетки динамически подсвечены точечными источниками освещения. Мы желаем захватить блеск монет от света, чтобы сделать игру более впечатляющей.

Если бы у нас было мощное железо, мы могли бы использовать стандартный подход к этой проблеме. Создать каждую монетку отдельным объектом, применить к объекту шейдер с повершинным, прямым или упреждающим освещением, и затем добавить свечение поверх всего этого с помощью пост-эффекта для получения ярко блистающих монет излучающих свет в окружающее пространство.

Но железо мобильных устройств задохнётся от такого большого количества объектов и вопрос свечения даже не будет рассматриваться. Что же нам делать?

Система частиц из анимированных спрайтов

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

  • Собираемые объекты или монетки
  • Летающий мусор
  • Hordes or Flocks of Simple Enemies
  • Ликующие толпы
  • Сотни пуль или взрывов

Существует бесплатное расширение для редактора Sprite Packer, которое позволят создавать системы частиц из анимированных спрайтов. Оно отрисовывает кадры вашего объекта в текстуру, которая затем может использоваться в качестве атласа анимированных спрайтов для системы частиц. В нашем случае, мы могли бы использовать его для вращения монетки.

Пример реализации

В Sprite Packer включён проект-пример, демонстрирующий решение именно для этой проблемы.

Он использует семейство ассетов различных типов для достижения ослепительного эффекта на низкопроизводительном железе:

  • Управляющий скрипт
  • Специализированные текстуры, созданные из результата работы SpritePacker
  • Специализированный шейдер, который тесно связан как с управляющим скриптом, так и с текстурой.

Вместе с примером поставляется файл readme, в котором объясняется как и почему система работает, с изложением процесса, использованного для определения необходимых функций и их реализации. Вот этот файл:

Проблема была определена как “Сотни вращающихся собираемых монет с динамическим освещением на экране в один момент времени”.

Наивный подход - создать кучу экземпляров перфабы монетки, но вместо этого мы собираемся использовать частицы для отрисовки наших монеток. Однако, это привносит ряд испытаний, которые нам придётся преодолеть.

  • Угол зрения - проблема, т.к. частицы его не используют.
    • Мы предполагаем, что камера остаётся правой стороной вверх и монетки вращаются вокруг оси Y.
    • Мы создаём иллюзию вращения монеток с помощью анимированной текстуры, которую мы упаковали с помощью SpritePacker.
      • Это привносит новую проблему: монотонность вращения всех монеток с одной скоростью и в одном направлении. *Чтобы это исправить, мы следим за вращением и нашей продолжительностью жизни и “отрисовываем” вращение в продолжительность жизни частицы в скрипте.
  • Нормали - тоже проблема, т.к. у частиц их нет, но нам требуется освещение в реальном времени.
    • Генерируем одиночный вектор нормали для полигона монетки в каждом анимированном кадре, сгенерированном в Sprite Packer.
    • Производим Blinn-Phong освещение для каждой частицы в скрипте, основываясь на векторе нормали, полученном из указанного выше списка.
    • Применяем результат к частице в качестве цвета.
    • В шейдере отдельно обрабатываем лицевую часть монетки и её обод. Что привносит новую проблему: как шейдеру узнать где - ободок, и на какой части обода он находится?
      • Нельзя использовать развёртки UV, т.к. они уже используются для анимации.
      • Используем карту текстуры.
        • Требуется Y-координата относительно монетки.
        • Требуется двоичное “на лицевой поверхности” против “на ободке”.
      • Мы не хотим вводить новую текстуру, дополнительные чтения текстур, и дополнительного потребления памяти текстурами.
      • Объединим требуемую информацию в один канал и заменим им один из каналов текстуры.
        • Теперь у нашей монетки неправильный цвет! Что нам делать?
        • Использовать шейдер для воссоздания отсутствующего канала с помощью объединения двух оставшихся.
  • Скажем, мы хотим получить свечение от света, отражающегося от наших монет. Пост-обработка слишком ресурсоёмка для мобильных устройств.
    • Создадим другую систему частиц и будем использовать в ней смягчённую, светящуюся версию анимации монетки.
    • Рисуем свечение только при слишком ярком соответствующем цвете монетки.
    • Мы не можем себе позволить отрисовку свечения каждый кадр на каждой монетке - это очень негативно скажется на fill rate (скорости пиксельного заполнения).
      • Очищаем свечения каждый кадр, позиционируем только те, что имеют яркость > 0.
  • Физика - проблема, сбор монеток - проблема: столкновения с частицами не очень хорошо ловятся.
    • Можем ли мы использовать встроенный механизм коллизий частиц?
    • Вместо этого, мы просто прописали столкновение в скрипт.
  • Наконец, у нас осталась одна проблема - скрипт выполняет много функций и становится медленным!
    • Производительность линейно зависит от количества активных монеток.
      • Ограничим максимальное количество монеток. Для наших целей хорошо подходят настройки: 100 монеток, 2 источника света. Это довольно быстро работает на мобильных устройствах.
  • Дополнительные вещи, которые тоже можно попробовать оптимизировать:
    • Вместо отдельного подсчёта освещения для каждой монетки, можно поделить мир на части и рассчитывать условия освещения для каждого кадра вращения в каждой из частей мира.
      • Использовать в виде поисковой таблицы с положением монеток и вращением монеток в качестве индексов.
      • Увеличить точность используя билинейную интерполяцию с положением.
      • Редкие обновления поисковой таблицы, или вообще полностью статическая поисковая таблица.
      • Использовать для этого зонды освещения (Light Probes)?
    • Вместо подсчёта освещения в скрипте, использовать частицы с картой нормалей?
      • Использовать шейдер “Display Normals” (с отображением нормалей) для запекания покадровой анимации нормалей.
      • Ограничивает количество источников освещения.
      • Исправляет проблему медленного скрипта.

Основная цель этого примера или “мораль сей истории” - показать, что если при использовании стандартных подходов при реализации чего-то действительно нужного вашей игре, у вас оно “тормозит”, это вовсе не значит что это невозможно реализовать, это лишь значит, что вам следует немного поработать над собственной системой, которая будет работать намного быстрее.

Способы работы с тысячами объектов

Существуют специфичные оптимизации скриптов, которые применимы в ситуациях, в которых участвуют сотни или тысячи динамических объектов. Применение этих способов в каждом скрипте вашей игры - ужасная идея. Их следует приберечь в качестве инструментов и советов по дизайну для больших скриптов, которые обрабатывают массу объектов или данных во время выполнения приложения.

Избегайте или минимизируйте O(n2) операции на больших наборах данных

В компьютерной науке, порядок операции, обозначаемый O(n), относится к способу, при котором при увеличении количества объектов, к которым применяется операция (n), соответственно увеличивается количество вызовов этой операции.

Например, представьте простой алгоритм сортировки. У меня есть n чисел и я хочу отсортировать их от меньшего к большему.

 void sort(int[] arr) {
    int i, j, newValue;
    for (i = 1; i < arr.Length; i++) {
        // record
        newValue = arr[i];
        //shift everything that is larger to the right
        j = i;
        while (j > 0 && arr[j - 1] > newValue) {
            arr[j] = arr[j - 1];
            j--;
        }
        // place recorded value to the left of large values
        arr[j] = newValue;
    }
 }

Важно отметить, что тут применяется два цикла, один внутри другого.

 for (i = 1; i < arr.Length; i++) {
    ...
    j = i;
    while (j > 0 && arr[j - 1] > newValue) {
        ...
        j--;
    }
 }

Предположим, что алгоритм будет работать с самым худшим случаем: входящие числа отсортированы, но в обратном порядке. В таком случае, вложенный цикл выполнится j раз. В среднем, когда i меняется от 1 до arr.Length–1, j будет arr.Length/2. С точки зрения O(n), arr.Length - это наше n, так что, в итоге вложенный цикл выполнится n*n/2 раз, или n2/2 раз. Но в рамках O(n) мы выбрасываем все постоянные вроде 1/2, т.к. мы желаем говорить о том, как увеличивается количество операций, а не о самом количестве операций. Так что алгоритм будет таким: O(n2). Порядок операций имеет большое значение при работе с большим набором данных, т.к. количество операций может стремительно расти по экспоненциальной кривой.

Игровой пример O(n2) операции - 100 врагов, где ИИ каждого врага учитывает передвижения всех остальных врагов. Возможно будет быстрее разбить карту на ячейки, записать передвижение каждого врага в соседнюю ячейку и затем заставить каждого из врагов проверять несколько ближайших ячеек. Тогда это была бы O(n) операция.

Кэшируйте ссылки вместо осуществления повторного поиска

Например, в вашей игре есть 100 врагов, и все они двигаются в сторону игрока.

 // EnemyAI.js
 var speed = 5.0;
 
 function Update () {
    transform.LookAt(GameObject.FindWithTag("Player").transform);
    // this would be even worse:
    //transform.LookAt(FindObjectOfType(Player).transform);
 
    transform.position += transform.forward * speed * Time.deltaTime;
 }

Это может работать медленно, если среди них достаточно много бегущих одновременно. Небольшой известный факт: все поля для доступа к компонентам в MonoBehaviour, такие как transform, renderer, и audio, эквивалентны соответствующим вызовам GetComponent(Transform), и потому они работают немного медленно. Метод GameObject.FindWithTag был оптимизирован, но в некоторых случаях, например, во вложенных циклах, или в скриптах, которые запущены на большом количестве экземпляров, оно тоже может работать немного медленно.

Вот улучшенная версия скрипта.

 // EnemyAI.js
 var speed = 5.0;
 
 private var myTransform : Transform;
 private var playerTransform : Transform;
 
 function Start () {
    myTransform = transform;
    playerTransform = GameObject.FindWithTag("Player").transform;
 }
 
 function Update () {
    myTransform.LookAt(playerTransform);
 
    myTransform.position += myTransform.forward * speed * Time.deltaTime;
 }

Избегайте использования ресурсоёмких математических функций

Трансцендентные функции (Mathf.Sin, Mathf.Pow, и т.д.), деление, и квадратный корень - все примерно занимают столько же времени, сколько занимает сотня операций произведения. В общей картине это может не иметь значения, но если вы вызываете их тысячи раз за кадр, то это уже может быть заметно.

Наиболее распространённый случай - нормализация вектора. Если вы нормализуете один и тот же вектор снова и снова, то постарайтесь нормализовать его один раз и сохранить полученный результат для дальнейшего использования.

Если вы не только нормализуете вектор, но и используете его длину, то было бы быстрее получить нормализованный вектор с помощью его умножения на величину, обратную длине, чем с помощью свойства .normalized.

Если вы сравниваете расстояния, вы не обязаны сравнивать настоящие расстояния. Вместо этого вы можете сравнить квадраты длин с помощью свойства .sqrMagnitude и сохранить при этом время, которое потребовалось бы для вычисления одного или пары квадратных корней.

Ещё совет. Если вы делите снова и снова на одну и ту же константу c, вы вместо этого можете умножать на обратное число. Только сперва рассчитайте обратное число с помощью выражения 1.0/c.

Сложные операции, напр., Physics.Raycast(), выполняйте лишь изредка

Если вам приходится выполнять что-то ресурсоёмкое, вы могли бы оптимизировать это с помощью более редких вызовов и кэширования результата. Например, вот скрипт для пули, который использует Raycast:

 // Bullet.js
 var speed = 5.0;
 
 function FixedUpdate () {
    var distanceThisFrame = speed * Time.fixedDeltaTime;
    var hit : RaycastHit;
 
    // every frame, we cast a ray forward from where we are to where we will be next frame
    if(Physics.Raycast(transform.position, transform.forward, hit, distanceThisFrame)) {
        // Do hit
    } else {
        transform.position += transform.forward * distanceThisFrame;
    }
 }

Мы сразу могли бы улучшить скрипт заменив FixedUpdate на Update и fixedDeltaTime на deltaTime. FixedUpdate относится к обновлению физики, что происходит чаще, чем обновление кадра. Но давайте пойдём ещё дальше, производя рейкаст только каждые n секунд. Чем меньше n, тем выше временное разрешение, и чем выше n, там выше производительность. Чем крупнее и медленнее ваши цели, тем большее значение может быть использовано для n до тех пор, пока не начнёт происходить темпоральное сглаживание (появление задержки, когда игрок попал по цели, но взрыв появляется там, где цель была n секунд назад, или когда игрок попал по цели, но пуля прошла сквозь неё).

 // BulletOptimized.js
 var speed = 5.0;
 var interval = 0.4; // this is 'n', in seconds.
 
 private var begin : Vector3;
 private var timer = 0.0;
 private var hasHit = false;
 private var timeTillImpact = 0.0;
 private var hit : RaycastHit;
 
 // set up initial interval
 function Start () {
    begin = transform.position;
    timer = interval+1;
 }
 
 function Update () {
    // don't allow an interval smaller than the frame.
    var usedInterval = interval;
    if(Time.deltaTime > usedInterval) usedInterval = Time.deltaTime;
 
    // every interval, we cast a ray forward from where we were at the start of this interval
    // to where we will be at the start of the next interval
    if(!hasHit && timer >= usedInterval) {
        timer = 0;
        var distanceThisInterval = speed * usedInterval;
 
        if(Physics.Raycast(begin, transform.forward, hit, distanceThisInterval)) {
            hasHit = true;
            if(speed != 0) timeTillImpact = hit.distance / speed;
        }
 
        begin += transform.forward * distanceThisInterval;
    }
 
    timer += Time.deltaTime;
 
    // after the Raycast hit something, wait until the bullet has traveled
    // about as far as the ray traveled to do the actual hit
    if(hasHit && timer > timeTillImpact) {
        // Do hit
    } else {
        transform.position += transform.forward * speed * Time.deltaTime;
    }
 }

Избегайте перегрузки стека вызовов во вложенных циклах

Просто вызов метода уже сам по себе несёт в себе небольшую нагрузку. Если вы вызываете такие вещи, как x = Mathf.Abs(x) тысячи раз за кадр, то лучше просто выполнять x = (x > 0 ? x : -x);.

Оптимизация производительности физики

Используемый в Unity движок NVIDIA PhysX доступен и на мобильных устройствах, но там, по сравнению с настольными системами, намного проще упереться в ограничения производительности железа.

Вот несколько советов по настройке физики для получения более высокой производительности на мобильных устройствах:-

  • Вы можете настроить Fixed TimestepTime manager) для сокращения времени, затрачиваемого на расчёты физики. Увеличение Timestep снизит нагрузку на CPU в ущерб точности физики. Зачастую, пониженная точность - приемлемая жертва в обмен на увеличение производительности.
  • Установите Maximum Allowed Timestep в Time manager на значение в диапазоне 8–10 FPS, чтобы ограничить время, затрачиваемое на расчёт физики в самом худшем случае.
  • Меш коллайдеры требуют значительно больше ресурсов, чем примитивные коллайдеры, так что старайтесь избегать их использования. Зачастую можно усреднить форму меша используя дочерние объекты с примитивными коллайдерами. Дочерние коллайдеры будут использоваться в виде цельного слитного коллайдера твёрдым телом (компонентом rigidbody) родителя.
  • Хоть коллайдеры Wheel Colliders не совсем коллайдеры с точки зрения твёрдых объектов, тем не менее, они достаточно сильно нагружают процессор.
Оптимизации рендеринга
Legacy Topics