Для начала небольшое предисловие. Мы работаем над игрой Empires in Ruins с пререндеренными 3D-моделями, которые перед сохранением в Unity превращаются в спрайты и атласы спрайтов. Если объяснять коротко, то при этом выполняется довольно долгий и медленный производственный процесс, но он позволяет нам использовать текстуры очень высокого разрешения для очень чёткой графики. Такой стиль напоминает стратегические игры 90-х наподобие Age of Empires (и многих других) в смеси с производственным процессом Baldur's gate, дополненным современным стилем и возможностью сильного масштабирования. Нам вообще нравится производить впечатление.



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

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



Если кто-то мне ещё не верит, мы перечислим причины. Предлагаю изучить их, если вы думаете о создании олдскульной изометрической стратегии в Unity3D.



Я уверен, что вы хорошо подготовились, и знаете все предлагаемые (и обычно очень полезные) техники оптимизации при работе с Unity. Настало время применить их в новой игре, и раз за разом терпеть неудачу.

  • Если вам нужна глубина в сцене, то забудьте о батчинге. Даже два объекта с одинаковым спрайтом и материалом, находящиеся на разных глубинах (или по оси Z, или в слоях сортировки) с несколькими объектами между ними (а такое случается часто) будут требовать по одному вызову отрисовки на каждый. И пределов здесь нет. P.S. По этой причине забудьте о выпуске игры на мобильных платформах.
  • Если вы предпочли 2D вместо 3D, потому что испугались количества треугольников, то не думайте, что при использовании спрайтов с альфа-каналом треугольников будет сильно меньше. Спрайты с альфа-каналом, то есть с неровным контуром, тоже могут использовать множество треугольников и вершин.
  • Всегда используйте только атласы спрайтов PoT (Power of Two). Особенно если вам нужно большое разрешение и высокий уровень масштабирования, только это спасёт игру от переполнения диска (не забывайте упаковывать текстуры в Unity перед финальной сборкой) и видеопамяти. Не то чтобы PoT-атласы можно сохранять на дискету, если у объектов есть много кадров анимации в разных направлениях, но у вас будет шанс занять меньше места, чем Mortal Kombat X.
  • Приготовьтесь к проблемам с математическими расчётами. Допустим, вам нужно симулировать перспективные траектории, похожие на 3D, на плоской поверхности со смотрящей на неё ортогональной камерой. Это возможно, но не так просто, как может показаться сначала.
  • Ещё одна огромная проблема возникнет, когда анимации чуть более сложнее базовых. Если не разбить их на несколько суб-анимаций (с неизбежным созданием громоздких аниматоров для управления ими), то что произойдёт, например, при атаке солдата? Итак, он готовится к удару, и кадр X должен нанести урон, воспроизвести звук удара и т.д. Ну ладно, а как же сообщить игре, когда это происходит? Единственный разумный способ — добавить к анимации событие, и всё пока выглядит нормально. Но одна большая проблема сначала не так очевидна. Естественно, вы хотите, чтобы игровая логика выполнялась в FixedUpdate (потому что в таком случае она будет надёжной и детерминированной), но анимации выполняются в простом Update. Уже замечаете грядущую проблему?
  • Всегда включайте MIP-текстурирование. Даже не спрашивайте себя, делайте это на автомате. Включайте MIP-текстурирование, если в игре не будет одного уровня масштабирования. Не забывайте, что при включении MIP-текстурирования текстуры растут в размерах (представим, что у нас есть текстура 1024x1024, при генерировании MIP-текстур Unity создаст версии 512x512, 256x256 и т.д.). Я не пытаюсь научить вас MIP-текстурированию, но вам просто нужно знать, что оно будет нужно.
  • Создавайте пулы объектов. Если вы стараетесь снизить нагрузку на процессор, куча вызовов Instantiate и Destroy НЕ будет вашими лучшими друзьями!
  • Вам ведь нравится физика Unity? Жаль, что гравитация не согласована и её невозможно согласовать с симулированной вертикальной осью вашего игрового мира. Ну что же, жизнь несправедлива.



