Protos, дневник #7: пространство рвётся по швам, а один эффект я похоронил

За этот заход у меня получилось лучшее, что было в проекте по картинке, и одновременно я четыре раза подряд провалил задачу попроще. Плюс сделал меню настроек и сломал его за следующие полчаса. Обо всём по порядку.

Protos, дневник #7: пространство рвётся по швам, а один эффект я похоронил
Protos, дневник #7: пространство рвётся по швам, а один эффект я похоронил

Пространство рвётся по швам

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

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

Получается, что кубы вырастают ровно там, где зона режет мир, и обтягивают предметы. В воздухе их нет.

Дальше два штриха, которые всё и решили. Чем ближе точка к поверхности, тем куб крупнее. И чем выше над полом, тем кубов реже и они мельче. В итоге у пола плотная россыпь, к потолку сходит на нет, и это читается не как декорация, а как разрушение: мир осыпается снизу.

Protos, дневник #7: пространство рвётся по швам, а один эффект я похоронил

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

Вычеркнул из плана целую задачу, потому что посмотрел на скриншот внимательно.

Считать один раз, а не при каждом запуске

Первая версия расставляла кубы на старте игры. Работало, но меня смутила арифметика.

Зона размером даже 10 на 10 на 6 метров при шаге сетки 40 сантиметров даёт около трёх тысяч запросов к физике. Каждый ищет ближайшую точку на всей геометрии вокруг. И это одна зона, а их на карте много. Всё это считается в один кадр, до того как игрок увидит картинку, и считает каждый раз ровно одно и то же, потому что стены никуда не двигаются.

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

Ровно так работает процедурная генерация в самом движке, и по той же причине.

Единственное, в чём я сомневался: сохранятся ли вообще эти кубы в уровень. В движке их массив помечен флагом «не сериализовать», и по этому флагу кажется, что данные пропадут. Полез в исходники и выдохнул: компонент пишет массив сам, вручную, бинарным блоком. Флаг стоит именно потому, что обычный механизм сохранения тут заменён на свой, более быстрый.

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

Цвет, который нельзя запечь

Кубы получились белые. Логично - материал один на всех, а зоны разного цвета.

Первым делом я сделал то, что делают все: динамический материал в рантайме, подставить цвет из зоны. Работает. И полностью ломает всю затею с запеканием, потому что динамический материал существует только пока игра запущена. В сохранённый уровень он не попадёт, а ссылка на него из сохранённого объекта - это мусор при сохранении.

Правильный инструмент назывался Custom Primitive Data. Это набор чисел, который лежит прямо в компоненте, сохраняется вместе с уровнем и подставляется в параметр материала. Ставишь галку на параметре в материале, пишешь туда цвет из кода - и всё, каждая зона своего цвета, один материал на всех, и это переживает перезапуск.

Отдельно порадовало, что задавать индекс вручную не пришлось: у компонента есть функция, которая принимает имя параметра, сама находит его в материале и вытаскивает нужную ячейку.

Трейл: четыре захода и чёрный ящик

Теперь про провал.

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

А вот как это должно выглядеть, я так и не смог сформулировать. Четыре раза пересобирал систему частиц, и каждый раз получалось не то. На четвёртый я это дело закрыл, отключил и записал в план как отложенное.

Разбор полётов вышел полезнее самой задачи. Я описывал желаемое словами: «хвост кометы», «как ракета входит в атмосферу», «неоновый след». В голове это звучит как одно и то же. На деле первые два - это плазма: размытая, объёмная, с ореолом, мягкая по определению. Третий - это свет: жёсткий, линейный, с чёткой границей. Собрать одно из другого нельзя, ровно то, что делает плазму плазмой, мешает ей быть неоном.

Вывод на будущее: для визуального эффекта сначала референс, потом код. Гифка на пять секунд решила бы за минуту то, на что я потратил вечер.

