Нефелия: как я провёл это лето

когда-нибудь мы осилим и нормальное объёмное освещение, но пока у нас есть красивый метеозонд, что тоже неплохо
когда-нибудь мы осилим и нормальное объёмное освещение, но пока у нас есть красивый метеозонд, что тоже неплохо

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

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

Туман

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

А вот с ассетами Occa Software пока куда приятнее взаимодействовать. Правда, сначала было непонятно, зачем 2 отдельных ассета: Buto и LSPP примерно для одного и того же объёмного тумана.

В ранних версиях lspp давал похожий на тот же Buto эффект, пусть и без клубящегося тумана, поэтому я решил, что оба одновременно в итоге не нужны. Но после очередного обновления на шоукейсе внезапно понял, что игроки больше не понимают, когда они находятся в облаке, а когда — нет. Подумал, что что-то отвалилось по моей вине, но оказалось, просто замыленным взглядом не увидел, что теперь LSPP это именно для Godrays и больше ни для чего, а вот чтобы сгустить атмосферу, нужен уже Buto. В итоге в отдельной ветке с 3d-облаками добавили и его тоже: по-моему, получилось неплохо.

Правда, по умолчанию там включены тяжелые опции, на старушке GTX 1060 роняющие fps до 40 кадров в секунду, но если внимательно пройтись по настройкам, можно подобрать почти ничего не уменьшающую конфигурацию.

Ветер

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

Для показа, куда дует ветер рядом с нами, используется простая система частиц пыли, спавнящаяся рядом с дирижаблем игрока.
Для показа, куда дует ветер рядом с нами, используется простая система частиц пыли, спавнящаяся рядом с дирижаблем игрока.

Кроме постоянно присутствующего глобального ветра, есть ещё отдельные "клетки" с направленным ветром, который можно увидеть издали и использовать для перемещения в нужную сторону:

Возможно, позже, когда сможем адаптировать какой-нибудь из чужих ассетов с трёхмерными облаками к нашему спавнеру, получится сделать по-настоящему 3d не-кучевые облака, перемещающиеся по ветру. Но пока хватает и таких.

Прогресс

Пока от MVP нас по-прежнему отделяют 5 не реализованных механик:

  • боёвка
  • навигация
  • случайные события в полёте
  • апгрейд дирижабля
  • найм новых облачников

По тому, что уже более-менее готово (сам полёт и его UI, торговля, сбор ресурсов в облаках, текстовые квесты и несколько более мелких механик), получили много ценного фидбека на шоукейсах, а недавно на Gamedev Weekend даже получили приз за лучшую 3d графику.

Сейчас я занимаюсь ИИ NPC-дирижаблей, а второй программист, Сергей, присоединившийся к команде не очень давно, написал с нуля шейдер для трёхмерных облаков, которые сейчас активно дорабатываются.

Рефакторинг

Переход к боёвке (да и привлечение другого программиста) был бы невозможен, если бы зимой я не потратил почти 3 месяца на переписывание большей части архитектуры.

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

Когда я начинал работать над этим проектом, Unity только-только выкатило ECS, и хотелось сразу перейти на него. Но тогда (как, в принципе, и сейчас), это было совершенно не Production Ready (в моём случае не получилось прикрутить систему управления камерой Cinemachine), поэтому я решил пока писать, как придётся, чтобы когда-нибудь перейти всё-таки на DOTS.

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

Решил не сильно заморачиваться с архитектурой и взять MVP, как я его понимаю:

  • модель это обёртка вокруг объекта данных, которая может быть прокинута через систему иньекции зависимостей (Zenject в моём случае), и которая посылает сообщение об изменении этих данных всем подписчикам. Модели могут быть и "синтетическими", т.е. сами подписываться на другие модели и отправлять уже своим подписчикам производную от них информацию в момент, когда в какой-то из нижележащих моделей что-то изменилось
  • View это Monobehaviour, который сам по себе ничего не делает, но предоставляет презентеру нужные данные и события, инициированные самим движком
  • Презентер создаётся через zenject'овскую же шаблонную фабрику и получает данные из модели и View, на их основании предпринимает какие-то действия, меняющие и то, и другое

