Повествование о том, как я успешно делаю свою игру в жанре RTS (уже не сказ)

Радуйтесь! Я Stanislaus и это моя инди-студия! На этот раз я расскажу о истории моего проекта, его текущем состоянии и будущем. И в ней не будет неожиданных сюжетных поворотов. Зато будут тевтонические рыцари верхом на гусеницам, бронеизбушки на курьих ножках и джиннобойная артиллерия.

Повествование о том, как я успешно делаю свою игру в жанре RTS (уже не сказ)

Начало

Новое начало проекта было положено в первых днях 2022 года. Я к тому времени защитил диплом и мог полностью сосредоточиться на игре. Кардинально отказываться от старых идей я не стал, это по-прежнему будет стратегия реального времени.

Исходя из прошлой работы я сделал выводы:

  • Игра на аналоговой карте (т. е. не имеющей структуры в виде регулярной сетки) выглядят гораздо реалистичнее, но расчёт игровой логики для них очень сложный и нетривиальный. Значит, надо делать игру на каких нибудь клетках.
  • Неудачное название — UltimaRatioRegum (лат. последний довод королей) значит стильно, красиво, имеет подходящий смысл, но — слишком длинное и трудно произноситься. Другой вариант — LesOiseauxDeGuerre (фр. птицы войны). Звучит приятно, импозантно и славно.
  • Надо использовать свой поиск пути, ибо он — основа любого ИИ. Только исходный код путеискателя позволит настраивать на нем все нужные параметры.
  • В прошлый раз проект был слишком связным, из-за чего при небольшом его изменении большое количество кода становилось неактуальным.

Мудро рассудив, я постановил что игра будет по-прежнему в реальном времени. В качестве основы для клеточного поля есть только три варианта: треугольники, квадраты и шестиугольники. Это фундаментальное геометрическое ограничение. Для полного замощения плоскости надо, что бы у фигур, сходящихся к одной точке, сумма углов была точно равна 360 градусов (полный круг). Я выбрал шестиугольники (в дальнейшем — гексы):

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

Работу над игрой я начал с создания модели хексы в Blender и написания скрипта для генерации поля. Координаты хексы я описал двумя int-ами и далее написал функцию для преобразования этих координат в мировые координаты сцены. После чего я нагенерировал гексагональное поле заданного размера. Ура! Впоследствии я написал библиотеку для гексагональной математики. Это был важный и, главное, безошибочный ход.

Один из первых скришнотов карты. От травы потом отказался - много ресурсов потребляет.
Один из первых скришнотов карты. От травы потом отказался - много ресурсов потребляет.

Все функции для генерации и модификации карты я разместил в одном скрипте, названном HexMapGenerator. Сами скрипты гекс хранятся в словаре благодаря чему можно мгновенно получить достукп к гексу по мировым координатам. Впоследствии я отказался от использования MonoBehavior-скриптов на гексах, вместо этого использовав обычные классы.

На карте расположены (будут) отдельные предметы ландшафта: горы, дороги, препятствия, укрепления, города и т. д. Карта планарная. Максимум из рельефа, что там будет — это визуальное смещение гексов по высоте.

Поиск пути

Для поиска кратчайшего пути между точками надо представить карту в виде взвешенного графа. Для гексагональной карты это уже почти готово и требуются лишь небольшие доработки. Для поиска пути я нашел готовый алгоритм A-star для гексагональной сетки. Он позволял находить кратчайший путь по клеткам с учетом препятствий и проходимости клеток. В дальнейшем путеискатель был сильно переработан:

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

Но главной особенностью стало изобретение Слоев Проходимости — сущности (названный LayerObstacle), которая определяет свойства юнитов по возможностям их перемещения. Например, можно запретить двум юнитам проходить сквозь один одного. Можно для различным слоев сделать разную проходимость клеток. Это имеет множество применений — обход области, которая находится под огневым контролем врага, запрет некоторым типам юнитов перемещаться по определенной местности и т. д.

