Gameplay Ability System (далее GAS) – это плагин-фреймворк, позволяющий создавать способности и атрибуты на манер РПГ или MOBA в вашей игре. Основные его положительные особенности – поддержка мультиплеера из коробки и высокая гибкость. Недостатки выделить довольно сложно, тем более что альтернатив-то, по сути, и нет. Однако проблема, с которой, практически наверняка, столкнется любой, кто начнет разбираться что это за GAS такой – довольно высокий порог вхождения, то есть объем знаний, которые необходимы чтобы хоть как-то начать применять плагин в практических задачах. И дальше интересующийся попадает на развилку: часовые видео "Intro to GAS" или документация. Тут-то и может пригодиться эта статья, в которой в обзорном формате рассмотрены все основные сущности, о которых необходимо знать для начала работы. Это позволяет читателю за минимальное время ознакомиться с базой, после чего уже намного осмысленнее и более точечно изучать документацию или другие туториалы.
Структурно статья содержит небольшую вводную часть о том, что вообще такое GAS, основную часть с обзором плагина, а также, для наглядности, небольшой пример-туториал, демонстрирующий практическое применение рассмотренных сущностей. Итак
Кто такой этот ваш GAS?
Чтобы понять, зачем вам вообще может понадобиться GAS, представьте любую знакомую вам РПГ или MOBA игру. В ней обязательно будут такие элементы как характеристики персонажей (статы), их способности (активные и пассивные), эффекты, которые накладывают эти способности и которые влияют на статы (самый банальный пример – нанесение урона). Всё это вместе с многочисленными инструментами, позволяющими реализовать, по сути, любую механику, предоставляет GAS. И всё это, как я уже упоминал, с поддержкой мультиплеера из коробки, что существенно снижает количество работы программистов.
Однако же, если взглянуть немного шире, то эту же концепцию можно расширить на проект любого жанра. Статы и эффекты, влияющие на них присутствуют хоть в слэшере, хоть в спортивном симуляторе. А если ваша игра еще и подразумевает наличие мультиплеера, то наиболее вероятно GAS – ваш выбор. Естественно, я не агитирую бездумно использовать GAS в любом проекте. Где-то это может быть избыточно. Тем не менее, этот плагин настолько универсален, что представить что в какой-то игре GAS использовать принципиально нельзя – довольно тяжело. Поэтому, на мой взгляд, для любого разработчика на Unreal Engine знакомство с GAS будет не лишним. Тем более что в UE5 он включен в движок по умолчанию.
Перед тем как перейти к основной части, я бы хотел поделиться крайне важной, на мой взгляд, ссылкой – это неофициальная документация от tranek. Она содержит исчерпывающее описание всех аспектов GAS и, согласно моему опыту, если в ней нет ответа на ваш вопрос, то, вероятнее всего, этот ответ придется искать непосредственно в коде плагина. Сейчас уже появилась официальная документация, но за более чем два года работы с GAS для себя я выработал паттерн "если всплывает какая-то непонятная проблема, смотрим, не написано ли про это у tranek'а" и пока этот метод не подводил. На некоторые места из этой документации я буду ссылаться и в этой статье.
Ну а теперь, наконец, перейдем к сущностям GAS. Начнем, разумеется, с
Ability System Component
UAbilitySystemComponent
, он же ASC
– “сердце” GAS. Это актор-компонент, который отвечает за все взаимодействия с ability-системой. Соответственно, любой актор, который имеет атрибуты, способности, может нести или накладывать эффекты, должен иметь этот компонент и именно через него он будет эти способности и эффекты использовать, накладывать и получать.
Атрибуты (Attributes)
Атрибут – это любое связанное с геймплеем численное свойство актора.
Здоровье, мана, восстановление здоровья, сила, ловкость, удача, количество слотов в инвентаре… думаю, вы поняли.
Атрибуты задаются структурой FGameplayAttributeData
. Каждый атрибут, по сути, состоит из двух значений: базового (BaseValue
) и текущего (CurrentValue
). Базовое – это постоянное значение, задающееся в начале игры (например, из конфига для конкретного объекта), а текущее – это базовое значение с наложенными модификаторами от действующих геймплейных эффектов (про Gameplay Effects
и их типы – ниже). Базовое значение может изменяться в процессе игры за счет применения мгновенных геймплейных эффектов. Отсюда вывод: любое изменение атрибутов предполагается производить через наложение эффектов, а не напрямую. Это также необходимо для корректной работы предсказаний изменений, что важно при игре по сети.
Например, временный эффект может увеличить урон в 2 раза относительно базового значения. По окончании действия эффекта, величина урона вернется к базовому значению. Однако мгновенный эффект нанесения урона изменяет сразу базовое значение здоровья на минус величину урона
Еще один важный аспект – изменение атрибутов. На них может быть завязана как геймплейная логика, так и чисто визуальная (например, отображение в HUD’е). Для отслеживания изменений при объявлении атрибута есть возможность подписаться на соответствующий делегат, предоставляемый ASC
:
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &ClassName::HealthChanged);
Где функция HealthChanged
должна быть объявлена в виде
void HealthChanged(const FOnAttributeChangeData& Data);
Все атрибуты персонажа хранятся и обрабатываются в наборе атрибутов (Attribute Set).
Attribute Set
UAttributeSet
инициализирует атрибуты, хранит их значения и обрабатывает их изменения. Он же ответственен за их репликацию. Обработка атрибутов может также включать геймплейную логику. Например, определение персонажа как мертвого, если его здоровье ниже или равно нулю. Сам Attribute Set хранится в ASC
, причем ASC
может иметь как один, так и несколько наборов атрибутов.
Например, если некоторые юниты в игре имеют здоровье и ману, а другие только здоровье, то можно ограничиться одним набором, хранящим все атрибуты, связанные и со здоровьем, и с маной, и просто не использовать некоторые из них, а можно завести два раздельных набора и выдать первому типу юнитов оба, а второму только набор, отвечающий за здоровье
Объявление атрибутов в AttributeSet
Перед объявлением атрибутов, в .h файле своего сета необходимо объявить макросы
// Uses macros from AttributeSet.h
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
Реализацию макросов можно посмотреть в
AttributeSet.h
Они позволяют автоматически генерировать геттер, сеттер и иниттер для атрибутов. Таким образом, для объявления атрибута достаточно следующего кода:
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Health)
После чего, для атрибута сразу будут доступны функции GetHealth()
, SetHealth()
и InitHealth()
.
Изменять атрибуты, как уже было сказано выше, предполагается при помощи геймплейных эффектов, так что давайте посмотрим на них поближе.
Gameplay Effect
UGameplayEffect
, он же GE
– это основной способ взаимодействия обладателей ASC
друг с другом.
Изменение здоровья (урон или лечение), ускорение, невидимость, quadruple damage – всё это эффекты, накладываемые на обладателя ASC и меняющие те или иные его атрибуты
GE
может быть одного из трех типов: мгновенный (Instant), временный (Duration) и бесконечный (Infinite). Мгновенные применяют перманентные изменения к базовому значению атрибута. Временные и бесконечные накладывают временные изменения на текущее значение атрибута, при этом также выдавая теги (GameplayTags
), соответствующие действующему эффекту. И временный, и бесконечный эффекты могут быть сняты вручную, а их отличие заключается в том, что временный заканчивается автоматически через заданное время, в то время как бесконечный действует до тех пор, пока не будет снят.
Пример: уменьшение здоровья вследствие получения удара оружием – это мгновенный эффект, кровотечение, полученное при этом – временный. Оно может как закончиться само через некоторое время, так и быть завершено, например, применением бинта. А вот дебафф на скорость атаки, полученный из-за повреждения руки – это бесконечный эффект, который будет длиться до, например, посещения лазарета в лагере.
Класс UGameplayEffect
предполагается использовать как data-only класс, то есть при создании нового эффекта, нужно лишь заполнить все поля класса желаемым образом, но не добавлять никакой дополнительной логики в него.
Настройки Gameplay Effect'ов
Кратко пройдемся по доступному функционалу:
Duration Policy – тип эффекта: Instant, Duration или Infinite. Для Duration и Infinite доступна настройка периодичности. Если указан период, то эффект будет срабатывать как Instant раз в период на протяжении времени действия.
Duration Periodic эффект "кровотечение" с периодом 1 сек и длительностью 10 секунд будет наносить урон раз в секунду в течение 10 секунд
Components. Они определяют что делает GE
при применении. Например: наложение, снятие или блокировка тега на цели, применение или снятие другого эффекта, выдача способности на время действия и так далее. Обычно доступных по умолчанию вариантов вполне достаточно, но при желании можно создать и кастомные компоненты, наследуясь от UGameplayEffectComponent
.
Modifiers – единичные изменения атрибутов. Для каждого Modifier’а нужно указать атрибут, способ его изменения (добавление, умножение, замена значения), величину изменения (может быть как постоянной, так и зависеть от других атрибутов или передаваться как параметр при наложении GE
), а также требования к тегам ASC
-источника и ASC
-цели, которые очень удобно использовать для создания системности геймплея.
Например, персонаж не может использовать способности, если у него есть тег
State.Dead
, а эффект горения не применится на цель с тегомImmunity.Fire
Executions – самый мощный способ которым GE
может повлиять на ASC
. Он позволяет изменять сразу несколько атрибутов по произвольным правилам. Для этого необходимо создать отдельный класс, наследованный от UGameplayEffectCalculation
. Этот класс может “захватывать” указанные атрибуты, то есть иметь к ним доступ и возможность отслеживать их изменения. Таким образом появляется возможность рассчитывать модифицированное значение изменяемых атрибутов в зависимости от различных факторов, в том числе от значений других атрибутов. У этого инструмента есть ограничения. Например, отсутствие возможности предсказания и доступность только для Instanced и Periodic эффектов.
По моему опыту, для абсолютного большинства эффектов достаточно использовать Modifiers: увеличение скорости на x% (бег), увеличение сопротивления к урону и всё в таком духе. Типичный случай, когда Execution может быть необходим – расчет урона. Он вполне может зависеть от большого количества факторов, причем не обязательно линейно: сопротивление урону цели, увеличение урона у источника, шанс критического урона, шанс уворота, наличие блока и так далее и так далее.
В контексте UGameplayEffectCalculation
также стоит упомянуть про Modifier Magnitude Calculations (UGameplayModMagnitudeCalculation
). Этот класс наследуется от UGameplayEffectCalculation
, несколько менее мощный, но зато может быть предсказан при игре по сети, а также может быть использован для эффектов с любой Duration Policy. Основная цель класса – вернуть вычисленное значение атрибута. Аналогично UGameplayEffectCalculation
, для этого могут захватываться другие атрибуты и выполняться произвольные манипуляции с ними.
Gameplay Cues. GC
– еще одна сущность GAS
, предназначенная для визуализации действия эффектов. Они не должны нести геймплейной логики. Подробнее про них поговорим в следующем разделе, а здесь ограничусь тем, что отмечу, что каждому GE
можно привязать несколько GC
.
Stacking. Отдельная категория настроек посвящена возможности сложения действия эффекта. Стаки могут накапливаться на источнике или же на цели. Среди настроек – максимальное количество стаков, а также способы обновления, сброса и исчезания стакающегося эффекта.
Применение GE
осуществляется при помощи следующих функций (естественно, они доступны и в C++):
Применить эффект можно “To Target”, указав ASC
-цель, а также “To Self”. В последнем случае ASC
-цель совпадает с ASC
-источником. Кроме этого, применить можно эффект, непосредственно указав его класс, либо же используя GESpec
(FGameplayEffectSpec
).
GESpec
– это структура, содержащая информацию о конкретном инстансе эффекта: его класс, источник, цель, уровень и так далее. В отличие от самого Gameplay Effect, GESpec
можно свободно создавать и модифицировать в рантайме. GESpec
может использоваться как для создания новых эффектов с заданными характеристиками, так и для хранения информации об уже примененных эффектах. Одной из важных возможностей GESpec
является наличие SetByCaller параметров, которые можно процедурно добавлять в спек и которые затем могут использоваться в самом GE
. Таким образом, появляется возможность создавать универсальные эффекты, силу действия которых можно настраивать параметрически.
Перед тем как перейти к способностям, в честь которых, собственно, назван плагин, рассмотрим Gameplay Cues
, которые визуализируют действие эффектов.
Gameplay Cue
Gameplay Cues
(GC
) – это сущность для всех негеймплейных вещей, связанных с эффектами: звуки, VFXы, тряска камеры и так далее. GC
обычно реплицируются и предсказываются.
GC
бывают двух существенно различных типов: GameplayCueNotify_Static
и GameplayCueNotify_Actor
UGameplayCueNotify_Static
наследуется от UObject
и используется для одноразовых мгновенных эффектов (например, визуальный эффект при ударе оружием). Этот вид GC
не инстанцируется (то есть работает в CDO) и может быть использован для Instant и Periodic эффектов (как вы, вероятно, помните, Periodic – это повторение Instant эффекта с некоторым интервалом). Соответственно, при помощи таких GC
можно проиграть звук, заспавнить эффект или выполнить какое-то другое мгновенное действие.
AGameplayCueNotify_Actor
– это уже Actor. Эти GC
инстанцируются и могут выполнять какую-то логику в течение некоторого времени (пока не будут сняты). Они применяются для Duration или Infinite эффектов и снимаются либо при завершении связанного эффекта, либо вручную. Соответственно, пример таких GC
– длительное горение, эффект ускорения, визуализация каких-либо аур и всё такое.
Каждый GC
должен иметь указанный тег, который обязательно должен начинаться с GameplayCue.
.
Например,
GameplayCue.Elemental.Fire.Burning
Этот тег указывается при добавлении GC
и именно его нужно указать в настройках GE
чтобы привязать GC
к эффекту. Gameplay Effect
, в котором настроены GC
добавляет их автоматически. Также GC
можно добавить (или снять) вручную при помощи следующих функций:
Функции Add...
и Remove...
используются для добавления и снятия GC
типа _Actor
, а Execute...
– для _Static
.
Для работы с GC
необходимо переопределять функции OnExecute()
, OnActive()
и OnRemove()
. Первая срабатывает когда выполняется Static GC
, а вторая и третья, соответственно, когда Actor GC
добавляется и убирается. То есть именно в эти функции нужно добавлять логику, которая будет выполняться GC
. Также отмечу, что поскольку AGameplayCueNotify_Actor
является актором, в нем можно переопределять и другие методы AActor
’а, например, Tick()
.
А теперь, наконец, про сами способности.
Gameplay Abilities
Способность (UGameplayAbility
) – это любое действие или навык, которое может быть выполнено актором.
Спринт, прыжок, стрельба из оружия, использование крюка-кошки, выпивание эликсира силы, пассивный блок атаки каждые X секунд, открытие двери и карабканье по лестнице – всё это Gameplay Abilities.
Соответственно, одновременно у одного носителя ASC
могут быть активны несколько способностей. Также стоит отметить, что в отличие от GE
, GA
НЕ является data-only классом и может (скорее даже должна) содержать логику работы.
Каждый актор может использовать только способности, которыми он (посредством ASC
) обладает. То есть прежде, чем активировать способность, необходимо её выдать. Сделать это можно, например, при помощи функции
FGameplayAbilitySpecHandle UAbilitySystemComponent::GiveAbility(const FGameplayAbilitySpec& AbilitySpec);
Как мог заметить внимательный читатель, для выдачи способности используется структура
FGameplayAbilitySpec
, которая является аналогомFGameplayEffectSpec
для способностей
Активировать имеющуюся способность можно, например, функцией
bool UAbilitySystemComponent::TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation = true);
Вообще, способов выдачи и активации способностей несколько больше (разные варианты лучше подходят для разных ситуаций), но описывать их все в рамках данной статьи мне видится излишним и малополезным.
Схема работы простой Gameplay Ability
Чтобы разобраться что вообще делает GA
, посмотрим на пример работы простой Gameplay Ability
из упомянутой выше документации:
При вызове TryActivateAbility()
, при условии, что ASC
обладает способностью, переданной в параметрах, выполняется проверка CanActivateAbility()
которая проверяет, может ли способность быть активирована. По умолчанию, CanActivateAbility()
проверяет валидность основных сущностей, связанных со способностью, а также некоторые дефолтные вещи, такие как соответствие тегов (про это подробнее чуть ниже).
Если проверка пройдена успешно, способность активируется и вызывается соответствующий ивент ActivateAbility()
, который уже выполняет непосредственно логику способности. Логика может быть произвольной, но есть несколько элементов, которые стоит подсветить отдельно:
CommitAbility()
– метод, отвечающий за использование ресурсов способностью. Он должен быть вызван вручную (не обязательно в начале выполнения способности) и пытается потратить ресурсы игрока, необходимые для использование способности (например, мана). Метод возвращает bool, характеризующий успешность. Если Commit не удался (например, недостаточно маны) – способность следует отменить.
Далее идет основная логика способности, которая может быть произвольной. Для завершения способности необходимо вызвать EndAbility()
в случае штатного завершения логики либо же CancelAbility()
, если способность была прервана по какой-либо причине. В целом, эти методы весьма схожи но использование CancelAbility()
может быть полезно чтобы развести логику завершения способности на “корректную” и “не корректную”.
Про настройки Gameplay Ability
Немного поговорим про настройки GA
. По ним пройдемся поверхностно, поскольку информации там хватит на отдельную статью. За подробностями, как и ранее, отсылаю к документации tranek.
Во-первых, это те самые теги способности. Они разбиты на кучу категорий и совокупно позволяют задать практически любые правила по которым способность может или не может быть использована.
Например, AbilityTags
: это теги, которыми обладает способность и которые описывают её; CancelAbilitiesWithTag
: способности с этими тегами будут отменены при активации текущей способности; BlockAbilitiesWithTag
: способности с этими тегами нельзя активировать пока активна текущая способность и так далее. Про все рассказывать смысла особого не вижу, поскольку их описания говорят сами за себя.
Во-вторых, это набор сетевых настроек (були и енумы). Опять же, подробный рассказ о них всех рискует вырасти до неприличных размеров, так что просто приведу их список.
Отдельно подчеркну, что при создании сетевой игры с использованием GAS
нужно тщательно за всем этим следить, потому что дебажить потом сетевое взаимодействие в поисках проблемы – удовольствие малоприятное.
В-третьих, стоимость и кулдаун способностей. Данные механики реализуются при помощи особых GE
, указываемых в соответствующих полях CostGameplayEffectClass
и CooldownGameplayEffectClass
. Cost GE
– это Instant эффект, который снимает заданное количество некоторого атрибута (опять же банальный пример – мана), а Cooldown GE
– Duration эффект, который висит на обладателе ASC
заданное время и блокирует повторное применение способности.
При активации способности ( UGameplayAbility::Activate()
) вызываются функции UGameplayAbility::CheckCost()
и UGameplayAbility::CheckCooldown()
, которые проверяют, соответственно, наличие нужного количества атрибута и отсутствие кулдаун-эффекта. При невыполнении этих требований способность не активируется.
Для списания стоимости способности и применения кулдауна можно вызвать GameplayAbility::CommitCost()
и UGameplayAbility::CommitCooldown()
. Эти функции могут быть использованы по отдельности (если необходимо выполнить их в разное время), либо же можно просто вызвать UGameplayAbility::CommitAbility()
, которая вызывает обе эти функции.
Про пассивные способности
Для создания пассивной способности понадобится оверрайднуть метод UGameplayAbility::OnAvatarSet()
, выполнив в нем TryActivateAbility()
. Таким образом, способность сразу активируется и будет выполнять свою логику. Для разделения активных и пассивных способностей можно, например, ввести переменную bool bActivateAbilityOnGranted
, определяющий, нужно ли активировать способность в OnAvatarSet()
void UGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec)
{
//Projects may want to initiate passives or do other "BeginPlay" type of logic here.
}
GA
выполняются за один тик. Для того, чтобы выполнять логику, растянутую во времени, используются Ability Tasks
. Таски выполняют определенные действия и в заданный момент вызывают делегаты, к которым можно привязать часть логики способности.
Например, таска PlayMontageAndWait()
позволяет проигрывать Anim Montage
, при этом привязав требуемую логику к окончанию или прерыванию анимации.
Пин Async Task
позволяет сохранить таску в переменную. Это может быть полезно, например, чтобы досрочно её прерывать.
Для целей вашей игры можно создать любую кастомную Ability Task
, выполняющую нужные действия или проверки и вызывающую нужные делегаты. Например, WaitReceiveDamage()
, при помощи которой можно привязать какую-либо логику к получению урона.
Собственно, на этом оканчивается обзор основных сущностей GAS
. На мой взгляд, описанного достаточно, чтобы иметь возможность начать читать документацию с любого раздела и примерно понимать о чем идет речь. Далее в статье приведен пример практического применения GAS
, изучения которого должно быть достаточно еще и для начала самостоятельной работы с плагином.
Пример
Небольшой дисклеймер: я предполагаю, что если читатель взялся за изучение GAS
, то с базовой работой в движке он уже знаком. Поэтому подробные инструкции вида "куда нажать чтобы создать новый класс" я буду опускать, останавливаясь лишь на важных моментах.
Итак, реализуем следующую РПГ-стайл механику: персонаж с атрибутами "здоровье" и "мана" может использовать способность, наносящую урон другим персонажам в некотором радиусе. Также он может применить на себя усиление, увеличивающее исходящий урон от способностей в два раза.
За основу был взят дефолтный ThirdPerson unreal проект. Выглядит это всё следующим образом:
Со вводной частью всё. Поехали кодить!
Для начала создадим новый AttributeSet
. Я свой, не мудрствуя лукаво, назвал ExampleAttributeSet
. Добавляем в него макросы, про которые я рассказывал в соответствующем разделе, объявляем атрибуты "здоровье", "максимальное здоровье", "мана", "максимальная мана" и "множитель урона". Полный код под спойлером.
ExampleAttributeSet.h
#include ...
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
UCLASS()
class GASEXAMPLE_API UExampleAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, Category = "Attributes | Stats")
FGameplayAttributeData Health = 100.f;
ATTRIBUTE_ACCESSORS(UExampleAttributeSet, Health)
UPROPERTY(BlueprintReadOnly, Category = "Attributes | Stats")
FGameplayAttributeData MaxHealth = 100.f;
ATTRIBUTE_ACCESSORS(UExampleAttributeSet, MaxHealth)
UPROPERTY(BlueprintReadOnly, Category = "Attributes | Stats")
FGameplayAttributeData Mana = 100.f;
ATTRIBUTE_ACCESSORS(UExampleAttributeSet, Mana)
UPROPERTY(BlueprintReadOnly, Category = "Attributes | Stats")
FGameplayAttributeData MaxMana = 100.f;
ATTRIBUTE_ACCESSORS(UExampleAttributeSet, MaxMana)
UPROPERTY(BlueprintReadOnly, Category = "Attributes | Damage")
FGameplayAttributeData DamageMultiplier = 1.f;
ATTRIBUTE_ACCESSORS(UExampleAttributeSet, DamageMultiplier)
};
Для простоты, значения атрибутов я инициализирую сразу, но в настоящем проекте дополнительно может присутствовать метод, выполняющий инициализацию атрибутов любым другим способом (из DT
или внешней конфиг-таблицы) и вызывающийся при инициализации персонажа.
Далее создаем свой новый ASC
и в конструкторе добавляем в него наш AttributeSet
.
ExampleAbilitySystemComponent
//ExampleAbilitySystemComponent.h
#include ...
UCLASS()
class GASEXAMPLE_API UExampleAbilitySystemComponent : public UAbilitySystemComponent
{
GENERATED_BODY()
UExampleAbilitySystemComponent();
}
//ExampleAbilitySystemComponent.cpp
UExampleAbilitySystemComponent::UExampleAbilitySystemComponent()
{
AddSet<UExampleAttributeSet>();
}
Опять же, добавлять AttributeSet
можно и в других местах. Например, в той же инициализации персонажа. Причем, в таком случае, на персонаже можно указать какие именно сеты добавлять.
В нашем персонаже AGASExampleCharacter
заведем массивы способностей и эффектов, которые будут выданы персонажу и наложены на него в начале игры. И, собственно, на BeginPlay()
выдаем эти способности и применяем эффекты.
AGASExampleCharacter
//AGASExampleCharacter.h
class AGASExampleCharacter : public ACharacter
{
//...
//Abilities that will be granted to character on game start
UPROPERTY(EditDefaultsOnly, Category = "Abilities")
TArray<TSubclassOf<UGameplayAbility>> GAsToGrant;
//Effects that will be applied to character on game start
UPROPERTY(EditDefaultsOnly, Category = "Abilities")
TArray<TSubclassOf<UGameplayEffect>> GEsToApply;
//...
}
//AGASExampleCharacter.cpp
void AGASExampleCharacter::BeginPlay()
{
//...
if (!ASC)
return;
for (TSubclassOf<UGameplayAbility>& GAClass : GAsToGrant)
{
FGameplayAbilitySpec GASpec = FGameplayAbilitySpec(GAClass);
GASpec.SourceObject = this;
ASC->GiveAbility(GASpec);
}
for (TSubclassOf<UGameplayEffect>& GEClass : GEsToApply)
{
FGameplayEffectSpec GESpec = FGameplayEffectSpec(GEClass->GetDefaultObject<UGameplayEffect>(), ASC->MakeEffectContext());
ASC->ApplyGameplayEffectSpecToSelf(GESpec);
}
}
Переходим в блюпринты и создаем виджет для статов персонажа и виджет-компонент, который повесим на персонажа. Подписываем его на изменения здоровья и маны овнера.
WBP_StatsWidget и BP_StatsWidgetComp
Начнем с виджета. Заводим UserWidget
и добавляем туда Progress Bar
'ы для здоровья и маны:
Заведем в нём функции для обновления значений в обоих ProgressBar
'ах:
Создаем WidgetComponent
, указываем у него в Widget Class виджет, созданный на предыдущем шаге и подписываемся на обновления здоровья и маны:
Для отслеживания изменения атрибута в данном случае мы использовали AbilityTask
'у WaitForAttributeChange.
Можно было воспользоваться напрямую функцией GetGameplayAttributeValueChangeDelegate
, которую я упоминал в разделе про атрибуты, но это потребовало бы немного больше заморочек в плюсовой части проекта. А эта таска делает буквально то же самое (в чем любопытный читатель может убедиться, открыв её код), при этом еще и наглядно демонстрируя как можно использовать AbilityTask
'и.
Теперь настроим необходимое управление. Хоть это и не относится к GAS
, для полноцнности гайда, быстро пробежимся по этому этапу
Настройка управления
Для начала создаем два InputAction
'а с триггером Pressed
и добавляем их в Input Mapping
, уже присутствующий в проекте. Назначаем этим действиям инпуты, у меня это клавиши F
и R
.
Читателям, не знакомым с тем, что такое все эти инпут экшены и маппинги, настоятельно рекомендую ознакомиться с Enhanced Input плагином, поскольку в UE5 это основной способ взаимодействия с инпутом.
Далее в нашем персонаже AGASExampleCharacter
заводим поля, в которые назначим наши InputAction
'ы (по аналогии с полями, которые там присутствуют изначально)
/** Use damage spell Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* UseSpellAction;
/** Apply increased damage buff Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* ApplyBuffAction;
и функции-реакции на инпуты
UFUNCTION(BlueprintImplementableEvent)
void UseSpell(const FInputActionValue& Value);
UFUNCTION(BlueprintImplementableEvent)
void ApplyBuff(const FInputActionValue& Value);
Функции я сделал BlueprintImplementableEvent
'ами, чтобы в них можно было удобно добавить любую BP-логику. Осталось только подписать вызов функций на наш инпут. Добавляем в функцию SetupPlayerInputComponent()
строчки
void AGASExampleCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
//...
EnhancedInputComponent->BindAction(UseSpellAction, ETriggerEvent::Started, this, &AGASExampleCharacter::UseSpell);
EnhancedInputComponent->BindAction(ApplyBuffAction, ETriggerEvent::Started, this, &AGASExampleCharacter::ApplyBuff);
//...
}
Опять же, там уже привязаны функции под все остальные инпуты, так что действуем по аналогии или, выражаясь умными словами, соблюдаем консистентность.
Создадим геймплейные эффекты, которые нужны для нашего примера. Это нанесение урона GE_Damage
и бафф, увеличивающий урон GE_IncreasedDamage
.
Создание Gameplay Effect'ов
Начнем с GE_IncreasedDamage
, поскольку он проще. По сути, этот эффект просто должен временно изменить значение атрибута DamageMultiplier
. Итак, создаем блюпринт, наследуемый от GameplayEffect
; меняем Duration Policy
на Has Duration
; выставляем длительность 3 секунды; добавляем Modifier и настраиваем его как на показано на картинке ниже: модифицируемый атрибут – DamageMultiplier
, действие – Override
, значение – 2.
Получившийся эффект при применении сделает атрибут DamageMultiplier
равным 2, а через 3 секунды пропадет, вернув атрибуту значение по умолчанию (1).
Теперь перейдем к урону. Поскольку величина урона должна рассчитываться с учетом дополнительной информации (значения атрибута DamageMultiplier
), для GE_Damage
нам потребуется ModifierMagnitudeCalculation
. Создадим новый класс, наследуемый от UGameplayModMagnitudeCalculation
. В нем нам необходимо завести поля для отслеживаемых атрибутов и переопределить конструктор и функцию CalculateBaseMagnitude_Implementation()
:
//MMC_Damage.h
#include ...
UCLASS()
class GASEXAMPLE_API UMMC_Damage : public UGameplayModMagnitudeCalculation
{
GENERATED_BODY()
FGameplayEffectAttributeCaptureDefinition DmgMultDef;
UMMC_Damage();
virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
};
//MMC_Damage.cpp
#include ...
UMMC_Damage::UMMC_Damage()
{
DmgMultDef.AttributeToCapture = UExampleAttributeSet::GetDamageMultiplierAttribute();
DmgMultDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Source;
DmgMultDef.bSnapshot = false;
RelevantAttributesToCapture.Add(DmgMultDef);
}
float UMMC_Damage::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
FAggregatorEvaluateParameters EvaluationParameters;
float BaseDamage = -30.f;
float DmgMult = 1.f;
GetCapturedAttributeMagnitude(DmgMultDef, Spec, EvaluationParameters, DmgMult);
return BaseDamage * DmgMult;
}
В конструкторе мы настроили захват нашего атрибута DamageMultiplier
, а в CalculateBaseMagnitude_Implementation()
выполняем расчет урона, перемножая базовый урон (который в данном примере я просто захардкодил) и множитель урона.
Снова создаем Gameplay Effect
, в этот раз GE_Damage
. Duration Policy
у него оставляем Instant
, добавляем Modifier
, в котором Magnitude Calculation Type
устанавливаем на Custom Calculation Class
и выбираем созданный на предыдущем шаге MMC_Damage
в качестве Calculation Class
:
Таким образом, при применении GE_Damage,
цели будет наноситься урон, зависящий от DamageMultiplier
.
Теперь, когда нужные эффекты созданы, их нужно как-то применить.
Применять эффекты мы будем, естественно, из способностей. Поэтому следующий шаг:
Создание и интеграция способностей
Начнем опять с баффа. Создаем блюпринт GA_ApplyBuff
, наследованный от Gameplay Ability
. В нем применяем эффект GE_IncreasedDamage
на владельца способности:
Теперь создаем способность GA_UseSpell
, которая будет наносить урон в области. Ее код выглядит следующим образом:
В сущности, ничего сложного. Собираем всех персонажей вокруг владельца способности кроме него самого и применяем к ним эффект GE_Damage
. Как я уже писал в начале, вместо визуализации тут используется дебаг-сфера, но читатель может проявить фантазию в данном вопросе.
Также, чтобы как-то ограничить применение GA_UseSpell
, добавим в нее манакост. Для этого создадим Gameplay Effect
GE_UseSpellCost
, который будет при применении списывать 25 маны у цели:
Чтобы применить этот GE
в нашей способности, нужно указать его в поле Cost Gameplay Effect Class:
Теперь, когда способности готовы, их нужно как-то применять. Для этого, во-первых, заносим их в список выдаваемых способностей в нашем персонаже (BP_ThirdPersonCharacter
), который мы заблаговременно создали:
А во-вторых, активируем эти способности при вызове соответствующих функций (которые мы ранее привязывали к инпуту):
Вот и всё. Мы создали абилки, выдали их персонажу и привязали к инпуту.
На этом этапе вся геймплейная часть проекта готова и должна работать. Наш спелл наносит урон, который может быть увеличен активацией баффа, а результат этого взаимодействия можно пронаблюдать по изменению полоски здоровья на персонаже (для тестирования нужно просто поставить дополнительных персонажей на карту). Если бы мы создавали калькулятор урона, то этим можно было бы и ограничиться, но поскольку мы говорим о создании игр, нам необходимо также проинформировать игрока о происходящих событиях. Поэтому создадим еще пару Gameplay Cue
, которые будут визуализировать действие обоих наших эффектов.
Создание Gameplay Cue
Поскольку GE_Damage
мгновенный, а GE_IncreasedDamage
– продолжительный эффект, для них потребуются разные типы GC
. Для получения урона создадим GC_ReceivedDamage
, наследуемый от UGameplayCueNotify_Static
. Обязательно указываем тег, по которому будем использовать эту GC
:
Для _Static
Gameplay Cue
логику нужно вставлять в функцию OnExecute()
, так что переопределяем её следующим образом:
Чтобы воспроизвести Cue
при применении эффекта, в GE_Damage
добавляем нужную Gameplay Cue
:
Аналогично создаемGC
для баффа, только теперь наследуемся от AGameplayCueNotify_Actor
. Не забываем проставить тег:
В _Actor Gameplay Cue
переопределять нужно функции OnActive()
и OnRemove()
. В первой мы добавляем визуальный эффект, а во второй, соответственно, его удаляем:
Осталось только привязать созданный GC
в GE_IncreasedDamage
:
Вот теперь точно всё! На этом моменте всё должно выглядеть как на гифке в начале раздела. Естественно, этим функционалом я не рекомендую ограничиваться. Даже для самой минимальной имитации геймплея стоит добавить еще обработку смерти (при уменьшении здоровья до нуля), регенерацию маны и прочее и прочее. Но всё это вы можете осваивать уже по видео типа "Making an RPG using UE GAS". Благо, в настоящее время они существуют уже в достаточном количестве (больше одного). Тем не менее, надеюсь, что этот пример был полезен для формирования понимания основ работы с GAS.
Вместо заключения
Несмотря на то, что объем получился достаточно внушительным, предполагается, что это всё еще намного удобнее, чем осваивать материал по документации. Как и говорилось вначале, исчерпывающей информации статья не содержит, но, надеюсь, что базовое понимание вопроса после прочтения появилось. Дочитавшим желаю успехов в дальнейшем изучении плагина!