И в конце подведём итог:

  • Почти невозможно избежать большого количества вызовов отрисовки. Вызовы отрисовки могут создавать «бутылочные горлышки» в процессоре и снижать FPS. Решение заключается в том, чтобы сделать всё, относящееся к игровой логике как можно более «умным» и лёгким. Используйте корутины, подумайте, может быть некоторые операции можно выполнять не каждый кадр, а, например, раз в x кадров. По возможности оптимизируйте игру для батчинга, но учтите, что многого сделать не удастся.
  • Большие текстуры и большие атласы спрайтов сильно загружают видеопамять. Не забывайте это и всегда стремитесь к наилучшей работе игры на целевых платформах. Конечно, в наши дни компьютеры бесконечно мощнее, чем они были всего несколько лет назад, но если вы не делаете игру только для самых мощных ПК (а это скорее всего не так), то будет безумием занимать больше 1 ГБ видеопамяти.
  • Наберите в команду очень хороших аниматоров/3d-художников — анимация спрайтов не прощает ошибок и её невозможно контролировать, когда она попадает в Unity.
  • Внимательно изучите 2d-ассеты в Asset store. Там есть множество ассетов, которые за пару долларов сэкономят вам многие часы мучительной работы. Поэтому есть смысл их покупать.
  • Если хотите, чтобы кроме разработки у вас осталось время и на личную жизнь, то лучше займитесь игрой в другом стиле.
  • Профайлер Unity — ваш самый лучший друг. Потратьте на него столько времени, сколько нужно, и это воздастся сторицей.

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



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

Комментарии (13)


  1. bfDeveloper
    20.03.2017 13:17

    Если вам нужна глубина в сцене, то забудьте о батчинге.

    Вот за это я и не люблю Unity (и все подобные комбаины «для простого старта»). Нет никакой проблемы батчить изометрию. Знаю проект, где вообще за 2 draw call'а сцена на 10 000 изометрических спрайтов с анимацией рисуется. Там безумное перекрытие объектов друг другом (over draw > 70). Но это всё оптимизируется и выпиливается, в итоге нет никакого over draw, нет тысяч вызовов. Одна проблема — этот подход требует скилов и очень гибкого движка. В случае с юнити единственный выход — уйти в C++, получить контекст и нарисовать всё самому. Но если так рисовать всю игру, то зачем вообще фреймвёрк?


    1. KumoKairo
      20.03.2017 13:44
      +2

      На самом деле любым инструментом нужно уметь пользоваться. Как и С++/OpenGL, так и любой готовый движок требует определённого опыта. Хорошим контрпримером с «забудьте о батчинге» является игра Mushroom 11, где в огромной сцене с глубиной отрисовки, большим количеством активных объектов количество DC не превышает 10-11. Доклад можно посмотреть тут https://youtu.be/bYqI77dteYk?t=2734 (залинкованное время — скриншот статистики отрисовки).
      Это не значит что любой только начавший что-то делать в движке сможет без всяких проблем достичь такого же результата — нужен опыт, контекст, и базовые знания по предметной области.


    1. Aquahawk
      20.03.2017 14:15
      +5

      Вот демка того самого проекта о котором говорит bfDeveloper Я пишу вообще под webGL.
      Да приходится атласы в рантайме собирать из того что есть на сцене. Тут по полной используется z сортировка для экономии на overdraw т.к. всё рисуется с альфа блендингом. Два дроколла потому что вся сцена сначала рисуется спереди на зад с альфаотсечением, и при этом заполняется depth буфер, а потом уже с блендингом сзади на перёд.
      Мобилам всё равно недоступен такой уровень количества ассетов на сцене, т.к. у них early z culling не работает. Основная проблема это запись в экранный буфер, большое overdraw не держат ни мобилки ни даже встроенные intel видео карты.
      Краткая сводка:
      252 типа ассетов на сцене, всего более 7000 типов может быть на участке игрока
      9000 инстансов этих ассетов на сцене, рандомно сортированы, худший вариант для батчера
      альфа блендинг на каждом ассете
      каждый ассет играет рандомную анимацию из тех что у него есть, в своей, рандомной позиции анимации.
      2 draw calls per frame (deferred alpha blending).
      Полностью gpu анимация. Никакого аплоада буферов на рендере вообще. Только текущее время в юниформ и два дроколла, с установкой ещё одного юниформа для того чтоб сообщить шейдеру альфу отрезать или блендить.


      1. impwx
        23.03.2017 18:11

        Очень круто! А почему в центре игрового поля моргает дерево?


        1. Aquahawk
          24.03.2017 12:27
          +1

          У дерева в кадрах анимации лежит анимация его срубания, оно при каждом ударе берёт следующий кадр. Т.е. в игре анимация деревьев всегда на паузе и тикает при ударах топором. Тут в демке она просто играется, потому кажется что моргает. Это уже к бизнес логике а не к движку относится, а до бизнес логики я ещё не дошёл в портировании. И да, фрутовые и декоративные деревья не рубят, потому они не мограют. Срубание подразумевают только дикие деревья, там из два видно. Если присмотреться даже видно как оно срубается.


  1. Aquahawk
    20.03.2017 14:32
    +1

    Ещё раз перечитал статью. Изучайте не фреймвёрк а технологии. И профайлер советую не только юнитёвый, но и NVIDIA Nsight и Intel Graphics Performance Analyzer.


  1. Shchvova
    20.03.2017 21:58

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


  1. perfectdaemon
    21.03.2017 05:12

    Если вы предпочли 2D вместо 3D, потому что испугались количества треугольников, то не думайте, что при использовании спрайтов с альфа-каналом треугольников будет сильно меньше. Спрайты с альфа-каналом, то есть с неровным контуром, тоже могут использовать множество треугольников и вершин.

    Потому что при сложной альфа-маске спрайта гораздо производительнее будет триангулировать его видимую часть, оставив альфатесту только самые сложные места. Отрисовать сотню другую лишних треугольников и отсечь невидимое/видимое по z-тесту намного лучше, чем альфа-тест, ломающий параллелизм шейдеров.

    Всегда используйте только атласы спрайтов PoT (Power of Two)

    Unity здесь не причем. Иногда полезно узнать азы программирования компьютерной графики до знакомства с высокоуровневыми движками.

    Естественно, вы хотите, чтобы игровая логика выполнялась в FixedUpdate (потому что в таком случае она будет надёжной и детерминированной), но анимации выполняются в простом Update. Уже замечаете грядущую проблему?

    Если это такая проблема, она решается как минимум двумя способами: Update + Time.deltaTime. Либо переносом логики из события анимации в FixedUpdate, в этом случае в событии просто выставляем нужное состояние, а обрабатываем его в FixedUpdate.

    Создавайте пулы объектов. Если вы стараетесь снизить нагрузку на процессор, куча вызовов Instantiate и Destroy НЕ будет вашими лучшими друзьями!

    И опять же Unity не при чем. Даже в случае с ЯП без сборщика мусора new/delete слишком затратно в игровом цикле. В NASA, например, программа должна аллоцировать всю требуемую память на старте и не требовать аллокации во время работы.

    Вам ведь нравится физика Unity? Жаль, что гравитация не согласована и её невозможно согласовать с симулированной вертикальной осью вашего игрового мира

    Рисовать изометрию с пререндеренными спрайтами и хотеть 3d-физику? Месье знает толк…


  1. Delphin92
    21.03.2017 14:56
    -1

    Итак, он готовится к удару, и кадр X должен нанести урон, воспроизвести звук удара и т.д.<...> Естественно, вы хотите, чтобы игровая логика выполнялась в FixedUpdate (потому что в таком случае она будет надёжной и детерминированной), но анимации выполняются в простом Update.

    Была схожая задача по синхронизации вращения многоствольной пушки и задержки выстрела — чтобы выстрел производился только когда ствол находится вверху. Я просто на каждый выстрел делал Invoke следующего, с задержкой, соответствующей времени поворота, а вращал вообще в отдельном скрипте. Если не нравится Invoke, можно создавать корунтину с WaitForSeconds.

    Есть какие-то недостатки такого подхода?


    1. perfectdaemon
      21.03.2017 15:02
      +1

      Invoke медленный, так как использует рефлексию, имя метода задается строкой, что чревато проблемами при рефакторинге. А так — вполне себе нормальное решение. Корутины чуть получше будут, хотя не уверен, что и это достаточно производительное решение. Впрочем, я не настолько хорош в Unity, поэтому нужны более компетентные люди


      1. Suvitruf
        23.03.2017 23:31

        А так — вполне себе нормальное решение.
        Если вы не хотите билдить под WebGL, конечно.


        1. Delphin92
          24.03.2017 00:10

          Если не трудно, не могли бы рассказать, прочему возникнут проблемы под WebGL?


          1. Suvitruf
            24.03.2017 00:15

            Getting started with WebGL development.
            WebGL не умеет в рефлексию и System.Threading.