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

При разработке игры ,можно столкнуться с проблемой оптимизации, когда даже при относительно не большом количестве полигонов и без использования каких либо эффектов, игра притормаживает даже на топовых устройствах. Это может быть из-за того, что в сцене много мелких объектов и они в разы увеличивают Draw Calls, с этой проблемой можно справиться путем объединения всех объектов с одним материалом в один Mesh, через встроенную в Unity функцию, которая находится по следующему пути: Component/Mesh/Combine Children, подробнее о ней будет чуть ниже.

В общем ниже будут перечислены основные приемы оптимизации игр в Unity.

Несколько советов по снижению Draw Calls (Draw Calls - количество обращений CPU к GPU)

1. Использовать как можно меньше текстур и материалов. Желательно, чтобы как можно больше объектов использовало один и тот же материал. Один и тот же материал может быть использован на объектах двумя способами: через renderer.material и renderer.sharedMaterial. В первом случае получается независимая копия материала, свойства которой можно менять без изменения тех же свойств на остальных объектах. Батчится же будут только меши, на которые наложены материалы через sharedMaterial. Материал, который вешается через редактор, по умолчанию вешается именно на sharedMaterial.
Но тут есть одно но - в каждом меше должно быть <= 300 вертексов (именно вертексов, а не треугольников), иначе он не будет участвовать в батче. Циферка эта заложена разработчиками и подобрана опытным путем. Так же сами разработчики не гарантируют, что она не поменяется, но пока такого не было.
Резюмируем. Хотим батчи - юзаем sharedMaterial на мешах с количеством вертексов <= 300, иначе будут дополнительные дк.

2. Объединять статические объекты, использующие один и тот же материал, в один общий Mesh. Для этого все объекты необходимо запихнуть в один родительский объект-контейнер и применить к нему скрипт из стандартного набора компонентов Unity, который находится в меню Component -> Mesh -> Combine Children.

Правильное использование музыки в игре:

1. Если фоновая музыка одна, то вполне уместно не упаковывать трек в памяти и читать его после старта сцены.
Фоновую музыку исключительно стримить с ресурсов и исключительно в MP3 (мало занимает места, хорошо стримится). Короткие звуки можно перегнать в ogg для уменьшения времени распаковки и распаковывать в памяти, либо стримить с ресурсов - по обстоятельствам. Так же необходимо понимать, что не все источники будут слышны одновременно на мобилках так, как они слышны в редакторе. На части устройств (смарты) при количестве AudioSource > 4 появлялся визг с хрипом, а так же ощутимые лаги, на части прекрасно работало (планшеты). Для себя принял решение держаться в рамках максимум 4-ех проигрываемых звуков. WAV - не использовать никогда.

Игровой мир:

1. Использовать только 1 источник света для освещения, затемнения отдельных частей карты лучше использовать Lightmap`ы
2. Старайтесь как можно реже, или вообще не использовать объекты с Rigidbody
3. Fog - дает сильную нагрузку
4. Шейдеры для материалов, стараться использовать из категории Mobile/... - более оптимизированы

Терреин:

1. Использовать небольшие размеры террейна ~150x150
2. Pixel Error - выставить с 0 до максимального значения ( хоть это и предаст прыгающие вершины при приближении камеры, зато в разы увеличит производительность)
3. Отключать флажок Draw
4. Не использовать отрисовку травы в инструментах Terrain`a - в разы уменьшает производительность

Скрипты:

1. Все расчеты производить исключительно в Update. В FixedUpdate можно делать небольшие проверки, специфичные для физики, но лучше вообще избегать использования FixedUpdate. Почему? Update вызывается FPS-равное количество раз и увеличение нагрузки просто будет просаживать скорость рендеринга рывками. Это не страшно, т.к. все апдейты должны вестись с учетом Time.deltaTime. FixedUpdate выполняется равное количество раз в секунду и движок ждет завершения выполнения. В результате игру будет визуально дергать при неравномерной вычислительной нагрузке в FixedUpdate.
2. Не использовать new (типа new Vectro3(x,y,z), new Rect()) в циклах и часто вызываемых местах
3. CompareTag более чем вдвое быстрее прямой проверки тэга по имени.
4. (thisTransform.position - target.position).sqrMagnitude быстрее Vector3.Distance
5. Делайте как можно меньше вызовов thisTransform.forward или position, лучше эти значения один раз получить в Update, и использовать во всем скрипте.. Это касается любых готовых свойств MonoBehaviour, включая сам transform. Ощущение, что они всегда получаются выбором из списка компонентов, без кеширования - его нужно делать руками. Все вектора в глобальном пространстве (forward, right, up и т.п.) всегда перевычисляются при каждом обращении к ним по иерархии GameObject - надо прочитать один раз и использовать в текущем коде.
6. Для включение скрипта на объекте при возможности использовать не GetComponent(...).enabled с заранее отключенным скриптом, а AddComponent(...)
при заранее не повешенном скрипте на объекте

7. Использование Статических переменных