Базовая модель выглядит примерно так:

using System; namespace MVP { public abstract class Model<T> { protected T ValueInternal; public virtual T Value { get => ValueInternal; set { if (IsEqual(value)) return; ValueInternal = value; if(IsInitialized()) OnChanged?.Invoke(ValueInternal); } } protected void ManualUpdateTrigger() { OnChanged?.Invoke(Value); } protected bool IsEqual(T newValue) { return ValueInternal != null && ValueInternal.Equals(newValue); } public virtual bool IsInitialized() { return ValueInternal != null; } public event Action<T> OnChanged; public void SubscribeOrGet(Action<T> callback) { if (IsInitialized()) { callback?.Invoke(Value); } else { OnChanged += callback; } } public void SubscribeAndGet(Action<T> callback) { if (IsInitialized()) { callback?.Invoke(Value); } OnChanged += callback; } } }

В итоге в основном за счёт моделей удалось сделать код, связанный с логикой дирижабля, ничего не знающим о самом движке = беспроблемно тестируемым edit mode юнит-тестами. Сейчас даже бой между 2 дирижаблями уже есть на уровне 2 сражающихся моделей "в вакууме", хотя с искусственным интеллектом перемещений всё-таки пошёл по более простому пути. После этого получилось изолировать инпут от его обработки, т.е. модели стало всё равно, игрок нажимает на кнопки, или ИИ посылает сигналы поворота/сброса балласта и т.д.

Для самого ИИ сейчас пишу набор иерархических состояний (на верхнем уровне это стояние на месте, следование по маршруту, сражение и бегство), возможно, удастся обойтись без GOAP, а возможно, грядёт ещё одно переписывание, если окажется, что такое сложное поведение проще будет представить иначе.

Ролевая система

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

  • стилеты. Грубая сила, воля или угроза их применить. В полёте пробрасывается при атаке, в диалогах при попытке надавить на собеседника или наоборот, при отражении чужой агрессии
  • монокли. Логика, считывание чужих долгосрочных стратегий и логических нестыковок в аргументах. В полёте применется при построении прогноза погоды и пробрасывании шанса обнаружить опасность прежде, чем она обнаружит нас. В диалоге применяется при диспутах и попытках понять истинные мотивы собеседника или другого человека
  • шестерни. Умение действовать в моменте, быстро реагировать, ловчить. В полёте применяется при ремонте. В диалоге - при проверках на возможность застать оппонента врасплох (в любом смысле), обмануть или найти экспромтом какое-то временное решение проблемы
  • кубки. Умение понять собеседника, увидеть мир с его стороны и найти решение, которое устроит всех. В полёте применяется при бросках лечения, в диалогах при попытке эмоционально сблизиться с собеседником, втереться в доверие

Офлайн

За это лето удалось поучаствовать во множестве мероприятий: фестиваль Indie varvar's под открытым небом, а также на Celestial Beasts в ИТМО + уже упомянутый Gamedev Weekend в Новосибирске и недавно были на шоукейсе Индикатора. Кроме того впервые увидели настоящий дирижабль над Ратной палатой.

Что дальше?

Надеюсь, что до конца года выпустим следующую версию демки со случайными событиями и начальной реализацией боёвки (или хотя бы с чем-то из этого). До следующей весны планируем завести страницу в Steam, а также Boosty или что-нибудь ещё краудфандинговое. Кроме того, очень вялотекуще (но после того, как дорастём до MVP, будем делать это более решительно) ищём инвестора.

Спасибо за то, что дочитали до конца:)
Спасибо за то, что дочитали до конца:)
29
2 комментария

Люблю такой сетинг, если попадется на глаза следующая демка то попробую

1
Ответить

Крутые! Подписался, буду следить!

Ответить