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

Плюсы и минусы
Плагин GAS до версии 4.22 был в статусе «Unsupported», сейчас это упоминание сменилось на бету-версию. Но практика показывает, что он очень широко используется крупными игроками индустрии, например, в Fortnite.
Несмотря на присутствие слова «Ability» в названии, возможности системы очень широки и использовать ее можно для любых геймплейных взаимодействий, не только в MOBA/MMO играх. Примером тому наш Survival проект, где на базе абилок построены все действия игрока (перекаты, пинки, удары, стрельба, перезарядка, взаимодействие с объектами уровня и т.п.). Основные плюсы, которые вы получаете при использовании этой системы:
- Очень большая гибкость;
- Может использоваться гейм-дизайнерами без участия программиста (на самом деле для изначального сетапа придется залезть в c++, но для остального скиллы программиста, по большому счету, не нужны);
- Multiplayer-ready решение (очень жирный плюс, подробнее расскажем о мультиплеере в следующих статьях);
- Не нужно тратить время на написание и поддержку самой системы.
Ну и минусы, куда же без них:
- Достаточно крутая кривая обучения (широкие возможности накладываются на ограниченное количество документации и статей);
- С ростом количества эффектов/тегов/абилок сложнее отследить причины возникновения ошибок.
Взвесив все за и против, мы с головой нырнули в изучение возможностей системы и перевели практически все игровые взаимодействия на абилки, ни разу об этом не пожалев. Далее постараемся поделиться этим опытом и с вами, разобрав несколько не совсем тривиальных механик (хотя с виду они такими и не кажутся).
Включение GAS
По умолчанию плагин выключен, поэтому, если вы не включали его ранее, нужно сделать это в меню Settings -> Plugins

Рестартуем анрил и можно переходить непосредственно к абилкам.
Абилка Ролл
Я старался не рассматривать «простые» абилки и эффекты, которые только и делают, что модифицируют значения аттрибут сета.
Механика затронет некоторые нюансы взаимодействия сущностей, расширения функционала геймплей эффектов и реакций на них. Приступим, но для начала убедимся, что:
- Используется Unreal Engine 4.24 (в принципе, это не особо важно, но от версии к версии могут быть небольшие расхождения в API);
- Плагин Gameplay Abilities включен (см. «Включение GAS»);
- Запущен темплейтный проект Third Person (с поддержкой c++), будем основываться на нем.
Дизайн абилки закладываем такой: при нажатии на кнопку, персонаж совершает кувырок вперед. Во время переката временно отключим коллизии с другими павнами, чтобы легко выбираться из окружения врагов. Также, если кто-то из NPC заденет нас во время переката — получит дебаф на скорость передвижения на некоторое время. В конечном итоге получим вот такой скилл:

Для начала создадим новый класс персонажа, который впоследствии станет базовым для игрока и мобов. Сразу же добавим в него AbilitySystemComponet и реализуем интерфейс AbilitySystemInterface.
Я намеренно буду опускать директивы include для улучшения читаемости.
//h UCLASS() class AGASBaseCharacter : public ACharacter, public IAbilitySystemInterface { GENERATED_BODY() public: AGASBaseCharacter(); UAbilitySystemComponent* GetAbilitySystemComponent() const override; protected: UPROPERTY() UAbilitySystemComponent* AbilitySystemComponent; }; //cpp AGASBaseCharacter::AGASBaseCharacter() { AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>("AbilitySystemComponent"); } UAbilitySystemComponent* AGASBaseCharacter::GetAbilitySystemComponent() const { return AbilitySystemComponent; }
После завершения, главное не забыть унаследовать сгенерированного темплейт-проектом персонажа от только что созданного класса.
Т.к. мы хотим накладывать на персонажа эффекты и тем самым управлять его скоростью, создаем простенький AttributeSet, который позволит эти значения изменять.
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \ GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) UCLASS() class UGASAttributeSet : public UAttributeSet { GENERATED_BODY() public: UPROPERTY(BlueprintReadOnly) FGameplayAttributeData Health = 100.0f; ATTRIBUTE_ACCESSORS(UGASAttributeSet, Health) UPROPERTY(BlueprintReadOnly) FGameplayAttributeData MoveSpeed = 600.0f; ATTRIBUTE_ACCESSORS(UGASAttributeSet, MoveSpeed) };
Несколько пугает наличие макросов. Все они объявлены в файле AttributeSet.h. Если вкратце — GAMEPLAYATTRIBUTE_PROPERTY_GETTER добавляет геттер для аттрибута, GAMEPLAYATTRIBUTE_VALUE_GETTER/GAMEPLAYATTRIBUTE_VALUE_SETTER добавляют геттер и сеттер для значения аттрибута. Таким образом мы добавили 2 аттрибута в наш AttributeSet. Это MoveSpeed — скорость бега, Health — здоровье.
Добавим AttributeSet к нашему, только что созданному, классу персонажа:
//h UCLASS() class AGASBaseCharacter : public ACharacter, public IAbilitySystemInterface { ... protected: UPROPERTY() UGASAttributeSet* AttributeSet; ... }; //cpp AGASBaseCharacter::AGASBaseCharacter() { AttributeSet = CreateDefaultSubobject<UGASAttributeSet>(TEXT("AttributeSet")); }
Теперь свяжем изменения значений AttributeSet с реальной скоростью передвижения персонажа в CharacterMovementComponent.
void AGASBaseCharacter::BeginPlay() { Super::BeginPlay(); AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSet->GetMoveSpeedAttribute()).AddUObject(this, &AGASBaseCharacter::OnMovementAttributeChanged); } void AGASBaseCharacter::OnMovementAttributeChanged(const FOnAttributeChangeData& Data) { GetCharacterMovement()->MaxWalkSpeed = AttributeSet->GetMoveSpeed(); }
На данном с c++ пока можно остановиться. Компилируем и открываем проект в UE4. Создаем новый Gameplay Ability Blueprint:

Я назвал свою абилку GA_CharacterRoll.
Добавляем в EventGraph абилки событие CommitExecute. Данный эвент вызывается автоматически при успешном запуске абилки (прошли проверки на Required/Blocked теги, успешно списались косты, применились кулдауны). Следом за ним добавляем геймплей таску PlayMontageAndWait. По эвенту OnCompleted сразу заканчиваем абилку. Таким образом, при запуске скилла мы проиграем монтаж, а по его концу завершим абилку. Для начала неплохо.

Чтобы персонаж не крутился на месте (как Sonic), предадим ему небольшое ускорение вперед. Для этого будем использовать ноду ApplyRootMotionConstantForce. Она добавляет постоянный импульс в течении указанного времени. У меня в поле Duration стоит -1. Таким образом, персонаж будет двигаться до тех пор, пока не закончится абилка.

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


Момент пересечения с другими Pawn будем отслеживать в кастомной GameplayAblityTask.
Для этого вернемся в c++ и напишем чуть-чуть кода:
//h DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGASWaitOverlapDelegate, const FGameplayAbilityTargetDataHandle&, TargetData); UCLASS() class UGASAbilityTask_WaitForComponentOverlap : public UAbilityTask { GENERATED_BODY() UPROPERTY(BlueprintAssignable) FGASWaitOverlapDelegate OnOverlap; virtual void Activate() override; UFUNCTION(BlueprintCallable, Category = "Ability|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility")) static UGASAbilityTask_WaitForComponentOverlap* WaitForComponentOverlap(UGameplayAbility* OwningAbility, UPrimitiveComponent* Component); private: virtual void OnDestroy(bool AbilityEnded) override; UFUNCTION() void OnHitCallback(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); UPROPERTY() UPrimitiveComponent* Component; }; //cpp void UGASAbilityTask_WaitForComponentOverlap::Activate() { Super::Activate(); if (Component) { Component->OnComponentBeginOverlap.AddDynamic(this, &UGASAbilityTask_WaitForComponentOverlap::OnHitCallback); } } UGASAbilityTask_WaitForComponentOverlap* UGASAbilityTask_WaitForComponentOverlap::WaitForComponentOverlap(UGameplayAbility* OwningAbility, UPrimitiveComponent* Component) { auto abilityTask = NewAbilityTask<UGASAbilityTask_WaitForComponentOverlap>(OwningAbility); abilityTask->Component = Component; return abilityTask; } void UGASAbilityTask_WaitForComponentOverlap::OnDestroy(bool AbilityEnded) { if (Component) { Component->OnComponentBeginOverlap.RemoveAll(this); } Super::OnDestroy(AbilityEnded); } void UGASAbilityTask_WaitForComponentOverlap::OnHitCallback(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { if (OtherActor) { auto targetData = new FGameplayAbilityTargetData_ActorArray(); targetData->TargetActorArray.Add(OtherActor); auto targetDataHandle = FGameplayAbilityTargetDataHandle(targetData); if (ShouldBroadcastAbilityTaskDelegates()) { OnOverlap.Broadcast(targetDataHandle); } } }
Статический метод WaitForComponentOverlap создает экземпляр нашей таски. На вход мы принимаем компоненту для которой будем отслеживать Overlap эвенты, затем через функцию NewAbilityTask создаем и инициализируем ее. В методе Activate подписываемся на Overlap эвенты компоненты и по их наступлению вызываем делегат OnOverlap. Стоит обратить внимание, что в аргумент мы отдаем не самого актора, а структуру FGameplayAbilityTargetDataHandle. Это специальная структура, которая указывает на цель, содержит либо акторов либо точку в мире, над которыми необходимо совершить некоторые действия ( в нашем случае — наложить эффект). Т.к. эффекты мы применяем к акторам, используем FGameplayAbilityTargetData_ActorArray и в массив добавляем Overlap актора. В методе OnDestroy отписываемся от всех эвентов на которые подписались.
Возвращаемся в анрил, самое время создать эффект замедления. Создаем новый блюпринт и наследуем его от GameplayEffect класса.
Всего бывает 3 вида эффектов: Instant, Infinite, Has Duration. Этим управляет параметр DurationPolicy.