При использовании JavaScript наиболее важной оптимизацией является использование статических типов вместо динамических. Unity использует метод, который называется type inference, для автоматической конвертации переменных JavaScript к статическому коду, для этого вам ничего не понадобиться делать.

var foo = 5; 

В приведенном выше примере foo автоматически распознается как переменная типа integer. Таким образом Unity может применить множество оптимизаций времени компиляции, без затратных динамических поисков имени переменной и тд. Это одна из причин, почему Unity JavaScript в среднем в 20 раз быстрее чем какие либо другие реализации JavaScript.

Единственная проблема – не все типы возможно распознать, в таких случаях Unity вернётся обратно к динамическому типу для этих переменных. Использование динамических типов на JavaScript было бы проще для написания кода, тем не менее это бы замедлило его выполнение кода.

Рассмотрим несколько примеров:

function Start ()
{  
   var foo = GetComponent(MyScript);  
   foo.DoSomething();  
}

В данном варианте foo будет динамически определено, таким образом вызов функции DoSomething длится дольше чем нужно, потому что тип foo неизвестен, и вначале нужно выяснить, поддерживает ли foo функцию DoSomething, и если поддерживает то вызвать её.

function Start ()
{  
	var foo : MyScript = GetComponent(MyScript);  
	foo.DoSomething();  
}

Здесь foo у нас имеет определённый тип. Производительность в данном случае будет гораздо лучше.

8. Используйте #pragma strict

Конечно, сейчас проблема состоит в том, что вы обычно не замечаете использование динамической вёрстки. #pragma strict сможет вам помочь. Просто добавьте в начало кода #pragma strict и Unity отключит динамическую вёрстку в данном скрипте, вынуждая вас использовать статическую. Там где тип будет неизвестен, Unity сообщит нам про ошибки компиляции. В этом случае foo будет выдавать ошибку на этапе компиляции:

#pragma strict 
 
function Start ()
{  
	var foo = GetComponent(MyScript);  
	foo.DoSomething();  
} 

9. Кешированый поиск компонентов

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

Всякий раз когда вы получаете доступ к компоненту через GetComponent или средством доступа переменной, Unity должен найти нужный компонент из игрового объекта. Время на поиск можно легко сократить если использовать кеширование ссылки на компонент в часной (private) переменной.

Просто преобразуйте этот код:

function Update ()
{  
	transform.Translate(0, 0, 5);  
} 

В этот:

private var myTransform : Transform; 
 
function Awake ()
{  
	myTransform = transform;  
}  
  
function Update ()
{  
	myTransform.Translate(0, 0, 5);  
} 

Последний вариант будет работать намного быстрее, так как Unity не нужно искать компонент Transform в игровом объекте каждый кадр.
Тоже самое применимо для скриптовых компонентов, где вы используете GetComponent вместо transform или других изменений свойств.

10. Используйте встроенные массивы

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

private var positions : Vector3[];  

function Awake ()
{  
	positions = new Vector3[100];  
	for (var i=0;i<100;i++)  
	positions[i] = Vector3.zero;  
} 

11. Не делайте вызов функции, если без этого можно обойтись

Самый простейший и лучший способ оптимизации – это совершать как можно меньше лишней работы. К примеру, когда враг далеко от нас, наилучшим будет сделать так, чтобы он «заснул». То есть не совершал никаких действий пока игрок не подойдёт ближе. Вот медленный вариант реализации этого случая:

function Update ()  
{  
	// Early out if the player is too far away.  
	if (Vector3.Distance(transform.position, target.position) > 100)  
	return;  
	perform real work work...  
}  

Это не самая удачная идея, так как Unity должен вызывать update функцию постоянно, а значит мы выполняем лишнюю работу каждый кадр. Наилучшим решением в данном случае будет отключение программы врага, пока игрок не подойдёт поближе. Существует 3 способа реализации этой идеи:

1) Использовать OnBecameVisible и OnBecameInvisible. Эти вызовы заложены в системе прорисовки. Как только какая-нибудь камера видит объект, вызывается OnBecameVisible, когда ни одна камера не видит его, делается вызов OnBecameInvisible. В некоторых случаях это оправдано, но иногда это проблематично для AI, так как враги становятся неактивными, когда вы отклоняете от них камеру.

function OnBecameVisible () 
{  
    enabled = true;  
}  
  
function OnBecameInvisible ()  
{  
    enabled = false;  
}  

2) Используйте триггеры. Простой шарообразный триггер области может творить чудеса. При выходе из выбранной нами области влияния мы получаем вызовы OnTriggerEnter/Exit.

function OnTriggerEnter (c : Collider)  
{  
    if (c.CompareTag("Player"))
    {  
        enabled = true;  
    }
}  
  
function OnTriggerExit (c : Collider)  
{  
    if (c.CompareTag("Player"))  
    {
        enabled = false;  
    }
}  

3) Используйте Coroutines. Главным недостатком Update является то, что он исполняется каждый кадр. Вполне возможным была бы проверка расстояния до игрока каждые 5 секунд. Это бы неплохо повысило производительность.