Кроме нахождения маршрута для юнита у поиска пути есть и другое важное применение — поиск оптимальной точки для атаки. Алгоритм простой, но остроумный. Попробуйте придумать его сами.

И несмотря на глубокую модернизацию этого алгоритма, я по-прежнему не знаю, как он работает.

Юниты

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

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

Логику юнита я разделил на отдельные компоненты: Pawn (главный), Movable (перемещение), SensonUnit (обнаружение соседей — так и не пригодился), Ammo (оружие). Поведением юнита управлял конечный автомат (FSM). Если юниту отправить сигнал то он перейдет в соответствующий режим, если есть возможность. Система не самая удобная в использовании, но это — необходимый компромисс между надёжностью и простотой.

С этим было связано много забавных явлений. Однажды у меня один юнит отказывался двигаться. Все функции срабатывали, но он все равно стоял на месте. Потом я заметил, что установил его скорость равно нулю. Выходит, он все это время двигался, но со скоростью, равной 0 м/с… В другой раз я заметил, что из 8 пущенных ракет 1-2 не взрываются. Причину долго не мог найти, а потом оказалось, что радиус триггера у боевой части настолько маленький, что он иногда за время кадра пролетал сквозь землю. Вышло забавно, словно это были бракованные ракеты.

Отдельные явления

Выделение юнитов и гексов

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

Как видно, контур отрисовывается поверх изображения
Как видно, контур отрисовывается поверх изображения

Траектория пути

Раньше что бы показывать путь, по которому следует юнит, я использовал компонент LineRenderer, однако у него было неприятное свойство: линия всегда была повернута в сторону камеры. Вместо этого я генерирую плоский меш по координатам пути. На него натянута текстура со стрелочкой, а uv-координаты постоянно смещаются по оси Y, что создает эффект движения стрелки. Для удаления пройденного пути используется отсечение по той же оси.

Вид ползущей стрелки вызывает у меня умиротворение.
Вид ползущей стрелки вызывает у меня умиротворение.

Мини-карта

Это первый мой опыт создания мини-карты в игре. Я использовал единственно правильный способ это сделать — рисовал на текстуре при помощи низкоуровневых графических функций GL. Для преобразования координат при повороте и смещении используется ещё один материал постобработки. Для оптимизации в кадре перерисовываются только те гексы, которые были изменены по сравнению с прошлым кадром.

Повествование о том, как я успешно делаю свою игру в жанре RTS (уже не сказ)

Флаг

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

Все флаги я взял наугад. Никакого смысла они не имеют.
Все флаги я взял наугад. Никакого смысла они не имеют.

Суть игры

Разрабатываемый проект является стратегией реального времени. Игрок управляет армией, нанимает новый войска, воюет с противником и захватывает территорию. Количество контролируемых юнитов ограничено приблизительно тридцатью. Размер карты — вопрос сложный. В настоящее время она равна 50х50 клеток и вряд ли станет меньше. Возможно, немножко больше, но не кардинально. Это связано с техническими ограничениями — я не хочу вводить иерархический поиск пути и динамическое отключение гекс за пределами видимости.

Каждая клетка обладает своими свойствами, от которых зависит эффективность военных действий. Например, болото сильно замедляет войска, укрепления снижают урон войскам и т. д.

В игре важное влияние оказывает снабжение. Каждый юнит имеет свой запас еды и снарядов, которые расходуются во время боя. Для их пополнения есть объект на карте, называемый склад. Он на определенной глубине от себя пополняет дружественные войска амуницией. Сам склад не бесконечен, а тоже должен пополняется (из-за края карты или с другого склада). Для этого нужны дороги, контролируемые игроком. При необходимости его можно построить самому. Склад является физическим объектом и быть уничтожен или захвачен.

Бои направлены на позиционное противостояние и захват ключевых точек. Баланс смещен в сторону обороны благодаря возможности постройки укреплений.