- Instant — мгновенный эффект, котрый меняет значение в аттрибут сете. Важно помнить, что Instant эффект не может накладывать на персонажа никаких тегов, они просто будут проигнорированы
- Infinite — эффект, который единовременно изменяет значения AttributeSet и накладывает/снимает с персонажа указанные теги. Сам эффект никогда не заканчивается, снять его можно только явно.
- Has Duration — геймплей эффект, который накладывается на ограниченное количество времени. Полностью аналогичен infinite за исключением того, что он будет снят по истечению указанного времени.
Важно помнить, что все манипуляции с AttributeSet, которые производят Infinite и Has Duration эффекты (для простоты просто Duration эффекты) будут отменены при снятии эффекта.
Так же Duration эффекты могут иметь период. В таком случае они будут применяться раз в указанный период, при этом все изменения что они производят будут трактоваться как Instant эффекты.
Каждый эффект може иметь несколько модифаеров — атомарных операций над аттрибут сетом. Основные операции над аттрибут сетом: Add/Multiply/Override. То, на какое конкретно значение измениться данный аттрибут определяется параметром ModifierMagnitude. Есть несколько видов модифаеров:
- Scalable float — обычное float значение, может быть загружено из CurveTable.
- AttributeBased — значение вычисляется на основе другого аттрибута
- CustomCalculation — Позволяет использовать кастомный C++ класс для вычисления значения аттрибута
- SetByCaller — значение аттрибута будет установлено извне (через FGameplayEffectSpec, с привязкой к конкретному тегу).
Каждый эффект имеет шанс применения (Chance to Apply to Target) и необходимые условия для применения. Это могут быть как простые условия, описываемые с помощью тегов (Application Tag Requirements), так и кастомные, описываемые извне (класс UGameplayEffectCustomApplicationRequirement).
Как уже упоминалось ранее, эффекты могут накладывать или снимать теги (Granted Tags), автоматически сниматься при условии наличия тех или иных тегов (RemovalTag Requirements), а также снимать с таргета другие эффекты, которые удовлетворяют условиям Remove Gameplay Effects with Tags или Remove Gameplay Effect Query.
На самом деле это не весь функционал геймплей эффектов, я упустил моменты с GameplayEffectExecutionCalculation, стаками эффектов, переполнением стаков, Immunity тегами, GameplayCue и много другое. Часть из этого будет упомянуто в этой статье дальше, часть будет затронуто в дальнейших статьях.

В эффекте настраиваем DurationPolicy -> Has Duration и меняем длительность действия. У меня это 5 секунд. Добавляем в эффект Modifier для аттрибута MoveSpeed, в качестве оператора выбираем Multiply и ставим магнитуду в 0.2 (таким образом снижаем скорость врагов на 80%).
Возвращаемся в эвент граф абалки GA_CharacterRoll и в самый конец дописываем только что созданную геймплей таску и наложение эффекта:

Эффект я вынес в отдельную переменную с типом Class Reference, в значение по умолчанию вписал свежесозданный эффект GE_Slowdown. Сама абилка закончена, однако мы не написали никакой логики по ее активации. Как я упоминал в самом начале, GAS умеет привязывать активацию скилов к InputAction движка. Для начала в Project Settings -> Engine/Input добавим 2 новых Action Mapping.

Я добавил Roll и Kick экшены. Теперь добавим персонажу возможность активировать абилки по наступлению этих событий. Для этого опять вернемся в c++. Первым делом создадим enum который будет описывать действия:
UENUM(BlueprintType) enum class EGASInputActions : uint8 { None, Roll, Kick };
В класс персонажа добавим маппинг экшена на абилити
UCLASS() class AGASBaseCharacter : public ACharacter, public IAbilitySystemInterface { ... public: UPROPERTY(EditDefaultsOnly) TMap<EGASInputActions, TSubclassOf<UGameplayAbility>> StandartAbilities; };
и в BeginPlay «выдадим» персонажу все абилки, что мы наконфигурировали
void AGASBaseCharacter::BeginPlay() { ... for (const auto& ability : StandartAbilities) { AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(ability.Value, 1, static_cast<int32>(ability.Key))); } ... }
Самое интересное — нужно зарегистрировать соответствие между именами Action Mapping и индексами EGASInputActions. Сделать это можно в методе SetupPlayerInputComponent, достаточно добавить туда строку:
AbilitySystemComponent->BindAbilityActivationToInputComponent(PlayerInputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"), FString("CancelTarget"), FString("EGASInputActions")));
Возвращаемся в анрил, открываем блюпринт персонажа и настраиваем для способность GA_CharacterRoll

Вот и все, можно запускать и проверять только что созданную способность.
Абилка пинок
Перейдем к реализации способности пинка. Суть довольно проста: игрок делает удар ногой, если он попадает по врагу, то тот отлетает назад, падает на спину и затем встает. Механически это будет выглядеть следующим образом: запускаем абилку пинка, в нужный момент находим цель перед собой, применяем на нее эффект отбрасывания, заканчиваем абилку.
Начнем с создания новой абилки, создаем блюпринт, наследника GameplayAbility. В эвент CommitExecute добавляем таску на проигрывание монтажа и таску ожидания GameplayEvent. Сам по себе GameplayEvent это событие, которое имеет тег и некоторые данные (Payload).

Конкретно в данном случае мы ожидаем эвента Event.Montage.Kick. Давайте напишем код, который этот эвент будет посылать. Для этого создадим новый блюпринт (BP_AnimNotify_SendGameplayEvent) и унаследуем его от AnimNotify. В Variables добавим одну публичную переменную с именем EventTag и типом GameplayTag. Переопределим функцию Received_Notify и отправим наш новый GameplayEvent актору на котором сработал данный нотифай.

Теперь в монтаж пинка можно добавить наш новый нотифай, значение EventTag сделаем равным Event.Montage.Kick:

Продолжим создание абилки. По наступлению GameplayEvent осталось всего-то определить цель и наложить на нее эффект падения. Перейдем к его созданию.
Рассмотрим более комплексный подход к обработке эффектов, когда нам не требуется модификация какого-либо атрибута, однако нужно каким-то гейм-механическим образом реагировать на применение того или иного эффекта. Для этого создадим новый класс эффекта в c++:
UCLASS(BlueprintType) class UKnockbackGameplayEffect : public UGameplayEffect { GENERATED_BODY() public: UPROPERTY(EditDefaultsOnly) float Strength = 100; };
Манипулировать будем «силой» пинка, чем она больше, тем дальше отбрасывает нашу цель. В нашем базовом классе персонажа подпишемся на делегат OnGameplayEffectAppliedDelegateToSelf — он вызывается, когда геймплей эффект применяется на овнера AbilitySystemComponent (однако только на сервере, стоит это помнить).
//h UCLASS() class AGASBaseCharacter : public ACharacter, public IAbilitySystemInterface { ... UFUNCTION() void OnGameplayEffectApplied(UAbilitySystemComponent* Source, const FGameplayEffectSpec& Spec, FActiveGameplayEffectHandle Handle); ... }; //cpp void AGASBaseCharacter::BeginPlay() { ... AbilitySystemComponent->OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &AGASBaseCharacter::OnGameplayEffectApplied); ... } void AGASBaseCharacter::OnGameplayEffectApplied(UAbilitySystemComponent* Source, const FGameplayEffectSpec& Spec, FActiveGameplayEffectHandle Handle) { if (auto knockbackEffect = Cast<UKnockbackGameplayEffect>(Spec.Def)) { if (auto movementComponent = GetCharacterMovement()) { auto instigator = Spec.GetEffectContext().GetInstigator(); auto direction = instigator ? (GetActorLocation() - instigator->GetActorLocation()).GetSafeNormal2D() : GetActorForwardVector() * -1.0f; auto constantForce = new FRootMotionSource_ConstantForce(); constantForce->AccumulateMode = ERootMotionAccumulateMode::Additive; constantForce->Force = direction * knockbackEffect->Strength; constantForce->Duration = Spec.GetDuration(); constantForce->FinishVelocityParams.Mode = ERootMotionFinishVelocityMode::SetVelocity; constantForce->FinishVelocityParams.SetVelocity = FVector::ZeroVector; movementComponent->ApplyRootMotionSource(constantForce); } } }
В обработчике проверяем что на нас пытаются наложить knockback эффект, если это так — применяем уже знакомый RootMotionSource в направлении, противоположном
источнику эффекта. Переходим в редактор и создаем блюпринт KnockbackGameplayEffect.

В блюпринте настраиваем силу и длительность эффекта. Осталось проиграть анимацию падения. Для всех «косметических» эффектов (звуки, партиклы, анимации и пр.) в GAS предусмотрена механика GameplayCue. Работает она так же, посредством геймплей тегов, с единственным ограничением: тег должен быть в категории GameplayCue. Добавим новую геймплей кью в наш эффект.

Т.к. наш эффект не модифицирует ни одного атрибута, уберем чекбокс “Require Modifier Success to Trigger Cues”. Добавим обработчик данной Cue, для этого откроем окно GameplayCueEditor (Window -> GameplayCueEditor):

Нажимаем Add new напротив интересующего нас тега (GameplayCue.Knockback) и создаем Gameplay Cue Notify Static обработчик. Переопределяем OnActivate функцию и проигрываем в ней монтаж падения.

Все, на этом эффект падения закончен. Можно добавить код по его активации в блюпринт абилки:

Не буду сильно останавливаться на функции GetTarget. На самом деле, для определения целей в GAS существует отдельная механика (TargetActor), но пока что реализуем этот функционал самым простейшим способом — LineTrace.

Падение мы реализовали. Теперь нужно сделать так, чтобы во время этого наша цель потеряла возможность двигаться. Для этого добавим последний эффект — стан. Создаем новый блюпринт от класса GameplayEffect. Все что он буддет делать — это накладывать на персонажа тег Effect.Stun на нужное количество времени.

Перейдем в c++ и напишем обработчик данного события:
//h UCLASS() class AGASBaseCharacter : public ACharacter, public IAbilitySystemInterface { ... UFUNCTION() void OnStunTagChanged(const FGameplayTag Tag, int32 Count); ... }; //cpp void AGASBaseCharacter::BeginPlay() { ... AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag("Effect.Stun")).AddUObject(this, &AGASBaseCharacter::OnStunTagChanged); ... } void AGASBaseCharacter::OnStunTagChanged(const FGameplayTag Tag, int32 Count) { if (auto aiController = Cast<AAIController>(GetController())) { if (Count > 0) { aiController->GetBrainComponent()->PauseLogic(TEXT("")); } else { aiController->GetBrainComponent()->ResumeLogic(TEXT("")); } } }
Подписываемся на событие изменения тега Effect.Stun, если количество тегов больше нуля — выключаем всю AI логику у персонажа, если меньше или равно нулю — включаем.
Дело за малым, добавляем применение второго эффекта на цель:

Финальный вид скилла:

Как видите, Gameplay Ability System открывает множество возможностей для разработчиков. В следующем материале мы подробно разберем мультиплеер составляющую GAS и таргетинг.
ссылка на оригинал статьи https://habr.com/ru/company/socialquantum/blog/505886/
Добавить комментарий