Пишем пошаговую PvP-арену с одновременными ходами

от автора

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



Весной 2017-ого года я наткнулся в стиме на Atlas Reactor. Игра представляла из себя какую-то дикую смесь из шахмат, покера и мобы, и такое необычное сочетание жанров меня очень зацепило. Она стала моей любимой игрой, я участвовал в онлайн-турнирах и ивентах, и всё бы было хорошо, но…

Летом 2019-ого года сервера закрыли, т.к. из-за низкой популярности игры их поддержка оказалась нерентабельной для издателя. Игра была построена по модели game-as-a-service, так что отключение серверов превратило клиенты в нерабочие куски кода.

Из-за уникального сочетания жанров похожих игр на рынке просто не было, а поиграть во что-то подобное очень хотелось.

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

В итоге я решил собрать народ и организовать работу над «духовным наследником» — сделать игру с похожими механиками, но новыми персонажами, абилками, картами и лором. Мы стали работать на чистом энтузиазме по вечерам и по выходным. Нам хотелось привнести в жанр что-то свое, добавить вариативности и глубины, исправить недочеты оригинала. С самого начала было ясно, что супер-популярной игра не станет, но у меня была уверенность в том, что хотя бы несколько тысяч людей могут ей заинтересоваться. Как минимум — фанаты закрытого Atlas Reactor-а.

Геймплей

В целом, боевка напоминает мультиплеерный XCOM, но со 100%-ными шансами попадания и героями (как в MOBA-жанре).

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

Обычно в матче участвует 8 человек. Они поделены на 2 команды по 4 человека, и каждый контролирует по одному персонажу. Однако из-за пошаговости ничего не мешает создать режим, в котором участвует всего 2 человека, и каждый контролирует всех четырех персонажей своей команды.

Игра поделена на ходы. Каждый ход поделен на две стадии — decision (принятие решений) и resolution (отображение результатов выбранных действий). Динамики добавляет то, что обе стадии происходят для обеих команд одновременно (такой тип пошаговости называется We-Go) — нет такого, что одна команда думает, а вторая — просто ждет, глядя в экран, где ничего не происходит. Вместо этого обе команды принимают решения и наблюдают за результатами одновременно. Одновременность ходов и наличие поля зрения у персонажей приводит к тому, что игру нельзя отнести к играм с полной информацией.

Decision — фаза, в которой у игроков есть непосредственная возможность выбирать желаемые действия. Игровое поле «застывает» на несколько десятков секунд, и игроки должны выбрать, какие конкретно действия подконтрольные им персонажи должны будут совершить, когда «остановка времени» закончится. Каждый игрок видит, какие действия собираются предпринять его союзники, но выбираемые противниками действия — скрыты.

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

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

Как правило, урон наносится в Blast-фазе, поэтому защитные способности срабатывают раньше — можно либо наложить щиты в Prep-фазе, либо переместиться (увернуться) в Dash-фазе. Но при увороте противник может получить урон, если пройдет через ловушку (установленную игроком в Prep-фазе). При этом если противник не будет уворачиваться, а просто останется стоять на месте — то ловушка не сработает и не нанесет ему никакого урона. То же самое и со щитами: если не атаковать защитованного персонажа, то они просто сгорят в конце хода, не оказав никакого эффекта. Интерес здесь в том, что, как было сказано выше, действия противников на момент принятия решений скрыты, а потому их приходится предугадывать.

Типичное значение урона не превосходит 35, чтобы игрокам было проще считать в уме. Статусных эффектов и способностей достаточно мало по той же причине. Основной режим игры — обычный deathmatch, идущий до 5 убийств или 20 ходов.

Реализация

Писать решил на C# (т.к. это мой основной язык), а движком выбрал Unity (т.к. опыта в создании игр у меня ранее не было, а он нативно поддерживает C# и достаточно дружелюбен к новичкам).

Пошаговые игры не требуют большого количества ресурсов, поэтому я с самого начала рассматривал наиболее бюджетные варианты для хостинга основного сервера. Была идея разместиться на heroku (т.к. это вообще бесплатно), но перезапуск приложения в рандомное время как минимум раз в день — крайне неудобно. Остановился на VPS с Linux за 45 рублей в месяц.

Учитывая ограниченность ресурсов на хостинге (особенно — очень мало места на жестком диске) и то, что вся игровая логика работает в 2D, решил сделать его на .NET Core (написав свой легковесный 2d-движок для рассчета зон поражения абилок), а Unity использовать чисто для визуализации на стороне клиентов. Это позволило достаточно просто контролировать поле зрения персонажей на стороне сервера, и отсылать клиентам только ту информацию, о которой они должны знать. Выбранные действия игроков перед обработкой на сервере проходят валидацию, что полностью исключает возможность читерить.

Для удобства игроков запилил простенький лаунчер на WinForms, который по необходимости скачивает обновленную версию клиента с Dropbox-а.

Теперь остановлюсь поподробнее на наиболее интересных (или вызвавших затруднение) моментах, с которыми мы столкнулись при разработке игры.

Порядок применения абилок

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

Например, раз в Prep-фазе можно накладывать щиты, то в ней нельзя наносить урон (иначе от id будет зависеть, пройдет урон в щиты, либо напрямую в здоровье до их наложения). Аналогично, поскольку в Blast-фазе можно наносить урон, в ней нельзя накладывать статусный эффект «Mighty», увеличивающий наносимый героем урон. Подобные правила появились для всех игровых элементов.

