Создание игры «35ММ». Постапокалипсис в России
Всем доброго времени суток, меня зовут Сергей Носков. Сегодня я бы хотел рассказать о создании моего первого полноценного инди-проекта под названием 35ММ, вышедшего в Steam в 2016 году. История конечно давняя, и с тех пор уже было опубликовано несколько статей и интервью на тему проекта, однако, подробного описания процесса разработки не было. Также, практически не были затронуты технические моменты реализации. Об этом, собственно, мы и поговорим.
Начнем с небольшой предыстории. 35ММ — это адвенчура с видом от первого лица в сеттинге постапокалипсиса на территории России. В народе — симулятор ходьбы. Игра повествует нам историю путешествия двух странников по опустевшим землям, оставленным цивилизацией. Основная часть населения вымерла после страшной болезни, и теперь природа отыгрывает у человечества свои очки. К сожалению, уже не помню точно, как зародилась идея данного проекта, но точно помню, что на тот момент я был ярым фанатом темы сталкера, игр «Метро» и вообще подобного атмосферного антуража. У меня всегда вызывали трепет и восторг пейзажи заброшенных городов, промышленных зон и деревень. Уж не знаю, что это за болезнь такая и как такую любовь объяснить, но нас таких много. В общем-то, подобной страсти по данной теме было достаточно для того, чтобы начать создание своего небольшого игрового мира.
Поскольку за плечами у меня уже было два небольших проекта (Свет и Поезд), а также опыт работы в движке Unity, я начал разработку новой игры именно в нем. Если не ошибаюсь, на тот момент уже была доступна пятая версия движка, но я в некоторой степени консерватор (не лучшая моя профессиональная черта), поэтому решил остаться в версии 4.7. Для общего понимания — между версиями движка 4 и 5 очень много существенных различий, особенно в плане рендеринга, освещения и материалов. В Unity 5 появились физически точные шейдеры, способные корректно отражать свет и отображать рефлексы. Простыми словами — блики и отражения на материалах с такими шейдерами выглядят более натурально и симпатично. В 4 версии базовые шейдеры были значительно проще, однако, навыки написания собственных шейдеров могли значительно повысить качество картинки. Об этом поговорим немного позже. Вполне понятно, что разработка игры включает в себя множество аспектов, помимо работы в самом движке. Для того, чтобы в движок было что запихивать-требуется контент: модели, текстуры, звуки, скрипты и т.д. Все то, что в итоге мы видим на мониторе и слышим в колонках. И конечно же, для каждого типа контента требуется свой софт. Для создания 3D моделей я использовал 3D max, для работы с 2D графикой и создания текстур — Photoshop, для работы со звуком — Adobe Audition, код писал в прилагаемой к Unity программе Monodevelop. Особо впечатлительным рекомендую закрыть глаза или перескочить к следующей главе — игра написана на Javascript. Только тсс, никому не говорите. Просто я знаю, что для некоторых это дурной тон и на Javascript уже
никто не пишет. В общем, основные инструменты выбраны и рабочий процесс потихоньку тронулся с места.
С чего все началось
В некоторых интервью я уже упоминал о том, что обычно не имею четкого плана разработки, а лишь общую картину. Поэтому, зачастую, создание локаций происходит стихийно и дизайн продумывается на ходу. Это, конечно, больше минус, чем плюс, но есть и положительная сторона — это очень занимательно и ты никогда не знаешь точно, что будет дальше. Получается так, что игра в некоторой степени живет своей жизнью уже на начальных этапах. Разработка 35ММ началась с создания самой первой локации — лесного заброшенного домика и просторного участка с полями и хвойным лесом.
Для построения поверхности земли использовался террейн с 5-6 текстурами травы и земли. Стандартный шейдер террейна окрашивает поверхность разными текстурами по RGBA маске, которую мы создаем кистью в самом движке, и результат часто выглядит очень замыленным, переходы между текстурами слишком плавные и не кажутся натуральными. Для доработки этого момента был изменен шейдер террейна и добавлена маска смещения. ЧБ текстура «смещала» маску, нарисованную в террейне и создавала более рваные и четкие края, что визуально немного усложняло вид поверхности.
Дополняло поверхность нашей земли несколько типов травы в виде хаотично раскиданных плейнов. В юнити также имеется билборд режим (когда плейны травы всегда смотрят на игрока), но для игры с видом от первого лица он не очень подходит, ибо слишком заметно, как трава «следит» за нашей камерой. С травой всегда нужно быть осторожным и добавлять ее в разумных пределах, поскольку данное удовольствие накидывает достаточно много вызовов отрисовок (drawcalls), а это в свою очередь сказывается на производительности. Чем больше drawcalls — тем выше нагрузка на систему компьютера. Но не стоит забывать, что не одни лишь вызовы увеличивают нагрузку. Есть масса способов убить производительность в игре. Дополнительно к траве на земле для разнообразия были раскиданы меши с ветками и камнями, а также ряд декалей с грязью и следами автомобильных колес.
После создания поверхности переходим к растительности, деревьям и кустам. Основную часть леса (хвойные деревья) я предпочел высадить с помощью инструментов самого террейна — то есть кистью. Плюс данного метода в том, что это делается быстро и легко, к тому же на большой дистанции такой лес трансформируется из мешей в билборды, что очень хорошо сказывается на оптимизации. Однако, вблизи нагрузка сильно увеличивается, поскольку, как я понял, такие деревья не батчатся. Возможно я ошибаюсь, и кто-то меня поправит. Батчинг — очень важный для оптимизации инструмент. Грубо говоря, это объединение мешей с одним материалом в один общий меш, что значительно сокращает количество drawcalls, соответственно снижает нагрузку. Еще один минус рассадки на террейне — одинаковое положение деревьев, то есть все они создаются под одним углом и эта одинаковость сильно бросается в глаза. В связи с этим, некоторые сосны, ели и лиственные деревья я устанавливал вручную под разными углами и с разными размерами, что вносило в пейзаж разнообразие. Таким же способом были расставлены и кусты.
Для полноты картины осталось разобраться с небом. Для этой задачи использовался обычный скайбокс материал с шестью текстурами. Были попытки модифицировать шейдер так, чтобы небо выглядело динамическим, но результат себя не оправдал и эту затею пришлось оставить. Альтернативным вариантом стало использование системы частиц с текстурой облаков и горизонтальными билбордами с анимацией движения. Насколько я помню, аналогичный вариант применялся в игре Stalker, да и наверняка много где еще.
Освещение
В 4-й версии движка Unity был очень удобный режим запекания лайтмапов — dual lightmapping. В текущих версиях имеется аналогичный вариант, но я пока детально не изучал его. Dual режим позволял нам на ближних дистанциях отрисовывать рилтайм тени и блики, но по мере удаления от камеры, все это дело плавно переходило в запеченные лайтмапы, что очень облегчало задачу для нашего «железа». В общем, данный способ я и применял на всех локациях игры. В результате, для каждой средней локации был запечен комплект примерно из 5-10 лайтмапов для ближнего плана, и аналогичное количество для дальнего (на ближнем плане тоже лайтмапы, но только с запеченным амбиент окклюженом).
Вообще, на многих участках я старался использовать полностью запеченный свет, за исключением света солнца. Местами были расставлены Point светильники для подчеркивания акцентов и большей освещенности. В основном это делалось в помещениях, куда проникало мало внешнего света. В ряде мест, конечно же, использовались и рилтайм светильники с тенями: свет от костра, настольной лампы, потолочного или настенного фонаря. Кстати говоря, в работе с освещением пришлось столкнуться с большой проблемой, связанной с Point светильниками и рилтайм тенями. На некоторых участках взгляд камеры на точечный источник света с тенями давал жуткие фризы и тормоза. Не совсем ясно, почему нагрузка была настолько велика, но профайлер в этот момент показывал зашкаливающие значения drawcalls на долю секунды. Исправить ситуацию помогло использование двух Spot светильников, направленных в противоположные друг от друга стороны. Такой вариант оказался менее тяжеловесным.
Модели
Большая часть 3D моделей для игры была создана самостоятельно. Что-то делалось достаточно тщательно, с запеканием карт нормалей и прочими тонкостями, а что-то создавалось на скорую руку в целях экономии времени. Основная часть объектов создавалась группами и использовала единую атлас-текстуру. То есть в одной текстуре находились участки, например, для бетонного блока, дорожного знака, кирпичного мусора, канализационного люка и тд. Это позволяло применять для всех данных объектов один материал, а соответственно, позволяло объектам забатчиться. Как мы помним — это довольно таки неплохо. Некоторые модели были добросовестно скачаны мной с просторов интернета, из свободных библиотек. В основном это мелкие пропсы для наполнения помещений, однако, все эти модели я старался немного видоизменить, чтобы схожесть не сильно бросалась в глаза. Часто замечал в инди — играх одинаковые ассеты, что несколько влияло на восприятие не лучшим образом. Наиболее проблемным в плане создания стал транспорт. Моделирование колесной техники с нуля очень трудозатратно и занимает массу времени. Потому, несколько экземпляров автомобилей было куплено мной в магазине Asset Store.
Отдельной «песней» стало создание персонажей. Это тот еще экспириенс. Для тех, кто не очень хорошо представляет, какой объем работы необходим для того, чтобы на свет появился персонаж, способный как-то существовать в игре, поясню. Создается высокополигональная модель со всеми деталями, пуговицами на рукавах, морщинами на лице и тд. Создается низкополигональная модель того же персонажа с текстурной разверткой (в моей игре количество полигонов на персонажа в среднем было около 5-8 тысяч). Далее с высокополигональной модели для низкополигональной путем хитрых или нехитрых манипуляций снимается карта нормалей, карта амбиента (мягкого затенения). Я обычно из амбиента потом делаю диффузную карту в фотошопе. В диффузной карте в альфа канале создаем карту спекуляра для создания блеска.
Для 2019 года конечно уже слишком примитивно, но для 16 года и для инди-проекта было вполне годно.
Далее, нашего перса нужно зариггать — поместить в него кости, за счет которых он сможет двигать конечностями, шевелить челюстью, сгибать и разгибать пальцы и т.д. Ну и в конечном итоге, все это дело нужно заанимировать. Обычно к персонажу создается набор анимаций с разными состояниями: ходьба, бег, стоячее или сидячее положение. Но требуются также и уникальные фрагменты, например, в моем случае для напарника нашего героя Петровича понадобилось большое количество вариаций действий: открывание дверей, рассматривание карты, драка с бандитами, бросок световой шашки на уровне Бор и т.д. Все это пришлось анимировать вручную, что, конечно, сильно бросается в глаза своей топорностью. Вообще, ручная анимация движений человека — занятие весьма сложное и добиться правдоподобного результата крайне сложно. Поэтому моушн-кепчер является наиболее подходящим для данной задачи решением. Насколько я знаю, сейчас этот вариант дешевле и быстрее, чем работа аниматора, хотя полученные данные нужно обрабатывать и «подчищать» вручную.
Шейдеры
Сразу уточню — о написании шейдеров я понимал на тот момент очень поверхностно. Мое обучение в основном заключалось в разборе готовых примеров и их доработке. Брал различные варианты из сети, менял параметры, добавлял новые или убирал старые и проверял, как это отражается на результате. Оказалось, что это крайне увлекательное занятие. Особо интересным для меня было оперирование разными каналами текстуры в качестве маски. В некоторых случаях я старался уместить максимальное количество информации в одну текстуру и использовать ее. В начале статьи я упоминал о различиях между 4-й и более поздними версиями Unity, а конкретно, о наличии в последних физически корректного шейдинга. Данный недостаток я попытался устранить своими силами и в стандартный шейдер со спекуляром, кубмапой и нормал мапой был добавлен эффект френеля. Это такая особенность отражающих материалов, при которой поверхности под углом, относительно нашего взгляда, отражают окружение (или кубмапу в данном случае) сильнее и обычно выглядят более светлыми и контрастными. Это очень хорошо заметно на глянцевом шаре, края которого кажутся более светлыми, чем центр. Мне удалось повторить данный эффект, а также добавить возможность заблюрить кубмапу в материале, что обычно мы можем наблюдать на отражающих, но шершавых поверхностях. Данный шейдер меня полностью устроил и был применен к большинству материалов в игре.
Вторым интересным опытом было создание шейдера для кожи персонажей. За основу был взят код, найденный в интернете, позволяющий использовать градиентную текстуру, отвечающую за силу и цвет освещения, влияющего на модель. Подобная текстура с красноватым оттенком в середине позволила имитировать человеческую кожу, которая как бы немного просвечивает, то есть имеет свою толщину, в которой свет плавно рассеивается. Эффект не идеальный, но выглядит лучше, чем стандартный пластиковый Bumped Specular.
Помимо вышеперечисленных шейдеров в процессе работы было создано множество второстепенных вариантов с индивидуальными эффектами.
Например, шейдер лужи с кубмапой и деформацией диффузной карты. Поскольку применять реальные отражения для луж слишком накладно, да и рендер в текстуру использовать не хотелось (это когда кадр сохраняется в текстуру и применяется в материале, в ходе чего, например, можно сделать искажения теплого воздуха), я решил сделать простые искажения текстуры земли, натянутой на лужу. Эффект был вполне симпатичным и совсем не напрягал железо. Кстати говоря, для искажений воздуха у керосиновых ламп и костра был как раз использован шейдер с рендером в текстуру. Кажется, это был Heat distort из ассета Detonator. Также, для имитации объемных лучей света был создан вертексный шейдер с эффектом Soft Particle и Rim light эффектом (когда мы смотрим на полигоны под углом, меш уходит в альфу). Это классический и уже "бородатый" способ реализации. Сейчас, для новой Unity есть крутой вариант, работающий на основе постэффекта и позволяющий рисовать реальные лучи света даже с учетом теней.
Еще стоит отметить комплект шейдеров, которые были сделаны для имитации мокрых поверхностей. В игре есть эпизод, в котором в определенный момент начинается проливной дождь и часть материалов плавно приобретает характерный блеск. Основной эффект применялся к террейну, на котором как и в случае с лужами начинала искажаться диффузная текстура. Также проявлялись и подтеки воды на окнах домов. Ну и самой «мокрой» фишкой были стекающие по объективу капли. Здесь на самом деле меня терзали сомнения, ведь у героя не было ни очков, ни шлема, и каким образом капли так навязчиво красуются на экране — было не ясно. Однако, визуально мне эффект так понравился, что я просто не смог от него отказаться.
Так мы плавно переходим к постэффектами. Говоря о каплях на экране — все просто: несколько капель из текстуры множатся и с разной скоростью движутся вниз. Параллельно вниз движутся волны (текстуры градиента), которые умножаются, каждая на свою группу капель. Потом все это дело суммируется, слегка выводится в «диффузку», если можно так выразиться, но в основном применяется в качестве маски смещения координат. В итоге наша картинка искажается от преломлений капель воды. Основной набор постэффектов, которые на камере были всегда или опционно (если игрок их включал или не отключал) — это антиалиасинг, SSAO, Bloom, Aberration, Vignette, Sun Shafts. Все это стандартные эффекты Unity, однако SSAO был модифицирован так, чтобы отрисовка теней на расстоянии сводилась к нулю, ибо вдалеке в тумане темные пятна теней смотрелись странно. Был также изменен и эффект аберрации (это цветовые искажения картинки при использовании линз, что-то вроде цветовых контуров по краям объектов). Стандартный эффект от Unity рисовал бордово-зеленые края объектов (довольно странное решение на мой взгляд). В действительности, чаще всего цвета ближе к желто-красно-синим, что и было мной реализовано. Еще одним постоянным эффектом был самописный колор коррекшн. Стандартный эффект Юнити показался мне слишком ресурсоемким, поэтому был реализован свой, упрощенный. В основном он создавал эффект тонмаппинга и немного менял цветовую гамму на более холодную. Выбор цветовой палитры картинки —это всегда сложная задача, в которой трудно определиться. Бывает так, что могут нравиться совершенно противоположные варианты и принять решение крайне сложно. В данном проекте я остановился на тусклой и холодной гамме. Многим она показалась чрезмерно блеклой, но, как мне кажется, она очень точно передает настроение, которое я пытался отразить в своей игре, настроение печали, уныния и одиночества.
Что же с кодом?
Я неоднократно упоминал в интервью о том, что от темы программирования всегда был далек и больше делал упор на визуальной составляющей. Первый полноценный код я начал писать, работая над игрой “Поезд”, так что к моменту разработки 35ММ некоторые навыки у меня уже были. Вообще, квестовый жанр мне показался очень подходящим для понимания программирования на моем начальном уровне. Большая часть действий в игре основана на триггерах. В триггер (кубик с коллайдером) попадает объект, и начинает что-то происходить, например стартует кат-сцена. В скрипте, как в сценарии к театральному спектаклю, построчно описано, когда и что происходит — сейчас у нас выключается камера игрока, включается камера кат-сцены, появляется персонаж в кадре, запускается анимация разговора и т.д. Я полагаю, что есть инструменты, которые весь этот процесс облегчают (полагаю, потому что не углублялся), но такой вариант мне до сих пор кажется наиболее понятным, потому что ты сам контролируешь все события. Перемещение нашего напарника в игре было реализовано с помощью триггеров, которые являлись чекпоинтами его маршрута. При попадании в триггер могла включаться какая-то новая анимация, или же персонаж мог что-то сказать.
Такой метод использовался на всех уровнях, кроме последнего. На финальной локации в городе, если мы добирались до нее с напарником, он уже не вел нас по маршруту, а наоборот бегал за нами. Там уже использовался контроллер, основанный на NavMesh (системе, которая позволяет объекту искать путь до цели и двигаться к ней).
Сложнее обстояли дела с медведем на второй локации игры. Там был использован контроллер, работающий лишь с ригидбоди (физическое тело), поэтому зверь оказался глуп и часто врезался в деревья и прочие объекты. Физический материал с нулевым трением позволил избежать серьезных застреваний и медведь в итоге как бы соскальзывал и продолжал нас преследовать. Здесь, и вообще на участках, где можно погибнуть, я столкнулся с самой серьезной для меня проблемой — запуск смерти и рестарта. В момент смерти нужно было учесть все текущие состояния персонажа: включен ли фонарик, открыта ли карта, активирован ли нож и т.д. Также требовалось сохранить значения здоровья и всех ресурсов и затем, все, что активировано, требовалось деактивировать и запустить анимацию падения камеры. После затемнения экрана нужно было все вернуть и прочитать сохраненные значения. На самом деле больших сложностей при должном подходе тут нет, но в моем случае выскакивало множество багов: то нож оставался в руках перед глазами в момент атаки медведя, то карта не убиралась — все в таком духе. К тому же, никогда не знаешь как себя в этот момент может повести игрок, куда он побежит и в какие условия заведет медведя, который может где нибудь застрять или, к примеру, атаковать нас через стену. В общем, много нюансов, которые сразу и не предусмотришь.
Взаимодействие нашего персонажа с объектами было реализовано с помощью луча Raycast. Все интерактивные объекты были помечены тэгом Subject, и когда луч в них попадает, он активирует подсветку (меш — индикатор с подсвеченными краями) и включает скрипт, который уже отвечает за то, какое действие мы можем совершить с этим объектов, например подобрать предмет, прочитать записку или открыть дверь.
Для взаимодействия изначально были планы сделать полноценные руки, которые тянулись бы к объектам, это создавало бы более явный эффект присутствия. Но такой вариант представлял для меня большую сложность реализации и перспективу наличия целой “пачки “ багов в дальнейшем, поэтому остались лишь руки, которые носят уже подобранные предметы. Перед камерой висит префаб с маленькими ручками, в которых уже имеются все предметы (фотоаппарат, нож, топор и т.д). При выборе же предмета в процессе игры, нужный включается, а ненужные выключаются.
Интересный момент был связан с анимацией разговора персонажей. Техника примитивна, но я сам додумался, горжусь, ага. Сперва я думал, что при общении персонажей придется при каждой фразе запускать анимацию открытия челюсти в рандомном порядке. Но потом в голову пришло то, что можно скриптом считывать уровень громкости звуковой дорожки в момент проигрывания и переносить этот уровень в float значение, которое уже отвечает за положение челюсти героя. В конечном итоге, челюсть автоматически открывалась при произношении слов в такт звуковому файлу. Это значительно упростило задачу, хотя и выглядело слишком “машинно”.
Оптимизация
Оптимизация — это очень важная часть разработки, от которой зависит то, насколько “гладко” игра будет работать на различном железе. Я затрону оптимизацию именно визуальной составляющей проекта. Для этого существует несколько полезных методов: группы лодов, окклюжен куллинг, отсечение объектов на расстоянии. LOD Group стоит использовать в случае с “тяжелыми” высокополигональными объектами. Для этого создается несколько мешей с разным количеством полигонов. Чем дальше камера находится от объекта, тем более упрощенная модель отрисовывается в кадре. К примеру, для 35ММ лоды применялись в моделях автомобилей, персонажей, некоторых деревьев. Обычно делалось 2-3 лода, среди которых каждый последующий меш имел почти в 2 раза меньше полигонов. Для наглядности: исходная модель автомобиля состоит из 15 тысяч полигонов, первый LOD имеет уже около 9 т. (уменьшается количество ребер, удаляются мелкие детали, типа петель, деталей салона), второй LOD доходит уже до 5 т. (удаляются дверные ручки, зеркала внутри салона, геометрия становится еще проще). Далее в том же духе. Для лодов, кстати говоря, был использован один интересный прием. Когда мы запекаем лайтмапы для объекта с лодами, нам приходиться печь для обоих объектов. Для того чтобы сократить время на запекание и сэкономить память системы, я использовал скрипт, который автоматически переносил назначенную лайтмапу со всеми координатами с родительского объекта (нулевой LOD) на все остальные лоды.
Второй метод оптимизации — это Occlusion Culling. Это механизм, при котором отсекается все, что не находится в поле зрения камеры, либо закрыто другим объектом. Например, когда мы заходим в помещение, за стеной мы уже не видим многих предметов на улице, а поэтому незачем тратить ресурсы на их отрисовку.
Еще один полезный способ упростить рендер — отсечение объектов на расстоянии. Это самый первый вариант, с которым я познакомился еще со времен проекта “Свет”. На камеру вешается скрипт, который настраивает для каждого слоя свое расстояние отрисовки. В моем случае специально было создано три категории слоев, с мелкими объектами (предметы быта, молотки, кирпичи и мелкий мусор), со средним и чуть выше среднего размерами (чайники, кусты, цветочные горшки, небольшие фонарные столбы и т.д). Трем категориям назначались расстояния: 40, 80 и 120 метров. Оказавшись дальше указанного расстояния, камера переставала рендерить соответствующий объект. Вариант очень удобный и действенный, поскольку мелкие пропсы издалека уже не видать, а потому, отрисовывать их нет смысла.
Звук
Основная часть звуков для игры была взята из бесплатных библиотек с просторов интернета. Обычно я качал нужные варианты, а потом комбинировал и миксовал их в Adobe Audition. В общем-то про эту часть работы нечего особо рассказать, ибо это достаточно рутинный, нудный процесс и для меня не особо привлекательный. К слову, работа по внедрению звуков, озвучиванию кат-сцен, подгонке звуковых файлов, чтобы в нужный момент нужный звук проиграл — все это заняло, наверно, одну четвертую общего времени работы над игрой. Единственным приятным моментом тут было внедрение музыки, над которой работал крутой и чрезвычайно талантливый композитор Дмитрий Николаев. Я очень доволен тем что у него получилось, ведь по большому счету, я не знал точно, что именно хотел слышать. Но Дмитрий очень хорошо прочувствовал настроение, которое было заложено в проект и реализовал его в виде атмосферных амбиентов. Получилось нечто фантастичное, загадочное и мелодичное.
Еще одним интересным этапом была работа с голосовой озвучкой персонажей. Несмотря на критику со стороны, я до сих пор доволен результатом и считаю, что актеры очень хорошо справились со своей задачей. К слову, основных персонажей озвучивали Всеволод Петрыкин и Александр Браги, за что им огромное спасибо.
В целом, по работе со звуком каких-то серьезных проблем не наблюдалось, хотя, после релиза обнаружился редкий баг, который я до сих пор не смог побороть, ибо так и не понял его природу. Иногда часть звуков переставала проигрываться, либо звучала с каким-то сильнейшим эффектом эха. При разговоре у героя мог внезапно пропасть голос и точно так же внезапно восстановиться. Были догадки, связанные с большой нагрузкой и большим количеством звуков, игравших одновременно. Также были предположения о связи бага с зонами реверберации, но это не точно.
На этом, пожалуй, все. С момента разработки прошло уже много времени, какие-то вещи подзабылись, а какие-то стали уже совсем не актуальными, но надеюсь, что статья для кого — то окажется полезной и может быть даст ответы на некоторые вопросы. Всем спасибо и удачи!