Don't Mess With Bober - летопись разработки инди-хоррора, или "Как подобрать костыли под размер своего релиза"
Привет, мы с моим товарищем выпустили новую небольшую игру Don't Mess With Bober 3 июля, и тут я хотел бы поделиться частью решений, которые были приняты в процессе реализации и выпуска игры.
Об игре
Почему опять хоррор?
С технической стороны жанр хоррор является наиболее простым в реализации. В то время как к примеру Шутер - требует приличное количество противников, с каким никаким AI (если вы не делаете зомби шутер, где они идут на вас сквозь стены по прямой), наличие вариативности оружия, более длинные локации и хорошее ощущение оружие (ААА проекты многих избаловали), то в случае с хоррором сам жанр позволяет вам запереть игрока в небольшой локации с 1 монстром без механики боя. Попался - погиб, все просто.
Да, для того чтобы получился хороший хоррор требуется больше вовлеченности в создание сюжета, хотя с текущим форматом быстротечного контента даже этот пункт при желании можно опустить.
Учитывая то, что мы два энтузиаста, формат небольших релизов позволяет нам постепенно набивать руку и повышать (хотя бы пробовать) качество дальнейших релизов, и возможный переход в другие жанры. И дабы не терять мотивацию, хочется чтобы даже эти небольшие релизы имели какой-то успех.
Почему Бобер?
Изначально нами была замечена тенденция того, что в наше поле зрение попадаются хоррор-проекты, которые имеют бОльший уклон в юморную сторону, нежели в хоррор составляющую.
Аудитория контент-мейкеров ждет выхода нового Poop Killer-а больше чем условного Resident Evil-а. Графика под PS1, которая в большинстве случаев является ассетным прикрытием косяков, не имеет значения, ведь сама абсурдность повествования вполне их перекрывает.
Fuck You Witch - необходимость ругаться на ведьму даже на непонятном (для иностранцев) языке все равно звучит весело, даже если часть локаций ты видел в других инди-проектах.
Окей, с юморной составляющей определились, но так откуда все таки бобер? Первый релиз моего товарища, который он делал 4 года в соло был с уклоном в советское окружение, что привлекло внимание на фоне общей тенденции (ну или попыток) поднятия российского геймдева. В рамках этого внимания было одно из предложений собрать Артбук-Альманах Монстров из нашего фольклора. С идеи мы посмеялись, т.к. отношение его антагонисты к фольклору не имели никакого отношения, и в рамках шуточек появилась следующая идея:
Естественно в расчет были взяты мемы про то, как поляки встречают бобров, и то, что в моем обиходе слово "бобер" используется как слово LoremIpsum, если где-то нужно заполнить какое-то поле. В итоге, идея делать хоррор про бобра закрепилась, но опять же сюжетную завязку нужно было адаптировать под наши возможности.
Разработка
С чего начинается разработка
Если пойти читать статьи о разработке видеоигр, в 90% случаев при грамотном подходе всегда начинают с design-документа. Важным уточнением всегда является еще то, что по этому design-документу якобы читающий должен представить одну и ту же игру, и реализовать ее точно также.
Даже несмотря на то, что это наш второй релиз, где мы работаем вдвоем, по прежнему бывают упущения, которые все равно приходится объяснять или переделывать. Да и постепенно в процессе разработки вдвоем все равно приходит понимание, что каждый раз отправлять напарника "иди читай-пиши диздок" звучит как просто посыл, и легче в диалоге уточнить эти моменты.
Изначально вся документация велась в Notion, но после их ухода было принято решение перенести процесс разработки в Milanote. Последний имеет все необходимое, но в слишком базовом формате, например To-Do список и колонку таблицы нельзя увеличить в ширине, но зато сам формат свободной пин-доски позволяет раскидать всё так, как душе угодно.
В качестве движка учитывая уже имеющиеся релизы ранее, без вопросов был выбрал Unreal Engine 5.4 (последний на тот момент), и как прошлые релизы, цель была написать всю игру на Blueprints (встроенный визуальный node конструктор).
В качестве системы контроля версий было взято GitHub + GitLFS, т.к. настраивать PlasticSCM и прочее на чьей либо машине желания было примерно ноль, а подписка на расширенный LFS Гита стоила всего 5 долларов в месяц. Сейчас же с их измененными расценками даже дешевле. Из минусов - мы так и не решились и не выбрали какой именно инструмент хотим использовать для решения возможных merge конфликтов, учитывая то, что вся разработка изначально велась на одной сцене, а не подразбитая на зоны ответственности. Тем более в этот раз было решение что в качестве геймдизайнера выступаю я, в массе работ ответственный за кодовую составляющую. По итогу было принято решение просто передавать проект через гит как эстафету, тем более что эта разработка является для нас part-time, и основные работы 5/2 никто не отменял.
Старт разработки
В первую очередь необходимо было определиться с инструментарием, как именно мы планируем реализовывать какие либо взаимодействия в игре.
Первый вариант - индивидуальный, но долгий. Для каждого триггера (коснись невидимый куб чтобы активировать) мы пишем в какой момент что и в каком порядке он вызывает, что дает нам точную настройку вызовов. Из минусов - мы имеем много индивидуальных триггеров, разница в которых может быть всего лишь в условно вызываемом звуке, где остальное будет дублироваться. Часть повторений можно избежать через наследование, но это уже ближе ко второму варианту.
Второй - сделать общие наброски конструктора, который позволит легко пересобирать события на сцене, без погружения в изначальные blueprint-ы. Здесь возможна опять развилка - брать какие-то готовые ассетные фреймворки, такие как Horror Engine, или же делать самостоятельно?
Так как мы все таки планируем повышать свои навыки, был выбран вариант сделать собственный конструктор, пускай далеко не идеальный, но зато свой, родной. Был реализован блок для вызова поочередно (с ожиданием выполнения или без) действий.
Действия же реализуются через наследование общего функционала в виде 1. Получил команду выполниться - выполнился - вернул эстафету инициатору, который при необходимости вызовет следующего. В реализации каждого из компонента был переопределен пункт "выполнился".
На опыте прошлого релиза, мы заранее начали разделять карту на под-уровни(Sub-Levels), чтобы иметь возможность оптимизации ради отгружать/подгружать их в нужный момент, и заранее каждые переменные загонять в StringTable, чтобы дальнейшая локализация была проще.
Мы хотели избежать стандартных шаблонов маленьких хоррор игр - и поэтому сразу было решено отказаться от записок, и то, что все диалоги идут в формате визуальных новелл - подожди пока текст допечатается в субтитре, а после нажми далее.
Но как тогда делать повествование, если игра должна иметь относительно динамичный темп? Решено, надо добавить в игру озвучку. Дело это не дешевое и не простое - как быть? На этапах первых сборок было решено вставлять простой TTS без нейронок, дабы иметь возможность оценить длительность прохождения всей игры.
Еще один из шаблонов, от которого мы хотели избавиться - это то, что мы хотели сделать игру, а не визуальную новеллу. Если в вашей игре нет абсолютно никакого вызова за всю игру, да, пускай несбалансированного, забагнутого, но хоть какого то - поздравляю, скорее всего вы сделали ВНку (хотя при должном желании вы можете добавить какие то мини-игры и в них).
Если в вашем хорроре нельзя проиграть/погибнуть - можно ли говорить что-то о погружении?
Но если игрок может погибнуть - значит, нужны будут какие-то чекпоинты, так как заставлять игрока проходить всю игру заново из-за оплошности - слишком жестко в формате сюжетной игры.
Примеряем костыли
Проблема: Так как сюжетно было решено, что часть игры, хоть и маленькая, должна быть днем - мы решили сделать относительно динамическую смену времени суток, т.к. хотели избежать создание запеченного дубликата уровня для дня и ночи. Не смотря на все хваленые люмены-наниты-ray-path-pelmeni-tracing-и простое переключение режима освещения лишь насыпало нам проблем с производительностью.
Потому что даже при наличии LOD-ов, динамическое освещение каждой ветки-листка резало на практически пустой локации на моей 1060 FPS до 30 в лучшем случае.
Решение: Дальние деревья были лишены детальных LOD-ов, самые дальние - теней. Было подрезано общее количество bounce света, а также урезано качество теней для деревьев. До идеала далеко - но уже стало вполне играбельно при полностью загруженной карте.
Проблема: Первые тесты нами, как разработчиками, показали что общая длительность игры была максимально короткой. Сюжет игры проходился за 20 минут, но с поблажкой на то, что мы знали все необходимые действия.
Решение: Переработать часть локаций - изначально пещера была просто длинным туннелем с бревном.
То, что было изначально.
Также мы решили немного усложнить сегмент стелса с бобром, путем улучшения его ИИ (изначально он был слишком глуп) и усложнив дизайн этой локации.
Проблема: наш ИИ имеет не стандартную гуманоидную форму, а значит точка, из которой по дефолту исходят все имеющиеся в UE "Чувства" - не подходят. Точка, откуда смотрит бобер - находится не там, где глаза, а где то в воздухе над ним.
Решение: Здесь нам впервые пришлось отойти от blueprint-ов, т.к. переназначение этой точки в самом UE нет по дефолту. Поиски решения выдали разве что костыльное решение в виде запихивать AI внутрь уже имеющегося AI, который будет находиться как раз там, где должны быть глаза - но все это создавало гораздо больше проблем, чем решало изначальную задачу. Так что - идем ковырять C++ реализацию.
Ну и также в рамках этой задачи была добавлена частичная дополнительная коллизия бобру, чтобы он не слишком часто проходил сквозь другие модели.
Решение делать чекпоинты через настоящее сохранение состояния объектов на уровне звучит логичным, но в виду плохой полировки может добавить проблем. Игрок умудрился уронить условный ключ за текстуры - перезагрузка не поможет, ключ уже вне досягаемости. Исходя из этого было принято решение воспроизводить примерное состояние уровня при загрузке с чекпоинта, так как линейность игры это позволяет. Но из этого мы получили еще пару проблем:
Проблема: при загрузке с чекпоинта необходимо каким то образом проскипать условные катсцены, чтобы каждый задействованный в ней объект был выставлен словно она уже проигралась.
Решение: всё оказалось относительно просто, хотя с моей точки зрения не совсем логично - хотелось бы просто наличие такого метода из коробки. Мы стартуем кат-сцену, после этого перематываем ее на последний кадр, и останавливаем.
Проблема: Изначально мы не стали в архитектуру закладывать переменные, которые показывали бы готовность отображения чего-либо игроку. То есть - загрузился ли подуровень, прогрузились ли все текстуры, воспроизвелись и перемотались ли все катсцены в нужную позицию. Тем более мы не учли возможную переходную сцену для загрузки, что делать?
Решение: учитывая то, что с проблемой мы этой столкнулись лишь ближе к концу разработки, было принято решение схитрить. При каждом переходе в потенциальную загрузку мы показываем заглушку с затемнением экрана, на котором в углу крутится иконка загрузки. И показываем ее же с выходом из затемнения при запуске условного уровня. Учитывая то, что выход из затемнения у нас начинается спустя 2-3 секунды, мы даем запас времени движку прогрузить хоть что-то, прежде чем показываем это игроку:)
На этапе тестирования я решил прикрутить управление с геймпада просто ради того, чтобы играющим с условного SteamDeck было проще. Добавить управление персонажем не составило никаких проблем, но была 1 механика, требовавшая переработку, и ... меню.
Проблема: в UE при работе через Blueprints нет события, которое бы говорило о том, что определенный пункт меню выделен. Что, например игрок сейчас на джойстике перелистнул ниже, и теперь подсвеченным должен быть пункт именно LOAD GAME, а не NEW. Есть стиль для кнопки по умолчанию, стиль для кнопки Hovered (наведена мышь) и disabled. Причем я не могу насильно переключить текущий стиль на hovered, когда игрок переключается на какой то пункт.
Решение: в случае нахождения игрока в меню, было решение прибегнуть к блоку, который я старался максимально избегать в процессе построения всей игры - OnTick, т.е. практически каждый кадр. Каждый кадр идет переборка всех UI элементов с проверкой "Выделен ли я?" и в случае положительного ответа - ему перезаписывается обычный стиль на визуальный стиль похожий на OnHovered. В то время как остальным он сбрасывается по умолчанию.
Проблема: После добавления контроллеров, естественно следующим кирпичом в меня полетело то, что "как это добавили джойстик, а подсказки по прежнему показывают управление с клавиатуры?". Но опять же, у нас нет события того, что происходит переключение устройства ввода - что делать?
Решение: учитывая то, что я уже смирился с использованием OnTick, и того, что времени на переписывание и поиск архитектурного корректного решения уже не особо оставалось - проверка была реализована через захват последнего Input, и проверку "С Джойстика ли?", на основе чего дальше меняется текст в Подсказках. Но, учитывая то, что они изначально были рассчитаны на текст - клавиши геймпада пришлось указывать также текстом, а не иконками.
Тестирование
Тесты и фидбэк
И, вот, вроде как игра кажется готовой. Но пока что тестировали ее только мы сами, и хочешь не хочешь - глаз замыливается, ты уже находишь оправдание каждому косяку, потому что честно уже устаешь, особенно когда изначальные сроки разработки оценивались в 2-3 раза меньше. Находим энтузиастов из имеющейся аудитории, и запускаем тестирование через предоставление ключей в Steam.
Один из самых сложных моментов, когда понимаешь, что замечание тестера попало в точку, но на исправление этого необходимо перерабывать очень большой участок. Морально ощущается что игра уже закончена, и ты находишься на стадии полировки.
Вот из-за мелочи в виде "Да почему я каждый раз должен смотреть эту кат-сцену после загрузки" - необходимо переработать ту проблему с загрузочным экраном что я описал выше, дабы иметь возможность стартовать игру после загрузки на условное нажатие клавиши. Ведь решение респавнить игрока до кат-сцены обусловлено тем, что игрок должен и дойти до этапа, и также быть в безопасности в процессе загрузки. Какая реакция будет, если игрока убьют в момент, когда экран выходит и затемнения после загрузки?
Возникали вопросы по стандартизации управления - На какую клавишу должно быть "Использование"? Игроки в РПГ и FPS скажут что скорее всего на E, если у вас в игре нет Leaning-а, но игроки в квесты и симуляторы ходьбы с вами не согласятся. Какая клавиша с джойстика должна отвечать за бег?
Что делать с кривой сложности стелса с противником? Когда мы имеем пример, что человек без опыта проходит всю игру за 30 минут, и пример когда человек дважды по часу воюет только с сегментом стелса?
По итогу в процессе нам пришлось перебрать кучу мелких решений, относящихся к геймплею - добавить ящики средней высоты, дабы намекнуть игрока что приседание может помочь, добавить визуальное отображение того, что нам казалось очевидным. Добавить реплик, чтобы игрок понимал, что он точно закончил квест, что ему сейчас действительно нужно бежать, и кат-сцена на это намекала.
Общие отзывы по результатам тестирование вышли положительными, и самое главное, что мы слышали часто как минус - МАЛО. То есть, игра реализована, есть концовка, но как нам удлинить её, при учете того, что тестеры уже знают, какой вариант был изначально?
В этот момент помогло то, что мы решили реализовывать игру можно сказать через свой фреймворк - таким образом бы добавили еще две активности в общий сюжет (Старт ночи и финальная встреча). Общая прибавка вышла незначительной в плане геймплейного времени, но мы не стали затягивать другие возможные раздражители - Ведь количество предметов для выполнения задания на локации со стелсом могло просто увеличиваться. Видели мы просто уже один кейс у других разработчиков, где ради повышения длительности количество нужных предметов было повышено с 60 до 666, но решили что не сможем себе в глаза смотреть после такого.
Локализация и озвучка
Изначально в сам проект все реплики вставлялись на английском в меру моих познаний, и помощи гугл-переводчика. Попытки работы с нейросетями вылились больше в раздражение еще в прошлом проекте, так что мы прибегнули к старому решению - Перевести все через гугл-переводчик, и без обмана аудитории указать это в описании на странице Steam. Переводить по одной реплике - слишком долго, тем более что у нас планировалось 6 языков - что делать?
Небольшая магия с помощью Google Таблиц, и вуаля - мы получаем кривой машинный ПРОМТ-овый перевод. Господа граммар-наци, охладите ваше негодование, я рассматриваю релиз в свою пользу:)
Изначальная озвучка TTS была слишком топорной, хотя в какой то мере уже стала привычной и родной, но новой аудитории такое могло бы и не зайти. Вариант привлечь живую озвучку - дорого, долго, да и требует дополнительной обработки поверх - ведь необходимо очистить звук и более менее выровнять его по уровням в самой игре.
Поэтому в качестве решения была выбрана нейронка 11Labs, где за 11 баксов мы озвучили всю игру с двумя говорящими персонажами на два языка. С Русскоязычными голосами пришлось повозиться, но тем не менее, мы смогли отойти от TTSки.
Маркетинг и релиз
По собственным ощущениям разработка игры и так уже сильно затянулась, и поэтому в конце мы решили что выпуск нужно ставить максимально СРАЗУ. Первоначальной датой для релиза планировалось что будет 24 июня, за два дня до того как все уйдут заниматься доставкой, но в виду незначительных недочетов мы не прошли первоначальную проверку от модерации Steam, что сместило дату на 3 июля.
Трейлеры игры были размещены в наших соцсетях, также были попытки закинуть посты в тематические группы через предлагаемые новости. Несколько ключей еще до даты релиза были разосланы стримерам-ютуберам, но ни ответного письма, ни активации по статистике мы не видим.
Так как наша дата релиза попала на летнюю распродажу, в виду оптимизации всех процессов Steam перестал обновлять статистику по wishlist-ам, и сейчас мы уже на протяжении недели видим цифру 88 даже после релиза. После окончания распродажи статистика обновится.
Учитывая то, что мы вышли в распродажу, когда каждая игра имеет зеленый тэг скидки - мы также решили сделать небольшую скидку в 15% в течении недели, хотя учитывая появляющийся фидбэк - возможно цена будет понижена на эту сумму после окончания скидки.
На момент написания этого лонга у нас в Steam 22 отзыва, один из которых негативный на китайском:
Ну, а для самых внимательных (и быстрых) читателей DTF, прикрепляю пару ключей:)
22Z3F-9TJML-HE38H
LPRLP-DHRA9-FNAHR
YT4K6-9GGYM-RXQAP
JM2HY-ENL9Z-55L5K
B0GD9-F4CPG-KRD5Z
Спасибо за внимание, можем пообсуждать в рамках правил в комментариях :)