Простая игровая логика

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

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

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

Вторая проблема — равноценность. Допустим, мы хотим создать абилку, которая при попадании в противника наносит ему урон и автоматически рикошетит в ближайшего к нему персонажа. Проблема здесь в сочетании «автоматически» и «ближайшего». Герои располагаются на клетчатом поле, а значит рядом с первичной целью на одинаковом минимальном расстоянии может оказаться несколько персонажей.

Что же делать?

  • Рикошетить в обоих
  • Сортировать героев по алфавиту
  • Сортировать по id игрока

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

«Честная» геометрия

Как оказалось, играть с честной геометрией — неинтересно. Стены непроницаемы для большинства выстрелов и блокируют поле зрения противников, и кроме того служат укрытиями (а если урон идет со стороны укрытия, то он снижается на 50%). Однако при этом стены крайне ограничивают зону поражения для самого героя, и из-за этого игроки не видели смысла ими пользоваться.

Paint раз


Пусть красный круг — это герой, а фиолетовый квадрат — это стена. Тогда зона видимости (и поражения) — это голубые клетки. Получается, что герой не способен атаковать угол в целых 90 градусов.

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

Paint два

Так, использование дополнительных четырех точек по краям героя снизило «мертвую зону» до 60 градусов (что уже стало вполне играбельным).

В итоге стены стали действительно полезными.

Paint три


Зеленым помечены клетки, выстрелы с которых по герою нанесут только 50% урона (не со всех клеток по нему можно попасть, не имея пробивания стен на атаке).

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

Вот ссылка на гифку (20Мб), там можно посмотреть, как работает автоматический сдвиг точки выстрела.

Локализация

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

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

Грабли

Теперь — самое интересное. Несколько очевидных советов, которые могут помочь начинающим программистам, решившим пилить свой пет-проект в команде единомышленников.

  • Если есть возможность — не стоит изобретать велосипед.
    Если проблема — распространенная, то на нее должно быть много уже готовых решений. Поиск подходящего готового решения может занять меньше времени, чем написание его собственноручно с нуля (особенно, если проблема сложная).
    У меня достаточно глупо вышло с сетевым кодом. На момент начала работы у меня были базовые знания, но не было опыта их применения на практике. Я толком не разобрался в том, как стоит писать сетевой код, и что для этого есть огромное количество готовых библиотек. В результате изобрел свой велосипед с NetworkStream-ами, TcpClient-ами и сериализацией в Json. Такое решение работало, но безбожно лагало, а любые потери пакетов приводили к потере соединения. В результате пришлось потратить около 30 часов на переписывание сетевого кода (выбрал библиотеку MagicOnion, теперь все работает нормально)
  • Не надо пытаться писать идеальный код
    Качество — это замечательно, но свободное время сильно ограничено. Если переписывать один кусок по сто раз из тяги к перфекционизму — то проект не сдвинется с места. Код должен работать и быть поддерживаемым. Если за разумное время красивого решения для проблемы придумать не удается — можно воспользоваться некрасивым. Главное — такой говнокод не должен расползтись на весь проект.
  • Документация
    — она нужна, если есть планы расширять команду разработчиков. Хотя бы какие-то базовые summary для наиболее часто используемых методов и нормальный readme. Это сэкономит очень много времени в будущем.
  • Не стоит завязывать на себе все процессы в команде,
    по крайней мере если в ней больше пяти человек. Иначе вместо написания кода придется расставлять задачи для других. Разделение труда — это полезно, поэтому если нравится просто писать код — стоит именно этим и заниматься, а задачи менеджмента переложить на тех, кому они интересны.
  • Git — полезная штука,
    особенно, если в него не заливать архивы на несколько десятков мегабайт. И защиту на основные ветки навесить тоже не помешает.
  • Не стоит уделять проекту всё свободное время,
    иначе есть риск перегореть. Не стоит по ночам чинить некритичные баги вместо сна.
  • Всегда будут недовольные,
    и с этим ничего не поделать. Обязательно найдутся те, кто будут критиковать проект и поливать лично вас грязью за то, что вы, якобы, не делаете для проекта достаточно. Неконструктивную критику стоит пропускать мимо ушей, и, при желании, лишать её авторов права голоса на ваших ресурсах.

Что имеем в итоге

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

Лично я за время работы над проектом прокачал разговорный английский (т.к. коммьюнити интернациональное), набрался опыта в работе с Linux, программировании и использовании Unity. Пет-проекты — это круто.

Со временем обязательно добавим обучение для новых игроков, нормальные модельки и анимации, адекватное меню и более удобный UI в целом. Так как хорошие 3D модели стоят очень дорого, завели аккаунт на Patreon, и коммьюнити начало нас материально поддерживать.

Ссылки для тех, кого заинтересовала идея

Если есть желание присоединиться к разработке или понаблюдать за развитием нашего проекта — вот ссылка на наш Discord-сервер. Будем признательны за любую помощь.

Если понравился жанр, но в первую очередь интересует не разработка игры с нуля, а реверс-инжениринг имеющегося проекта — то вот ссылка на дискорд команды, занимающейся созданием приватного сервера для закрытой Atlas Reactor

ссылка на оригинал статьи https://habr.com/ru/post/504420/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *