Перші кроки в оптимізації і поліровці гри на Unity3d

imageПісля того як я закінчив мій перший проект, прийшла думка про портуванні його на мобільні пристрої або хоча б запуску на вбудованому GPU. У всіх гайдах по оптимізації, в одному з перших рад вам повідомлять, що не варто переживати про продуктивності заздалегідь, починайте оптимізувати, після того як всі закінчите, і ви поступово наведе порядок. Так і я, випустивши спочатку гру на десктоп, вирішив, що ніколи не буде пізно оптимізувати її під мобільні пристрої. На жаль, мені не вдалося повною мірою досягти поставленої мети, бо, схоже, що мобільні ігри слід з самого початку розробляти з прицілом на слабке залізо. На даний момент, для подальшої оптимізації під мобільні платформи я бачу тільки необхідність серйозно переробити геймплей і дизайн ігрового світу. Однак і в поточному варіанті здобуто цінний досвід оптимізації під Unity3d і результуючий приріст продуктивності більш ніж у 300% на інтегрованому GPU.

Давайте почнемо з CPU
Достатньо очевидний список сформувався під час оптимізації:

  1. Не використовуйте Властивості! Поля методи — ваші найкращі друзі.

  2. Кешируйте всі, що ви отримуєте через GetComponent<>, сюди ж входять transforms, rigidbodies та ін

  3. Намагайтеся ніколи не звертатися до об'єктів двічі для отримання одних і тих же даних. Майже завжди це доступ через Властивості, від яких слід відмовлятися. Часто можна побачити як у різних скриптах запитується позиція одного і того ж об'єкта, що краще замінити на кешування тієї самої позиції, оновлюючи її один раз всередині Update або навіть FixedUpdate в цьому об'єкті.

  4. Кешируйте всю математику. Кожен виклик Vector.Up буде під капотом викликати конструктор, що не дуже швидко. Я створив статичний CachedMath клас, в який були складені всі напрямки, які часто використовуються вектори і кватерніони.

  5. Спробуйте обходитися без використання типу String. Кожна рядок вимагає виділення пам'яті, і при безконтрольному використанні рядків, ви побачите, як GC зупинить всі потоки для свого виклику. В моєму випадку основними джерелами рядків були індикатор FPS і таймер під час гонки. Рішенням стало створити пул строкових літералів для всіх цифр від 1 до 100. Це повністю виключило виділення рядків у кожному кадрі.

  6. Ніколи не використовуйте foreach, просто замініть на for, якщо хочете зберегти GC і дорогоцінний час CPU. До тих же наслідків часто призводить і використання шаблонних методів(узагальнення).

  7. LINQ є ще одним джерелом навантаження на GC. Намагайтеся спрощувати ваші LINQ виразу, або ще краще, повністю замінювати їх на прості конструкції.

  8. Всі рядки використовуються в Animator-об'єктах слід сконвертувати в цілочисельні ідентифікатори черезAnimator.StringToHash()

  9. Инстанциирование об'єктів є дуже важкою операцій, тому варто для частих створінь використовувати пул об'єктів і потім їх переиспользовать.

  10. Видаляйте всі порожні методи 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.

Вода. В моєму випадку, мені необхідно було отримати більш продуктивну воду. Спочатку в сцені використовувалася waterProDaytime з включеним заломленням, яка зазнала мінімальних змін для підтримки піни уздовж берегової лінії. Була прибрана камера заломлень і замінена на виклик grabpass. Вся справа в тому, що для коректного відображення заломлення, камері довелося відключати матрицю відсікання, т. к. в іншому випадку — всі об'єкти, вище рівня води, просто не відкидали тіней. З-за цього обмеження, камера додатково отрисовывала сцену цілком, і виклик grabpass опинився в цьому випадку швидше. Також був змінений параметр LOD множника на час відтворення відбиттів. Таким чином импостеры трохи довше відображаються у воді, що додатково знижує навантаження.

Всі зміни підвищили продуктивність на інтегрованому GPU 6-8 22-24 кадрів в секунду. Як і раніше низький показник, але кращого досягти поки не вдалося. Все ще рекомендую запуск своєї гри на дискретній графіці.

Полірування
Новий реліз не хотілося випускати тільки з зміною продуктивності, тому було вирішено закрити кілька досить важливих моментів в тому, як гра виглядає, а саме — UI.

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

У ньому були відсутні звуки, не було руху, життя. Запустивши один з проектів, я почав помічати деталі головного меню, які раніше для мене були просто невидимі. Так що я додав всі відсутню в першому наближенні. Тепер обрана кнопка анімована, переклад фокусу і натискання кнопок озвучені з безкоштовних ресурсів в Asset-Store.

Всі елементи в самій грі анімувати було б занадто відволікаючим і дратівливим гравця ефектом, тому я додав внутрішнє переливчасте світіння, яке дуже м'яко нагадує про UI елементах.

І останнім став екран медалей. Він був просто статичний, в ньому було жахливо нудно. А адже це має бути місцем, в якому гравець з насолодою спостерігає за своїм прогресом. Так що я додав трохи життя. Тепер його прикрашають три системи частинок, що створюють м'які переливаються кулі світла схожі на світляків. Частинки, на жаль за замовчуванням не доступні на UI, тому довелося створювати додаткову камеру, яка малює тільки ці частинки в текстуру, потім накладывающуюся на UI.

Що було залишено позаду
Мені не вдалося запустити гру на мобільних пристроях. Навіть саме потужне залізо не відтворює гру з прийнятною продуктивністю. Можливо, з майбутніми версіями Unity3d щось зміниться, але на даний момент, як я вже згадував, розробка гри з прицілом на мобільні пристрої повинна проводитися інакше з самого початку.

Також, в моєму випадку виявлялися дуже дивні артефакти відтворення на мобільних пристроях з світяться в деяких випадках, буквально як лазери матеріалами, які виглядають цілком нормально на десктопі.

Інша мета, яка з'явилася під час оптимізації у вигляді швидкого запуску програми, або навіть плавного запуску, також не була досягнута. Холодний старт в моєму випадку триває більше хвилини. Причому кожен наступний запуск скорочує цей час майже вдвічі. Так що, схоже, це якесь внутрішнє необхідна вимога Unity-плеєра. Найголовнішим недоліком є зависання UI потоку при активації сцени. Я вже використовую асинхронний варіант завантаження сцени, навіть переключив його в Additive режим, однак UI просто зупиняється після 90% завантаження, коли необхідно переключити прапор allowSceneActivation. Було б здорово, якщо хтось підкаже обхідний шлях для Unity5.x, або щось на зразок виклику події яке може потокобезопасно і з головним пріоритетом змінювати Ui-об'єкти з їх перемальовуванням, щоб була хоч якась індикація процесу додатки.

p.s.
Звичайно, це тільки моя історія і вона не вирішить магічним чином всі ваші проблеми. Місцями вона надто суб'єктивна, але все ж сподіваюся, що хтось знайде ці поради корисні.

P. P. SЯ описав головне, не вдаючись глибоко в технічні деталі. Проект зазнав дуже великій кількості змін, і не все було описано — зараз можу точно пригадати, що з минулого публікації просили розібратися з водою, яка просочується крізь човен — і це точно було закрито. Також були додані звуки під час проходження чекпоінтів та отримання медалей, як і невелика анімація таймера на чекпоинтах і колірне інформування про час.
Якщо комусь цікаво подивитися всі в дії, на жаль, відео ще немає, однак тріал тепер нескінченний.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.