Возможный список юнитов:

  • Пихоты — дальность стрельбы 2 клетки. Бонус против легких целей.
  • Бомбардиры — дальность атаки 3 клетки. Стрелки с тяжелыми ручницами. Бонус против бронированных целей. Слабы против пихоты.
  • Бронебойщики — дальность атаки 1 клетка (ближний бой). Бонус против бронированных целей.
  • Конные-кавалевийская конница. Верхом на лошадях (всадники). Дальность атаки 1 клетки. Нет бонусов или слабостей.
  • Мортира — дальность атаки 5 клеток. Бонус против укреплений.
  • Бронированные избушки — самоходная артиллерия. Дальность атаки — 5 клетки. Бонус для бездорожья. Усиленная броня.
  • Гусеничная кавалерия — дальность атаки 1 клетка. Бонус для бездорожья.
  • Джиннобойная артиллерия — катапульта, запускающая кувшин с бешеным джинном. При попадании кувшин разбивается и джинн в ярости крушит все что видит. Аналог ядерного оружия. Для своих опасен лишь немного меньше, чем для врагов. Дальность атаки — 5 клетки. Перед применением помолиться.

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

Искуственный интеллект и что с ним делать.

Для стратегий ИИ является сложный вопросом. Тактика «бежать и стрелять в сторону игрока» уже не работает и надо более серьезные решения. Поле решений конечно, но все равно слишком велико для перебора. Разумеется, сделать полноценный интеллект для бота невозможно, но этого и не требуется.

В настоящий момент я для логики я использую обычный скрипт (не MonoBehavior), с простым конечным автоматом. С его помощи я реализовал групповое перемещение юнитов к цели и групповую атаку. Этого достаточно для морального удовлетворения, но недостаточно для конечного продукта. Лучшее, что я могу сделать — это сделать адекватное поведение бота и замаскировать отсутствие ума у него. В конце концов, все разработчики так делают.

Основой любого ИИ является понимание им обстановки на карте. Для этого я создал географический анализатор. Основные его функции — поиск в ширину с различными условиями, определение оптимальной точки атаки и т. д.

Групповая атака. Старое видео.

Изначально пространство решений (количество возможных ходов) для бота крайне велико. Например, если бот имеет 30 юнитов, а каждый из них — 6 соседних клеток, то всего есть 6^30 вариантов ходов. Многовато, однако. Для борьбы с этим надо исключить заведомо неверные и неэффективные ходы. Одним из способов этого является группировка юнитов. Если управлять не каждым юнитов отдельно, а их группой то можно заметно сократить число вариантов. С точки зрения геймплея игра практически не теряет. Все помнят пословицу «Один в поле не воин»? Так вот, это оно и есть.

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

Но этого мало. Мне бы хотелось узнать, как с этими проблемами справляются другие разработчики? Есть ли какие-нибудь хитрости или секреты? В настоящее время пишу ИИ практически вслепую, а хотелось бы знать, основные подходы. В общем — мне нужна ваша помощь (а ещё донаты).

ЛОР

По части ЛОРа и персонажей я пока подробностей рассказать не могу. Скажу только сюжет игры будет посвящен гражданской войне в одной выдуманной стране. Технологии 18-19 веков, булыжники и броневичек прилагаются.

Повествование о том, как я успешно делаю свою игру в жанре RTS (уже не сказ)
3636
23 комментария

Название - это последние о чем надо думать. Это буквально логотип игры, не более.

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

А что было до? Я вот первый раз тебя вижу, а ты пишешь так, как будто все должны тебя знать.

Текст очень сумбурный, сначала ты пишешь про алгоритмы, а потом суть игры. Много воды. Видимо это последствия написания диплома, прям поток мыслей. Информации очень много, как будто стаяла задача написать туториал. Мало картинок. Это бы избавило от лишних пояснений.

4
Ответить

У меня была прошлая статья. Может. кто вспомнит.
А картинок и так больше чем, обычно у меня.

Ответить

Не читал, но одобряю.

2
Ответить

Что за... ?

2
Ответить

именно - тевтонические ? :>

2
Ответить

ДЯ!

Ответить

вопрос "а нахуя это делать в реалтайме" остается открытым

2
Ответить