- Не используйте Свойства! Поля и методы — ваши лучшие друзья.
- Кешируйте все, что вы получаете через GetComponent<>, сюда же входят transforms, rigidbodies и др.
- Старайтесь никогда не обращаться к объектам дважды для получения одних и тех же данных. Почти всегда это доступ через Свойства, от которых необходимо отказываться. Зачастую можно увидеть как в разных скриптах запрашивается позиция одного и того же объекта, что лучше заменить на кеширование той самой позиции, обновляя ее один раз внутри Update или даже FixedUpdate в этом объекте.
- Кешируйте всю математику. Каждый вызов Vector.Up будет под капотом вызывать конструктор, что не очень быстро. Я создал статический CachedMath класс, в который были сложены все направления, часто используемые векторы и кватернионы.
- Попробуйте обходиться без использования типа String. Каждая строка требует выделения памяти, и при бесконтрольном использовании строк, вы увидите, как GC остановит все потоки для своего вызова. В моем случае основными источниками строк были индикатор FPS и таймер во время гонки. Решением стало создать пул строковых литералов для всех цифр от 1 до 100. Это полностью исключило выделение строк в каждом кадре.
- Никогда не используйте foreach, просто замените на for, если хотите сберечь GC и драгоценное время CPU. К тем же последствиям зачастую приводит и использование шаблонных методов(generics).
- LINQ является еще одним источником нагрузки на GC. Старайтесь упрощать ваши LINQ выражения, или еще лучше, полностью заменять их на простые конструкции.
- Все строки используемые в Animator-объектах следует сконвертировать в целочисленные идентификаторы через Animator.StringToHash()
- Инстанциирование объектов является очень тяжелой операций, поэтому стоит для частых созданий использовать пул объектов и затем их переиспользовать.
- Удаляйте все пустые методы Update и FixedUpdate. Также, если ваш скрипт использует оба или только фиксированный, то стоит подумать о переносе любой возможной логики из фиксированного в обычный Update.
Конечно, любые оптимизации стоит проводить только, если вы видите задержки в окне профилировщика. Главное, уничтожайте на корню постоянные выделения памяти.
Также никогда не поздно упростить некоторую логику в ваших скриптах, или количество обрабатываемых данных в разумных пределах. Однако еще раз повторюсь, что вам нужны веские основания полученные с помощью профилировщика, что конкретный метод слишком медленен. После изменений обязательно проследите, чтобы профилировщик отображал меньшие цифры, чем до начала оптимизаций.
Самым плохим моментом оптимизаций является то, что ваш структурированный и "
идеальный" код растекается в местами не очень читабельное нечто. К сожалению, это неизбежно. Главное помнить о том, что это жертва в угоду производительности.
Теперь GPU
С
CPU советы были достаточно универсальны и они применимы в любом проекте. Чего не скажешь о
GPU-оптимизациях, которые зачастую сильно зависят от конкретной сцены. Однако, если вы не используете сильной магии в своих шейдерах, то явный индикатор — это количество
проходов GPU(pass-calls).
Моя игра содержит открытый мир с океаном как основой для передвижений и несколькими островами в качестве декораций. В моем случае проходов было больше
2000, и мне удалось снизить это значение до примерно
300.
Материалы. Уменьшайте количество используемых материалов насколько это возможно. Каждая смена материала это новый проход, также как и каждый текстурный слой внутри материала это тоже новый проход. Конечно, я несколько упрощаю и проходы формируются не так просто, но факт остается — слишком много проходов будут непомерно нагружать слабый
GPU. Для мобильных устройств рекомендуют что-то в районе
40-60 проходов. Более продвинутые устройства могут обрабатывать и в районе
сотни. Так что вам есть куда стремиться!
Видимые объекты. В моей сцене слишком много объектов, которые постоянно присутствуют на экране. Проблема лишь в том, что они и должны быть видимы! Конечно, издалека нам не нужна такая же детализация как и вблизи, поэтому очевидным решением было использовать
LOD-объекты.
Импостеры. Я предпочел заменить мои объекты с помощью импостеров (в целом это очень похоже на
биллборды, но это множество текстур полученных пререндером объекта со всех сторон). Во встроенном
Asset-Store от
Unity3d множество готовых платных решений для
LOD и импостеров. Однако я решил воспроизвести базовый алгоритм самостоятельно. Я создал скрипт-расширение редактора, который создавал копию необходимого объекта, менял его слой, затем создавал камеру которая была ограничена только этим специальным слоем, и производил отрисовку объекта в текстуры со всех сторон. Были добавлены основные параметры, как название результирующей папки с текстурами, разрешение получаемых текстур, расстояние до объекта, смещение по высоте, количество сторон и флаг для сохранения или отключения освещения во время создания импостера. После того как все действия завершены, скрипт удалял уже ненужную копию объекта.
Спрайты. Теперь почти все объекты заменяются спрайтами на определенном удалении от камеры. Но количество проходов было все еще огромным. Тогда я обнаружил, что спрайты это далеко не всегда легковесная форма для отображения. Каждый спрайт по умолчанию триангулирует картинку, создавая множество вершин. На каждые
900 или около того(по официальной документации) вершин, создается очередной проход (официально
группировка|пакетирование|batching — сохранение данных множества объектов в одну инструкцию для
GPU — вообще неприменим к
SpriteRenderer объектам). В то же время нельзя заменить все спрайты на полные квадратные регионы с прозрачностью, т. к. все прозрачные пиксели все еще требуют отрисовки, и
GPU их не пропускает. Также прозрачность ведет к проблемам во время отрисовки всех спрайтов из-за проверки на глубину отрисовки.
GPU все еще будет создавать дополнительный проход для одного или двух спрайтов, между отрисовкой множества уже сгруппированных только потому, что этого требует проверка по глубине. Единственное, что удалось сделать — это изменить тип спрайта на
Multiple, что меняет внутренний механизм триангуляции, который создает намного меньше вершин.
Упаковщик спрайтов(SpritePacker). Это последнее о чем вы должны помнить при работе со спрайтами. Чтобы явно указать для спрайта необходимость упаковки в карту атлас, нужно указать его
Tag. В момент отрисовки спрайтов из одного атласа
GPU не создает дополнительных проходов, даже если порядок отрисовки по глубине не оптимален для неупакованных спрайтов. Размер результирующего атласа также важен. По умолчанию он ограничен значением в
2048х2048. Это максимальный размер атласа, и он динамически подстраивается под оптимальный, в зависимости от заполнения. В моем случае этого было недостаточно для упаковки всех необходимых мне спрайтов на одной странице. Замена алгоритма упаковки на собственный, который основан на базовом, но с измененным значением размера на
4096x2048 значительно улучшило производительность.
Дальнейшее увеличение до
4096x4096 почти не отразилось на количестве проходов, но при этом даже несколько ухудшило производительность. Стоит помнить, что некоторые спрайты не могут быть размещены на одном атласе вместе — для этого они должны иметь одинаковые настройки компрессии, и часть других параметров, иначе они будут автоматически разделены по разным группам. Поэтому старайтесь группировать спрайты по атласам логически и визуально, чтобы в один момент на экране отображалось как можно меньше атласов, ведь каждое переключение между ними, включая неоптимальное расположение по глубине будут стоить вам проходов.
В моем случае я разделил атласы на
UI-спрайты, затем все объекты, которые расположены очень далеко — пришлось использовать несколько атласов, но они были разделены на диаметрально противоположные по расположению в мире группы, и одновременно на экране увидеть их достаточно сложно, и все оставшиеся промежуточные объекты.
После всех изменений, производительность улучшилась настолько, что отключение всех объектов импостеров практически не влияет на результирующий
FPS.