Что первое приходит на ум инди-разработчику игр, когда он сталкивается с необходимостью добавить функцию, которую он понятия не имеет, как реализовать? Разумеется, он отправляется искать следы тех, кто уже прошел этот путь и удосужился записать свой опыт. Это то, что я сделал некоторое время назад, когда начал создавать тени в своей игре.
Найти необходимую информацию – в виде статей, уроков и руководств – не составило труда.
Однако, к своему удивлению, я обнаружил, что ни одно из описанных решений мне не помогло.
Поэтому, осознав свое, я решил рассказать об этом миру.
Стоит заранее предупредить, что данный текст не претендует на роль какого-то окончательного руководства или мастер-класса.
Используемый мной метод, возможно, не является универсальным, далеко не самым эффективным и не полностью покрывающим задачу создания двухмерных теней.
Это скорее рассказ о том, к каким уловкам пришлось прибегнуть неопытному разработчику в моем лице, чтобы добиться результата, удовлетворившего его требования.
Сам результат перед вами:
А подробности пути к ее достижению ждут вас под катом.
Немного о самой игре Dwarfinator — это двухмерный шутер с базовой защитой и боковой прокруткой, разработанный с прицелом на мобильный и настольный сегменты.
Геймплей состоит из планомерного уничтожения волн врагов в двух чередующихся режимах – защита и преследование.
Прогресс игрока включает в себя прокачку «танка» путем улучшения и замены различных его элементов, таких как вооружение, двигатели и колеса, а также повышение уровня и изучение активных и пассивных навыков.
Прогрессия окружения предполагает постоянное увеличение количества мобов в волне, добавление в волну новых типов врагов по мере прохождения локации, а также последовательную смену нескольких локаций, каждая из которых имеет свой набор врагов.
Постановка задачи
Итак, на момент принятия решения добавить в игру тени у меня было:- локация в виде двух спрайтов, один для отображения за мобами и другими сущностями, второй для отображения перед ними;
- мобы и статичные разрушаемые объекты, постоянно анимированные и состоящие из отдельных спрайтов в количестве от нескольких до нескольких десятков;
- снаряды, свои и врага, представленные в большинстве случаев либо одним спрайтом, либо системой частиц, в последнем случае тень не требовалась;
- танк, состоящий из нескольких частей, собираемых так же, как и мобы;
- стены, имеющие несколько фиксированных состояний, которые опять же представляют собой набор отдельных спрайтов.
Для всего этого нужны были простейшие тени, повторяющие контуры объекта и отбрасываемые от одного фиксированного источника света.
В то же время нужно было быть осторожным с производительностью.
В силу специфики жанра и особенностей его реализации большинство объектов, отбрасывающих тени, в любой момент времени располагаются непосредственно на экране.
А их общее количество может быть больше сотни, если говорить об игровых сущностях, и пары тысяч, если говорить об отдельных спрайтах.
Выполнение
Собственно, главная загвоздка заключалась в том, что Dwarfinator, грубо говоря, — это 2.5D игра.Подавляющее большинство объектов существует в двумерном пространстве с осями X и Y, а ось Z используется редко.
Визуально и частично с точки зрения игрового процесса ось Y используется для отображения высоты и глубины, разделяя таким образом на виртуальные оси Y и Z. В такой ситуации использовать стандартные инструменты Unity для создания теней не представлялось возможным.
Но на самом деле мне не нужно было честное освещение; достаточно было иметь возможность вручную создавать тень для каждого объекта.
Поэтому самое простое, что мне пришло в голову, — просто разместить за каждой сущностью ее копию, повернутую в трехмерном пространстве так, чтобы имитировать нахождение на поверхности локации.
Все спрайты такой псевдотени были окрашены в черный цвет, при этом сохранялась иерархическая структура владельца тени, что позволяло анимировать ее синхронно с владельцем одним и тем же аниматором.
Синхронизированная анимация выглядела примерно так:
Однако тень требовала прозрачности.
Самым простым решением было установить его для каждого теневого спрайта.
Но такая реализация выглядела неудовлетворительно — спрайты накладывались друг на друга, образуя там менее прозрачные области.
На скриншоте ниже показано, как выглядит тень, состоящая из нескольких полупрозрачных сегментов.
Также можно увидеть используемые параметры искажения тени: поворот по оси X -50 градусов, поворот по оси Y -140 градусов и масштаб по оси X увеличен в 1,3 раза относительно родительского объекта.
Стало очевидным, что прозрачность необходимо применить к тени как к целому объекту.
Первым экспериментом на эту тему было прикрепление камеры к тени, рисование этой тени в RenderTexture, которая затем использовалась как материал, прикрепленный к родительской тени Plane. Ему уже можно было без проблем выставить прозрачность.
Сами тени располагались за пределами кадра, чтобы избежать перекрытия зон захвата камеры.
Подход сработал, но оказалось, что уже пара десятков теней вызывают серьёзные проблемы с производительностью, в основном из-за количества камер на сцене.
Кроме того, ряд анимаций включал значительное перемещение отдельных спрайтов мобов внутри его корневого объекта, из-за чего поле зрения камеры должно было содержать область, значительно превышающую размер реального изображения в конкретный момент времени.
Решение было найдено быстро — если нельзя нарисовать каждую тень отдельной камерой, то почему бы не нарисовать все тени одной камерой? Все, что нужно было сделать, это выделить отдельную область сцены под тенями, чуть выше поля зрения основной камеры, навести на эту область дополнительную камеру и отобразить ее вывод между локацией и другими сущностями .
Ниже вы можете увидеть пример вывода с этой камеры:
Производительность от этой реализации пострадала гораздо меньше, поэтому решение считалось рабочим и применялось ко всем мобам, статичным объектам и снарядам.
Далее пришла очередь спрайта локации.
Использовать один спрайт для всех объектов, как это было реализовано ранее, оказалось невозможно.
Использование копии объекта в качестве собственной тени работает только в том случае, если объект полностью плоский.
Уже при создании теней для мобов было заметно, что точки контакта с поверхностью, разнесенные по третьей координате, нарушали корректность тени относительно этих точек.
На следующем снимке экрана показан пример такого нарушения.
За точку контакта с поверхностью принимается пятка моба, но тени от ног уже выходят за пределы самих ног.
И если в случае с ногами огра еще можно немного изменить положение тени и замаскировать проблему, то для нескольких десятков стволов деревьев шансов на это нет. Все объекты локаций, которые должны были отбрасывать тень, должны были быть выделены в отдельные GameObjects. Именно это я и сделал, разместив экземпляры соответствующих разрушаемых объектов на префабе локации и отключив скрипты, которые не использовались в этой позиции.
При этом благодаря этому появилась возможность включать их в общую сортировку объектов сцены, а снаряды, летящие за пределы локации, теперь не рисовались строго поверх всех объектов, а летали между ними.
Кроме того, появилась возможность анимировать сами объекты.
Но тут меня ждала новая беда.
Благодаря теням и десяткам новых объектов максимальное количество GameObjects одновременно на сцене, а вместе с ними и компонентов Animator и SpriteRenderer, увеличилось более чем вдвое.
Когда я выпустил на локацию всю волну мобов, которая насчитывала около 150 штук, Profiler укоризненно показал мне около 40мс, потраченных только на рендеринг и анимацию, а частота кадров в целом колебалась в районе 10. Я отчаянно оптимизировал собственные скрипты, боролись за каждую миллисекунду, но этого было недостаточно.
В поисках дополнительных инструментов оптимизации я наткнулся на документацию и руководства по динамическому пакетированию.
Еще немного о пакетировании Если коротко, то пакетная обработка — это механизм, позволяющий минимизировать количество вызовов рендеринга, а вместе с ним и время, затрачиваемое на взаимодействие CPU и GPU в момент отрисовки кадра.
При его использовании вместо отправки каждого элемента на рендеринг по отдельности похожие элементы группируются и визуализируются вместе одновременно.
В случае с Unity движок сам старается использовать этот механизм по максимуму и дополнительных действий со стороны разработчика практически не требуется.
Frame Debugger показал, что я в лучшем случае пакетировал детали каждого объекта или моба отдельно.
Создав спрайты для первой и второй по атласу, я добился пакетирования теней всего за несколько вызовов отрисовки, но владельцы этих теней упорно отказывались пакетировать друг с другом.
«Эксперименты на отдельной сцене показали, что динамическая пакетная обработка не работает, когда у объектов есть компонент SortingGroup, который я использовал для сортировки отображения сущностей на экране.
В теории можно было обойтись и без этого, но установка значений сортировки для каждого спрайта и системы частиц в объекте отдельно могла оказаться даже дороже, чем отсутствие пакетной обработки.
Но что-то меня беспокоило.
Теневой объект, будучи потомком хост-объекта в реальной сцене, технически был частью той же SortingGroup, но проблем с динамической группировкой теневых объектов не возникало.
Единственное отличие заключалось в том, что основные объекты визуализировались непосредственно на экране основной камерой, а теневые объекты сначала визуализировались в RenderTexture. В этом и была загвоздка.
В чем именно причина такого поведения, в Интернете неизвестно, но когда камера рендерила изображения в RenderTexture, SortingGroup больше не нарушала пакетную обработку.
Решение показалось очень странным, нелогичным и вообще просто костылем.
Но реализовав рендеринг сущностей тем же методом, что и рендеринг теней, и получив таким образом помимо теневого слоя еще и слой сущностей, я уже добился вполне приемлемых значений производительности.
На снимке экрана ниже показан пример рендеринга слоя сущности.
В целом рендеринг какой-либо сущности по координате Y выглядит так:
- Объект размещается по координате Y − 20;
- Объект рисуется камерой, наблюдающей за этой координатой в RenderTexture для объектов;
- Тень объекта размещается по координате Y+20;
- Тень объекта рисуется камерой, наблюдающей за этой координатой в RenderTexture для теней;
- Основная камера рисует на экране основной спрайт локации — единственный элемент, который в данный момент отображается непосредственно на экране;
- Основная камера рисует на экране Plane с RenderTexture теней в качестве материала;
- Основная камера рисует на экране плоскость, используя в качестве материала RenderTexture объектов.
На скриншоте ниже камера редактора переведена в 3D-режим, чтобы продемонстрировать расположение слоев относительно друг друга.
Нюансы
Но как выяснилось в процессе тиражирования решения на другие сущности, общий случай не покрывает все возможные сценарии.Например, были сущности, расположенные на определенной высоте относительно поверхности, в частности, снаряды и некоторые персонажи кат-сцены.
Кроме того, снаряды имели еще и свойство вращаться в зависимости от направления их движения на экране, поэтому, помимо задания точки пересечения объекта и его тени, необходимо было выделить вращаемую часть в отдельный дочерний объект, редактируем логику вращения снаряда и их анимацию.
На следующем скриншоте показан пример вращения снарядов и их теней.
Летающие персонажи, как и запланированные летающие мобы, кроме того, могут перемещаться в пределах своей виртуальной координаты Y, что потребовало создания механизма расчета положения тени по положению ее владельца на виртуальной оси Y.
На гифке ниже показан пример перемещения объекта по высоте.
Еще одним корпусом, выделяющимся из общей концепции, стал танк.
В отличие от всех остальных сущностей, танк имеет очень значительные размеры по виртуальной оси Z, а общая реализация теней, как уже говорилось, требует, чтобы объект был практически плоским.
Самый простой способ обойти это — вручную нарисовать формы теней для отдельных частей танка, поскольку на теневой слой можно было поместить что угодно.
Чтобы правильно построить нарисованные от руки тени, мне пришлось на основе скриншота существующей тени собрать конструкцию из линий, которую можно увидеть на скриншоте ниже.
Если масштабировать и разместить эту структуру так, чтобы верх находился в какой-то точке родительского объекта, а низ — в точке соприкосновения с поверхностью, то правый угол конструкции покажет, где должна находиться соответствующая точка тени.
Спроецировав таким образом несколько ключевых точек, нетрудно построить на их основе всю тень.
Кроме того, отдельные части танка могли иметь разную высоту для крепления дочерних частей, что, как и в случае с летающими персонажами и мобами, требовало корректировки положения тени каждой конкретной детали.
На скриншоте ниже показан танк, его тень в сборе и он же в виде отдельных частей.
Тени стен оказались отдельной болью.
На момент начала работы над тенями стены имели ту же природу, что и детали танка — один объект из нескольких десятков отдельных спрайтов.
Однако у стен было несколько состояний, контролируемых аниматором.
Долго думая, что с ними делать, я пришел к выводу, что концепцию стен нужно менять.
В результате стены были разделены на секции, каждая из которых имела свой набор состояний, свой аниматор и свою тень.
Это позволило нам использовать тот же подход к созданию теней для участков, параллельных оси X, что и для мобов, а для тех участков, которые не подпадали под это правило, придумать что-то свое.
В некоторых случаях мне приходилось создавать собственный аниматор тени раздела и вручную задавать положение спрайтов.
Например, в случае с разделом, показанным на скриншоте ниже, тень создается путем применения искажения к каждому отдельному бревну, а не ко всему разделу.
Заключение
Вот и все, собственно.Несмотря на все вышеперечисленные нюансы, изначально поставленная задача была выполнена в полном объеме, и теперь мой проект может похвастаться вполне прилично выглядящими тенями, хоть и несколько сомнительного происхождения.
Надеюсь, что благодаря этой статье для очередного инди-разработчика, задавшего аналогичный моему вопрос, Интернет станет немного полезнее, если не как пример для подражания, то хотя бы как чужая ошибка для собственного обучения.
.
Теги: #Разработка игр #gamedev #unity #shadows
-
История Одного Перехода (Не Через Альпы)
19 Oct, 24 -
Новое В Учете Суммовых И Курсовых Разниц
19 Oct, 24 -
Отладка Самолета? Это Очень Просто!
19 Oct, 24