Отдельно техническая находка оттуда же. Частицы упорно рождались одной кучкой на уровне таза, вместо того чтобы идти по всему телу. Я грешил на сэмплинг скелета, на настройки доступа к данным меша, проверил лог - там чисто, ни одной ошибки.

Причина оказалась смешная. Я собирал систему из шаблона, а в шаблоне уже был свой модуль расположения частиц - он спавнит их в фигуре вокруг начала координат компонента. Компонент привязан к капсуле, центр капсулы это примерно таз. Модуль сэмплинга скелета я добавить забыл, и всё работало ровно как написано, поэтому в логе и было пусто.

Рубрика «компилируется и врёт»

Три штуки, все из этого захода.

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

Тип параметра, который не тот. В системе частиц вектор и позиция это разные типы. Код пишет вектор, а если параметр создан как позиция - значение просто не доедет. Ни ошибки, ни предупреждения. Эффект есть, работает наполовину, и понять почему невозможно.

Сообщение не того уровня. Сэмплинг поверхности скелета требует отдельной галки на меше. Если её нет, функция возвращает ноль, а движок пишет об этом в лог на уровне «просто информация», а не «предупреждение». Строка есть, но она не выделена, и глаз по ней проскакивает.

Общее у всех трёх: программа не падает и не ругается. Просто делает не то. Такие вещи ловятся только запуском и внимательным чтением лога целиком.

Меню настроек, которое я сломал за полчаса

Сделал виджет настроек: качество, разрешение, режим окна, вертикальная синхронизация. Нужен, чтобы тестеры на слабых машинах могли снизить нагрузку.

Собрал, проверил, обрадовался. За следующие полчаса довёл до состояния, в котором в лобби настройки не закрываются и висят поверх всего, а в игре не открываются вовсе.

Обе поломки, что характерно, структурные и совершенно одинаковые по природе.

Первая: я положил закрытие окна внутрь проверки на валидность. Логика была «если родительское меню есть, показать его обратно и закрыться». Родительского меню в лобби не оказалось, проверка ушла в другую ветку, и закрытие вместе с ней. Окно осталось висеть.

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

Правило из обеих: то, что должно произойти в любом случае, не кладут внутрь условия или цикла. Звучит как банальность ровно до того момента, когда сам так делаешь.

Заодно решил, каким будет мир

Долго тянул с вопросом, где всё происходит. Решил: город в трёх вертикальных слоях, лес остаётся фоном внизу и деревьев в финале будет по минимуму.

  • Подземные катакомбы: тесно и темно, здесь механика света и сканирования работает лучше всего
  • Низкая застройка в три-пять этажей: основной слой паркура
  • Высотки: вертикаль и вид для роликов

И тут же вылезла цифра, которая определяет всю архитектуру. Персонаж залезает на уступы до 275 сантиметров, выше не умеет ничего. Обычная высота этажа - 300.

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

Решение на пять сантиметров: делать этаж 250. Тогда здание проходится снизу доверху напрямую, руками, и вся вертикаль работает сама.

Это второй раз за проект, когда размеры уровня выводятся из кода персонажа, а не наоборот. Я про это писал в дневнике #5, и с тех пор только убеждаюсь: сначала посчитай, что умеет персонаж, потом строй под это мир.

Что дальше

Фундамент для боёвки: система атрибутов и эффектов, на которой потом встанут здоровье, урон и смерть. Дальше оружие. Потом отдельная карта-полигон, чтобы проверять механику не в тяжёлом лесу.

Команду я всё ещё собираю: нужен аниматор и художник по персонажам. Люди начали приходить, первое тестовое уже в работе. Денег у проекта нет и я об этом говорю сразу, зато вся сделанная работа остаётся у автора, а если игра выйдет и окупится - участники получат свою часть. Пишите в дискорд: tot_samiy

Protos, дневник #7: пространство рвётся по швам, а один эффект я похоронил

Спасибо, что читаете.

1