Как устроены абилки в War Robots
Привет! Меня зовут Владимир Попов, и я клиентский разработчик на проекте War Robots.
War Robots существует уже несколько лет: за это время в игре появились десятки новых мехов. И, конечно, ни один из них не был бы уникальным без собственного набора способностей.
О том, как устроена и как эволюционировала система способностей в нашей игре, просто и без особых технических подробностей я расскажу в этой статье.
Для начала погрузимся в историю и посмотрим на старый вариант реализации — сейчас он уже не используется на проекте.
Старые абилки были устроены весьма тривиально: в них существовал один компонент, который вешался на робота. Это была монолитная конструкция, в которой программист полностью описывал, как работает способность: ее флоу, как и с чем она взаимодействует. Вся логика описана внутри одного компонента, который геймдизайнер мог просто повесить на робота и настроить параметры. Не было возможности поменять флоу абилки — геймдизайнеры могли изменить только параметры и тайминги.
Старая абилка могла существовать только в двух состояниях: активном и неактивном. К каждому состоянию можно было закрепить свое действие.
Рассмотрим пример абилки Jammer (англ. «помехи»). Она была в свое время, например, у робота Stalker. Работала она следующим образом:
- Если способность активна, проигрывается анимация, и робот переходит в состояние Jammer. В этом состоянии в робота нельзя целиться.
- Если способность неактивна, ничего не происходит.
- При попытке активации способности проверяется, прошло ли с момента последней активации больше n секунд.
- Деактивация происходит автоматически спустя m секунд.
Долгое время нам хватало этой функциональности. Но со временем все изменилось: и геймдизайнеров, и программистов такой подход перестал устраивать. Программистам было тяжело поддерживать такие абилки, потому что код становился монструозным — с очень длинной цепочкой наследования, где каждую ситуацию надо было описать. Геймдизайнерам не хватало гибкости системы. То есть, ради любого изменения в абилке им нужно было заказывать у программистов доработку, даже если в соседней способности существовала точно такая же функциональность.
Тогда мы поняли, что нужно что-то менять. И разработали новую систему. В ней каждая абилка стала представляться набором из нескольких связанных объектов. Функционалось разделилась на стейты абилки и компоненты стейтов.
Как это работает?
У любой абилки есть мастер. Это ее центральный объект. Он связывает остальные объекты абилки с внешним миром и наоборот. И все основные решения тоже принимает он.
Стейтов может быть любое количество. По существу стейт здесь мало чем отличается от состояния «активна»/«неактивна» в старой версии. Но теперь их может быть любое количество, а предназначение их стало более абстрактным. Одновременно у абилки может быть активен только один стейт.
Основным новшеством по сравнению со старой системой стали компоненты. Компонент описывает какое-то действие. У каждого стейта может быть любое число компонентов.
Как работают новые абилки?
Одновременно абилка может находиться только в одном из стейтов. Мастер занимается их переключением. Компоненты, которые линкуются к стейту, реагируют на активацию/деактивацию стейта и в зависимости от этого могут либо начать выполнять некое действие, либо прекратить его выполнять.
Все объекты стали настраиваемыми. Геймдизайнер может как угодно миксовать стейты и компоненты между собой и таким образом получать новую абилку из предустановленных блоков. Программисты теперь нужны только для того, чтобы создать новый компонент или стейт, что сильно облегчает написание кода. Теперь они работают с небольшими сущностями, описывают какие-то простые элементы и не собирают абилку сами — этим стали заниматься геймдизайнеры.
Флоу стал таким:
- Мастер активирует первый стейт;
- Стейт активирует все свои компоненты;
- Стейт определяет момент переключения абилки в другой стейт;
- Мастер деактивирует прошлый стейт;
- Прошлый стейт деактивирует свои компоненты;
- Мастер активирует новый стейт;
- Новый стейт активирует свои компоненты.
Впоследствии эта процедура повторяется вновь и вновь. Для простоты использования стейт является не только контейнером для компонентов, но также определяет, когда нужно переключиться в другой стейт, и запрашивает у мастера переход.
Со временем и этого нам стало мало, и схема абилки преобразовалась в следующий вид:
Мастер, стейт и компоненты остались на своих местах, но к ним добавились новые элементы.
Первым делом бросается в глаза то, что мы добавили каждому стейту и компоненту условия. Для стейтов они определяют дополнительные требования для выхода из стейта. Для компонентов они определяют, может ли этот компонент выполнить свое действие.
Контейнер зарядов (чарджей) содержит в себе заряды, перезаряжает их, останавливает перезарядку при необходимости и предоставляет заряды стейтам для пользования.
Таймер используется тогда, когда несколько стейтов должны иметь общее время выполнения, но при этом их собственное время выполнения не определено.
Важно заметить, что все объекты абилки являются опциональными. Технически для работы абилки достаточно лишь мастера и одного стейта.
Абилок, которые целиком собраны без привлечения программистов, не так много, но разработка в целом стала заметно дешевле, ведь программисты теперь пишут очень маленькие вещи: например, один новый стейт или два компонента, — остальное же переиспользуется.
Подытожим, какие составляющие части абилок у нас есть и что они из себя представляют:
- Мастер выполняет функции стейт-машины. Он предоставляет стейтам и компонентам информацию о мире, а миру — информацию об абилке. Мастер служит связующим звеном между стейтами, компонентами и служебными частями абилки: чарджами и внешними таймерами.
- Стейт слушает команды на активацию и деактивацию от мастера и, соответственно, активирует и деактивирует компоненты, а также запрашивает у мастера переключение в другой стейт. Стейт сам определяет, когда ему нужно переключиться в следующий. Для этого он пользуется своим внутренним условием: кликнул ли игрок на кнопку способности, прошло ли определенное время с момента активации стейта и др., — и внешними залинкованными в стейт условиями.
- Компонент слушает команды на активацию и деактивацию от стейта и выполняет некое действие: дискретное или длительное. Действия могут быть совершенно разными: нанести урон, похилить союзника, включить анимацию и др.
- Условие проверяет, в каком состоянии находится нужный элемент, и сообщает об этом стейту или компоненту. Условия могут быть комплексными. Стейт не запрашивает переход в другой стейт, если условие не выполнено. Компонент также не выполняет действие, если условие не выполнено. Условия — сущность опциональная, не в каждой абилке они есть.
- Контейнер зарядов содержит в себе заряды, перезаряжает их, останавливает перезарядку при необходимости и предоставляет заряды стейтам. Он используется в мультизарядных абилках, когда нужно дать игроку возможность использовать ее несколько раз, но не больше, чем n раз подряд.
- Таймер используется тогда, когда несколько стейтов имеют общее время действия, но неизвестно, сколько времени действует каждый из них. Любой стейт может запустить таймер на n секунд. Все заинтересованные стейты подписываются на ивент об окончании таймера и что-то делают, когда он заканчивается.
Теперь вернемся к схеме абилки. Как она стала действовать?
- На старте игры мастер выбирает первый стейт и активирует его;
- Стейт активирует все свои компоненты;
- Компонент проверяет выполнение условия и только после этого производит действие;
- Стейт начинает проверять условие перехода в другой стейт;
- Если условие выполняется и выполняется залинкованное в него дополнительное условие, стейт запрашивает у мастера переход в другой стейт;
- Мастер деактивирует этот стейт и активирует другой;
- Вся процедура выполняется заново.
Стейты могут использовать заряды в качестве дополнительного условия перехода. Если такой переход осуществляется, количество зарядов уменьшается. Также стейты могут использовать общий таймер. В таком случае общее время их выполнения будет определено таймером, и каждый стейт в отдельности может длиться любое время.
Мы не стали придумывать что-то совершенно новое для UI. Устроен он у нас так.
У мастера есть свой UI. В нем определяются какие-то элементы, которые должны быть в UI всегда и не зависят от того, какой стейт сейчас активен.
У каждого стейта есть своя пара в UI. UI стейта отображается только тогда, когда его стейт активен. Он получает данные о своём стейте и может тем или иным образом выводить их. Например, стейты с длительностью обычно имеют полосу и текст в своем UI, которые отображают оставшееся время.
В случае, когда стейт ждет внешней команды для продолжения работы абилки, его UI отображает кнопку. И ее нажатие отправляет команду в стейт.
Теперь разберем работу абилок на конкретных примерах. Для начала рассмотрим робота под названием Inquisitor.
Мы имеем четыре стейта, которые сменяются друг за другом. Над стейтами вы видите их отображение в UI. У двух из них вы видите компоненты, которые к ним относятся. Другие два стейта просто не имеют компонентов.
Флоу работы абилки:
- Все начинается со стейта WaitForClick. В данный момент абилка ничего не делает и просто ждет команды снаружи.
- Как только такая команда получена, мастер переключает стейты. Следующим активным стейтом становится WaitForGrounded.
- У этого стейта есть некоторые компоненты. Поэтому при его активации робот прыгает, проигрывает звук и анимацию. Помимо прочего, пока стейт активен, на робота действует эффект Jammer, который запрещает прицеливание в робота.
- При приземлении робота его абилка переходит в следующий стейт.
- В этом стейте есть три компонента: уже знакомые Sound и Jammer, а также Shake, которые вызывает потряхивание камеры у всех игроков в радиусе n.
- Поскольку этот стейт с Duration, он работает n секунд, затем абилка переходит в следующий стейт.
- Последний стейт также с Duration, но у него нет никаких компонентов: это обычный кулдаун.
- По его завершению абилка возвращается в первый стейт.
Другой пример — Phantom. Здесь многое происходит аналогично Inquisitor, но все же есть свои нюансы:
- Начинаем с WaitForClick.
- Затем Duration, в котором устанавливается телепорт, изменяются статы меха, проигрываются звук и анимация.
- После этого — DurationOrClick, в котором изменяются статы меха, проигрываются анимация и FX.
- Если был сделан клик, мы идем в еще один Duration, в котором мех телепортируется, изменяются статы, проигрываются анимация, FX и звуки.
- После этого стейта или после окончания времени у DurationOrClick мы переходим в Duration.
Основное отличие — здесь появляются стейты с ветвлением. DurationOrClick переходит в стейт a, если прошло указанное время, или в стейт b, если до этого игрок успел нажать на кнопку абилки.
Таким образом, казалось бы, наша система эволюционировала от простого к сложному, однако тем самым упростилась жизнь и программистов, и геймдизайнеров. Помощь первых теперь нужна по большей части при добавлении небольших компонентов, вторые же получили большую автономность и теперь могут самостоятельно собирать новые абилки из уже имеющихся стейтов и компонентов. При этом игроки тоже получили профит в виде более разнообразных и комплексных способностей мехов.