В последний месяц только и занимаюсь тем, что провожу предельную оптимизацию и глобальный рефакторинг. Поэтому начинаю описывать типичные техники, которые использую каждый день, в стиле маленьких советов. Все нижесказанное опробовано на собственном опыте и могут быть в корне неверны, поэтому если кто хочет обсудить что-либо, оспорить - you're welcome! А кто после прочтения поста не понял, почему его FPS шутер превратился в пошаговую стратегию - тот артист :)
Итак, у нас есть готовая рабочая версия игры, но она безбожно тормозит. Какие телодвижения в каком порядке произвести, чтобы выиграть процессорное и графическое время:
- В ПЕРВУЮ ОЧЕРЕДЬ юзаем профайлер. Даже если знаешь, где можно сделать быстрее, к примеру, уменьшить асимптотику у какой-нибудь структуры, все равно смотреть профайлер. Часто думаешь, что тормозит по одной причине, а на самом деле вылезает совершенно левый оверхед.
- Лидеры тормозов:
- отрисовка - 70%
- расчет физики - 20%
- скрипты - 10%.
- Первый признак того, что все плохо: много draw-call-ов(далее dc). Немного теории: рисованием чего-либо занимается компонент Renderer(и различные его вариации: MeshRenderer, SkinnedMeshRenderer, ParticleRenderer, LineRenderer, etc), который принимает на вход материал с заданными параметрами(шейдер, текстура и т.д.) и отрисовывает. Видеокарта для каждого материала делает отдельный проход рисования, что и является dc. Ну так вот, чем больше материалов на сцене, тем больше страдает видеокарточка девайса-> тем больше страдает пользователь -> тем больше единичек в app store.
- А еще есть такая вещь, как batching, смысл его заключается в том, что несколько объектов, использующих один и тот же материал отрисуются за один проход. То есть, к примеру, выгодней делать двадцать однотипных домов, как в Набережных Челнах, чем 4 дома как в центре Санкт-Петербурга, потому что двадцать однотипных домов отрисуется за один проход. Поэтому везде, где возможно, используем один и тот же материал.
- У батчинга есть проблемы:
- если у двух объектов разный scale - батчинг не работает. Возможные пути решения:
- менять размер mesh-a, а не объекта, если это meshrenderer(к примеру, так делается в NGUI ).
- строго следить за однотипными объектами и бить по рукам любого, кто не любит стандартные числа.
- если объекты принимают тень в реалтайме(читай: не лайтмапятся) - батчинг не работает.
- если меш очень сложный(больше 900 вершин для тупо вершин, 300 вершин для uv, 180 вершин для uv1, uv2), то батчинг не работает.
- если объекты используют один и тот же материал, но у одного материала какое-нибудь свойство отличается - unity создает новый инстанс материала - тут разные материалы - не может быть и речи о батчинге
- если шейдер использует более одного прохода - батчинг не работает.
- На тему материалов: используем мобильные шейдера вместо обычных. Обычные шейдера используем только тогда, когда реально приспичит. Сегодня как раз столкнулись с веселой ситуацией: есть 50 совершенно одинаковых объектов(различаются только позицией и поворотом), они все помечены батчиться статически, при этом в игре с ними получается +50 dc. Начали искать, где проблема. Случайно решили вместо diffuse шейдера поставить mobile/diffuse - объекты забатчились. Вообще, юнитеки божатся, что мобильные шейдеры в десятки-сотни, а в некоторых случаях и тысячи раз быстрее десктопных.
- Потолок dc(буду судить только по ios, в android-е там совсем зоопарк):
- iphone 3gs, ipod 4: 70 dc
- iphone 4: 90 dc
- iphone 4s: 200 dc
- Оптимизировать количество материалов можно благодаря использованию текстурных атласов. Смысл очень прост: много картинок засовываем в одну большую, рендерерам говорим использовать определенные участки карты и видеокарта может отрисовывать это все в один проход. Я просто оставлю это здесь: http://www.digitalopus.ca/site/mesh-baker/, http://www.codeandweb.com/texturepacker.
- С атласами не все может быть так круто: четыре атласа 2048x2048, но загружаемые и разгружаемые в памяти лучше, чем один атлас 4096x4096. Пока хотя бы один материал использует текстуру, видеокарта держит текстуру в памяти.
- Теперь придем к отрисовке текстур: используем сжатие на 146%. PVRTC для ios, ETC/DXT для android, mip-map-ы обязательно и тогда есть вероятность, что видеокарточка не повесится, а скажет вам спасибо.
- 2D - это отдельная тема.
- Instantiate/Destroy объектов дорого не только для CPU, видеокарта тоже обязана загружать/освобождать ресурсы. Не заставляйте ее делать слишком часто, используйте паттерн object pool.
- Ну и самая противная кнопка, подставленная на твой стул - это отрисовка где-нибудь в Update/FixedUpdate. На примере: вот так чуваки обновляют визуальный компонент подсчета очков:
Для далеких из мира Unity3d: Update вызывается каждый кадр! Вместо того, чтобы просто обновить один раз вьюшку, когда модель обновится, мы устраиваем маленький ddos мобильника. И вообще, нужно минимизировать количество Update/FixedUpdate и переходить к event-логике, а еще лучше к конечным автоматам.private void Update(){ pointsMesh.text = Model.points.ToString(); }
- Да, совсем забыл про свет: везде, где возможно, заранее запекаем свет, предпосчитываем тени. Если тени не принципиальны(например, 2D графика), то убираем по возможности везде, где только возможно. Производительность увеличивается прямо на глазах!
Про графику вроде все. Вспомню, напишу еще. На следующей неделе напишу некоторые советы по оптимизации скриптовой части.
Вот что можно почитать у юнитеков на эту тему: