company_banner

Эволюция: графика и механика



    В январе этого года наш игровой департамент выпустил мобильную игру «Эволюция: Битва за Утопию». Игру хорошо приняли, людям нравится играть в нее. Я часто вижу людей с «Эволюцией» в метро. Даже начал желания загадывать, когда оказываюсь между игроками. И в этом посте, подготовленным по моему докладу на КРИ 2014, я хотел бы подробнее рассказать о процессе разработки и особенностях «Эволюции».

    Недавно у нас вышло большое контентное обновление «Черный легион». Оно, естественно, привлекло много новых игроков, но самое главное, мы вернули большую часть своих игроков, которые уже прошли основную сюжетную ветку. В этой статье я расскажу о том, как развивалась наша игровая механика в процессе разработки. Вкратце упомяну о сторонних инструментах, которые мы использовали. И побольше внимания уделю графическим «фишкам», расскажу о том, как на слабых девайсах получить настоящие динамические тени и динамическое освещение.

    Игровая механика


    В начале разработки, в первых концептах «Эволюция» выглядела совсем иначе, нежели сейчас. У нас было две камеры, split screen, и они отображали две армии, которые располагались довольно далеко друг от друга. Это расстояние между ними никак не чувствовалось. Например, одна армия бросает гранаты. Они долетают до центра экрана, пропадают на некоторое время и потом появляются на второй части экрана. Игрокам это было неудобно. Мы провели некоторые изыскания, нарисовали различные схемы, провели мозговой штурм, придумали новую механику и геймплей. Затем потихоньку стали все это реализовывать.



    В ранних версиях у нас не было даже авто-огня. Мы заставляли игрока постоянно нажимать на соответствующую кнопку, чтобы стрелять в выбранного противника. Плюс у нас был прицел, имитирующий разброс пуль при отдаче: вы стреляете, прицел раздвигается и начинает постепенно сходиться обратно. Пока он не свёлся, вы наносите чуть меньший урон. Все это было слишком сложно, даже для нашей хардкорной аудитории. В это время вышла игра «Dead Trigger 2». Это обычный First Person Shooter. Но, на наше удивление, в этой игре ввели авто-огонь. Мы посмотрели, попробовали внедрить подобную схему управления в Эволюцию и пришли к такой механике, которая сегодня и представлена в игре.



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

    Сторонние инструменты


    В нашей игре практически 80% всей игровой механики построено вокруг GUI. В «Джаггернауте» мы использовали систему GUI собственной разработки, она в каком-то состоянии до сих пор там работает. Но когда мы проектировали «Эволюцию», то посмотрели, что предлагается на рынке. Так мы нашли NGUI и сравнили его функционал с тем, что было у нас. После чего пришли к выводу, что либо нам придется несколько месяцев дописывать свою систему, либо мы можем взять готовое решение.

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



    Посмотрите на эти здания, в игре они анимированы. Это обычная флеш-анимация. Чтобы ее импортировать, мы использовали еще один инструмент, который называется uniSWF. Они неплохо взаимодействуют вместе с NGUI.

    Еще один большой фреймворк, который мы использовали в «Эволюции» — это PlayMaker. Когда мы начинали проектировать «Эволюцию», нам нужна была какая-то технология для анимационных, да и вообще, для игровых машин состояний (Finite state machines или конечные автоматы). Технология Mecanim в то время была в зачаточном состоянии, и не позволяла делать очень нужную для нас вещь, а именно – переключать наборы анимаций. Игрок может взять в бой два вида оружия — пистолет и что-то потяжелее: пулемет, дробовик и т.п. Mecanim в том состоянии своего развития этого не позволял делать. Поэтому мы взяли за основу технологию PlayMaker, который не только позволяет описать неплохую стейт-машину для анимации, но также позволяет добавить некоторую логику из скриптов.

    Система освещения


    Для «Эволюции» мы написали собственную систему освещения. Вообще, для чего писать что-то свое, если в движке уже все готово, просто бери и используй? Все дело в том, что системы, встроенные в Unity, довольно универсальны. За обслуживание этих систем приходится платить производительностью. Особенно если целевое устройство слабопроизводительное. По сути, нужно платить за универсальную систему освещения своим FPS. Поэтому мы отказались от этого подхода в пользу так называемых Dynamic Light Probe. Также мы полностью отказались от обычных динамических источников света, которые есть в Unity. Всю статическую геометрию мы освещаем с помощью Lightmap. Для персонажей мы используем специальные Light Probe, и собственный шейдер.

    Light Probe – это сферическая гармоника, содержащая информацию об освещении в указанной точке. Таких Light Probe у нас на сцене несколько десятков. Движок выбирает 4 ближайших к персонажу Light Probe и интерполирует их в одну. Затем мы добавляем в нее свет от наших специальных динамических источников света. Таким образом, получается Dynamic Light Probe – т.е. сферическая гармоника с добавлением динамического света. После этого начинается рендеринг персонажа с шейдером, который умеет работать со сферическими гармониками.



    Если представить сферическую гармонику упрощенно, то ее функционал будет похож на кубическую текстуру (Cube map). В шейдере мы делаем выборку из сферической гармоники по вектору (аналогично Cube map). Для диффузного (diffuse) освещения мы используем вектор нормали к поверхности, для отраженного (specular) – отраженный вектор взгляда.

    Диффузное освещение считается в вертексном шейдере, отраженное – в пиксельном. Таким образом, мы за один Draw Call рендерим модель персонажа с динамическим освещением от нескольких источников света.

    Тени


    До прихода в Mail.Ru Group я несколько лет разрабатывал свой движок, и успел проработать практически все алгоритмы рендеринга теней и освещения, и понимаю, как работает рендер Unity и тени в частности. Там используется shadow mapping, а точнее PSSM — parallel split shadow mapping. Невозможно на слабом устройстве получить высокий FPS при использовании такой техники.

    Поэтому я разработал три системы, чтобы попробовать какая из них будет более производительной. Остановился на алгоритме planar-shadows, или тенях, которые рисуются на плоскости. Это самый простой алгоритм, но и вместе с тем самый производительный.

    Что такое планарные тени? Это рендеринг такой же модели, только сплющенной по какой-то из осей до нуля. Выбор оси масштабирования зависит от угла источника света.

    В интернете вы можете найти кучу примеров, как посчитать такую матрицу масштабирования. Я хотел бы остановиться на других аспектах. Во-первых – это блендинг тени. Если ее блендить ее напрямую, то получаются такие артефакты как на следующем рисунке.



    Там, где пересекаются несколько полигонов, они и блендятся несколько раз. И тень получается в некоторых местах темней. Как с этим бороться? Мы все эти тени рисуем сначала в stencil buffer, отключая запись в back buffer. Затем, когда все тени занесены в stencil buffer, мы делаем финальный проход рендеринга, на котором заливаем весь экран нужным цветом, с учетом stencil-маски. Получаем корректные динамические тени, которые никак (!) не влияют на FPS. Во-вторых, планарные тени можно рендерить только на плоские поверхности (недаром они называются планарными).



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

    Пост-эффекты


    Стандартное решение для пост-эффектов, которое используется во всех примерах Unity — это использование метода OnRenderImage. Суть его в следующем: c Unity работают не только программисты, но и гейм-дизайнеры, художники, аниматоры и другие специалисты. У них всех разная квалификация, и разработчики Unity вынуждены делать универсальные решения, чтобы пользователи просто брали движок из коробки и получали какой-то более-менее работающий пост-процесс. Из-за подобной универсальности в Unity прибегли к следующей технике — они за вас копируют back buffer в отдельную текстуру и отдают вам ее на обработку. В чем здесь проблема? Копирование back buffer’а в текстуру на iPad 2 убьёт больше половины FPS. Даже простой вызов метода OnRenderImage без дальнейшей обработки снижает производительность больше, чем в 2 раза. Как с этим бороться? Мы указываем камере отдельную рендер-текстуру и обрабатываем ее в методе OnPostRender. Единственное, что придется сделать дополнительно — отрисовать результат в back buffer.



    Также, по возможности, если вы сможете придумать, как заменить вот такой мощный пиксельный пост-процесс на какие-то геометрические эффекты, то это стоит сделать, потому что геометрические эффекты работают заведомо быстрее.



    Еще пара слов о геометрическом пост-процессе: так как мы все равно рендерим всю сцену в свою рендер-текстуру, то впоследствии можем использовать ее как захотим. В данном случае, рисуя снайперский прицел, мы накладываем текстуру на специально подготовленный mesh, у которого растянуты текстурные координаты, таким способом, что создается впечатление преломления по краям. В динамике это выглядит, как будто вы смотрите через линзу. Этот пост-процесс вообще никак не влияет на FPS.

    Вот такими методами и уловками мы получили наши 30 FPS (и выше) на iPad 2 и iPhone 4S. Приложив некоторые усилия, мы получили, практически, бесплатное в плане производительности динамическое освещение и тени. Мы прошли долгий путь разработки, сменили множество механик и подходов, чтобы получить ту игру, которую вы видите сейчас. Если у вас есть какие-то вопросы, то буду рад ответить на них в комментариях.

    Презентация с КРИ:


    Александр Черняков
    Программист IT Territory, студии Mail.Ru Group
    Mail.Ru Group 791,05
    Строим Интернет
    Поделиться публикацией
    Комментарии 15
    • +1
      А при каком количестве полигонов на сцене у вас получилось 30 FPS на iPad 2 с учетом примененных выше технологий?

      Ну и конечно же было бы очень круто если бы вы пошарили систему освещения, но это уже я думаю мечты :)
      • +1
        60k-80k полигонов
        100-130 draw calls
      • 0
        А анти-алиасинг на Unity/iPAD2 пока недоступен?
        • 0
          Доступен. Но Эволюция работает без него.
        • 0
          Затем, когда все тени занесены в stencil buffer, мы делаем финальный проход рендеринга, на котором заливаем весь экран нужным цветом, с учетом stencil-маски. Получаем корректные динамические тени, которые никак (!) не влияют на FPS.

          Как-то вразжопицу получается. Не ясно для чего нужна была полноэкранная заливка, если можно сразу писать в кадр и стенсил с тестом на чистоту этого самого стенсила? Никаких заливок экрана и не понадобилось бы. А то, что полноэкранный квад никак не повлиял на филлрейт, может говорить лишь о том, что упираетесь в CPU или vertex processing. Хотя кто знает во что там эта ваша юнити упирается… ;)
          • 0
            Полноэкранная заливка, в первую очередь, нужна для избежания блендинга нескольких слоев полигонов (как на скрине в статье).
            Вы предлагаете одновременно на одном проходе и читать и писать в стенсил-буфер? Разве такое возможно? А если перерисовывать тень дважды, то у нас будет не 60-80k полигонов, а 100-120k.
            К тому же это не спасет от блендинга пересекающихся полигонов. Полноэкранный квад не влияет на филлрейт, потому что большая часть квада отсекается стенсил-маской еще до выполнения пиксельного шейдера и блендинга соответственно.
          • 0
            они за вас копируют back buffer в отдельную текстуру и отдают вам ее на обработку.

            Как такое вообще можно реализовать средствами OpenGL ES 2.0 на iPad2? Наверное, можно было бы использовать glBlitFramebuffer, но эта функция доступна только в OpenGL ES 3.0.
            Возможно я ошибаюсь, и можно присоединить render buffer к back buffer, к которому в свою очередь присоединена текстура. Я так никогда не делал и никогда не видел подобных решений. Думаю, что это вряд ли возможно, т.к. back buffer это все таки не frame buffer, хотя я и не уверен в случае с ios. Если это все же возможно, что должно произойти после swap'а front и back буфера?
            По моему личному мнению, было бы очень странно, если в Unity действительно перед пост процессингом, содержимое из back буфера копировалось в текстуру.
            • 0
              Дело в том, что в free-версии Unity RenderTexture вообще недоступна и пост-процесс соответственно невозможен. Движок просто все рисует в backbuffer.
              В pro-версии RenderTexture можно создавать самим, но движок-то остается одним и тем же. Соответственно он будет рисовать в backbuffer, пока мы не укажем явно RenderTexture для камеры. А если мы вообще не укажем RenderTexture, то при пост-процессе в методе OnRenderImage будет создана RenderTexture, в которую будет скопировано содержимое back buffer.
              Как прочитать backbuffer в текстуру на OpenGL ES 2.0 я не в курсе. Как это делает движок тоже не знаю. В любом случае, даже если это не backbuffer, а какой-то внутренний буфер движка, он все равно будет скопирован в новую RenderTexture, что тоже медленно, особенно если retina-разрешение на каком-нибудь iPad3.
            • +1
              По поводу планарных теней — как решили вопрос с отсечением теней? Если просто добавить материал к MeshRenderer/SkinnedMeshRenderer, то bounding box тени не будет соответствовать bounding box'у модели, в результате чего тень будет постоянно исчезать и появляться из ниоткуда, так как отсечение понятия не имеет о том, что вы как-то там в шейдере вершины сдвинули. Сейчас как раз просто тоже занимаюсь реализацией планарных теней, и это просто-таки какая-то нерешаемая адекватно проблема.
              • 0
                Хороший вопрос :)
                У нас каждый персонаж рисуется нашим кастомным шейдером, у которого есть тэг «ShadowCaster».
                Мы создали отдельную камеру для теней и задали ей replacement shader с таким же тэгом. Этот шейдер рисует те же самые модели с теми же bounding box'ами, только плющит их нашей матрицей тени.
                • 0
                  Ну вот я про это и говорю, что bounding box не меняется. Грубо говоря, если сделать тень достаточно длинной, и расположить камеру так, чтобы в поле зрения камеры не попадала сама модель, но попадала тень — то не будет отрисована ни модель (что логично), ни её тень (что плохо).
                  • 0
                    Этим страдают практически все техники отрисовки теней. Мы просто оставили все как есть, у нас не видны эти «щелчки» тени.
                    Unity не дает возможность управлять BoundingBox'ом или системой отсечения, нельзя зафорсить отрисовку объекта, если он невидим камерой, это правда.
                    Если бы у меня стояла такая задача, я бы попробовал сделать следующее:
                    — для камеры, которая рисует тени задать фрустум и позицию так, чтобы она видела все объекты, отбрасывающие тень.
                    — в шейдере отрисовки тени заменить матрицу вида и проекции теневой камеры на матрицы вида и проекции основной камеры.
                    • 0
                      Что нельзя задать bounding box'ом в Unity — это да, досадно, учитывая, что посчитать-то его для тени проще простого =/
                      Но мысль про замену матриц интересная. Спасибо, попробую :)
              • 0
                Ответьте, пожалуйста, на вопрос из предыдущего поста про балансировку! Я всё ещё помню и жду!
                • 0
                  Про балансировку поступил ряд вопросов, и мы напишем отдельный пост про это. Следите за обновлениями!

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое