Unreal Engine Plugin for extending the GAS in a Lyra-like manner. The reason this plugin exists, is that I find myself very often writing boilerplate to setup GAS for my projects. Therefore, my aim was to provide a base GAS setup, that can be used for (almost) any type of project.
(I don't really like the name "Modular Gameplay Abilities" anymore lol, I was feeling a bit too fancy when I chose that name.)
If you discover any issues or have questions, feel free to reach out to me 🙂
My discord: majort
Warning
This is still in development and some features may not work as expected.
The UModularGameplayAbilty is an extended version of the base UGameplayAbility providing more functionality and customization options in the context of activation, failure, cooldowns, etc.
Gameplay Ability activation can happen in 3 different ways:
| Activation Policy | Description |
|---|---|
| 1. Passive | Used for abilities that always apply gameplay effects and/or tags when being given to an ASC. (e.g. "On Spawn") |
| 2. Triggered | Abilities that should be activated by a trigger (for example a GameplayEvent / Gameplay Message / etc.) |
| 3. Active | Abilities that should explicitly be activated by player actions. (E.g. pressing an input key) |
Note
Triggered or Passive abilities won't receive Input events, unless ForceReceiveInput is turned on.
Read more about ability input here
This will run through the default ability activation process but also checking for its Activation Group. The principle of Activation Groups is pretty much ported from Lyra.
On a high level, each ability can have its own activation group the defines its relationship to other abilities.
| Activation Group | Description |
|---|---|
| 1. Independent | Usually the majority of your abilities will run independently from another. Meaning they don't care about the activation of other abilities. |
| 2. Exclusive (Replaceable) | Setting the Activation Group to "Exclusive (Replaceable) means there should only be one exclusive ability running at a time. But it can be canceled and replaced by other exclusive abilities. |
| 3. Exclusive (Blocking) | This does pretty much the same as the "Exclusive (Replaceable) Activation Group, however instead of getting canceled by others, this will actively block all other exclusive abilities from activating. |
Potential usage of this could be:
- Independent: Abilities that can be used at any time, e.g.
Death,Respawn,Do A Thing. - Exclusive (Replaceable): Abilities that can be replaced by others, e.g.
Aim Down Sights,Sprint,Crouch. - Exclusive (Blocking): Abilities that should block others, e.g.
Show Inventory,Show Map. (Only one at a time should be shown)
For convenience, the UModularGameplayAbility comes with built-in "input released/pressed" methods,
which will be triggered according to the ability's Activation Policy.
Both native and blueprint versions.
virtual void OnAbilityInputPressed(float TimeWaited);
virtual void OnAbilityInputReleased(float TimeHeld);The ExplicitCooldownDuration property can be used to specifiy per-ability cooldowns.
This will be injected into the provied Cooldown Gameplay Effect class, together with the Explicit Cooldown tags and Asset Tags.
Therefore, allowing you to share the same Cooldown Gameplay Effect with multiple Abilities.
Warning
Since CheckCooldown() checks for cooldown tags (applied from the Cooldown Gameplay Effect), you must have unique Explicit Cooldown tags PER ability.
Otherwise the cooldown might function as a global cooldown, blocking all abilities, or cause unexpected behavior.
(The UModularGameplayAbility overrides the GetCooldownTags(), appending the Explicit Cooldown tags.)
As Gameplay Abilities are also able to be activated by bot-controlled pawns, the UModularGameplayAbility provides further logic modifying the AI's behavior.
E.g. during the Ability activation, if instigated by an AI, it will stop any Behavior Logic / AI Movement / RVO Avoidance / ...
To enable those "AI Events", you need to check the TriggerAIEvents property.
By default, the following AI events are implemented:
| Property | Description |
|---|---|
| Stops AI Behavior Logic | Stops AI Behavior logic until the abilit is finished/aborted. |
| Stops AI Movement | Will pause the current AI move until the ability is finished/aborted. |
| Stops AI RVO Avoidance | Not implemented yet |
| Activation Noise Range | When greater 0, will report a Noise event to the UAISense_Hearing sense, using the provided Range as radius and the Activation Noise Loudness as Loudness. |
| Impact Noise Range | Not implement yet |
| Activation Noise Loudness | @see Activation Noise Range |
Further AI Events can be implemented by overriding TriggerAIEventsOnActivate/Deactivate in either C++ or BP.

These are a few utility functions I also added to the ability class. Though, I feel like they should be come with the UGameplayAbility by default...
The UModularAbilitySystemComponent is pretty much the same as in Lyra.
Featuring a way to activate abilities based on gameplay tags or input id.
Read more about activating abilities via input here
The AModularAbilityActor is an example actor, that provides logic for an actor that is meant to use the Ability System. Thus setting up an example of how to lazy-load the Ability System and manage pending attribute modifiers for optimal performance.
The idea is, that these kind of actors will only create the Ability System, when first accessed.
Meaning their Ability System gets only created on demand.
Each actor can become a "lazy ability actor" by implementing the IPendingAttributeReceiver interface.
class IPendingAttributeReceiver
{
GENERATED_BODY()
public:
/** Called to update any pending attributes that were set before the Ability System was initialized. */
virtual void SetPendingAttributeFromReplication(const FGameplayAttribute& Attribute, const FGameplayAttributeData& NewValue) = 0;
};Whenever an OnRep_SomeAttributeName gets triggered inside your AttributeSet, you would call the new LAZY_ATTRIBUTE_REPNOTIFY (@see Modular Attribute Set) instead of the old GAMEPLAYATTRIBUTE_REPNOTIFY which will add the pending attribute from replication to the owning actor.
When the Ability System got created, you would then apply those pending attributes.
To see how one would do that, take a look at the AModularAbilityActor.
Thanks to:
- Vorixo
- Epic Games, Inc.
The ModularAbilitySet is a DataAsset, which stores a list of Gameplay Abilties, Gameplay Effects and Attribute Sets that can be granted and taken from an Ability System.
| Property | Description |
|---|---|
| Abilty Class | The ability class be granted. |
| Ability Level | The initial level of the granted ability. |
| Input Tag | A gameplay tag that will be added to the FGameplayAbilitySpec's DynamicAbilityTags. Used for input activation* |
Note
I don't really like using Gameplay Tags as input identification keys and would rather use the existing InputID's that GAS comes with.
I will show a way how to use them after this section.
| Property | Description |
|---|---|
| Gameplay Effect | The gameplay effect class to be given. |
| Level | The level of the granted effect. |
A list of attribute set classes to be spawned on the target.
In my game, I use a custom input id enum instead of Gameplay Tags.
Some people might disagree with me and say to use Gameplay Tags because of its modularity instead, but I'd rather not have the cost of replicating unnecessary gamplay tags (even though with fast replication it only replicated indices instead of the whole tag) and use the existing Input ID's that GAS comes with.
To actually use Input ID's you need to do several things.
- In the
Modular Gameplay Abilties Settingsyou need to enablebEnableAlterAbilityInput. - Create a new struct, that derives from
FAbilityActivatedByInputDatawhich contains your custom ability input enum.
And overrideGetInputKeyto return it's value.
e.g.,
USTRUCT(BlueprintType, meta=(DisplayName="Ability Input Data"))
struct FBotaniAbilityActivatedByInputData : public FAbilityActivatedByInputData
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Gameplay Ability")
TEnumAsByte<EBotaniAbilityInputBinds::Type> InputKey = EBotaniAbilityInputBinds::None;
protected:
virtual int32 GetInputKey() const override
{
return InputKey;
}
};- Create a custom Ability Set class, and add your "Ability Activated By Input Data" struct.
e.g.,
/** Non-mutable set of abilities that can be granted or removed to an actor that has an ability system component. */
UCLASS(Const, meta=(ShortTooltip="Set of Gameplay Abilities and Effects"), PrioritizeCategories=(AbilitySystem))
class UBotaniAbilitySet : public UModularAbilitySet
{
...
/** List of gameplay abilities that are meant to be activated by input. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Input, meta=(TitleProperty="InputKey"))
TArray<FBotaniAbilityActivatedByInputData> InputActivatedAbilities;
//~ Begin UModularAbilitySet Interface
virtual void GiveToAbilitySystem(UAbilitySystemComponent* AbilitySystem, FAbilitySetHandle* OutHandle, UObject* SourceObject = nullptr) const override;
//~ End UModularAbilitySet Interface
};
- Override the
GiveToAbilitySystem()method, to use the Input ID's.
Note
As of now, you need to implement the full method all by yourself.
Though, I will change this to make it much simpler in the future.
GiveToAbilitySystem() implementation
void UBotaniAbilitySet::GiveToAbilitySystem(
UAbilitySystemComponent* AbilitySystem,
FAbilitySetHandle* OutGrantedHandles,
UObject* SourceObject) const
{
check(AbilitySystem);
// Only authority can give or take abilities
if (!AbilitySystem->IsOwnerActorAuthoritative())
{
return;
}
// Assign ASC
if (OutGrantedHandles)
{
OutGrantedHandles->SetTargetAbilitySystem(AbilitySystem);
}
// Grant the abilities
if (UModularGameplayAbilitiesSettings::IsUsingAlterAbilityInput())
{
auto FindInput = [this](const UClass* AbilityClass)
{
return InputActivatedAbilities.FindByPredicate([AbilityClass](const FBotaniAbilityActivatedByInputData& Rhs)
{
return Rhs.AbilityClass == AbilityClass;
});
};
for (int32 Idx = 0; Idx < GameplayAbilities.Num(); ++Idx)
{
const TSoftClassPtr Ability = GameplayAbilities.Array()[Idx];
if (Ability.IsNull())
{
ABILITY_LOG(Error, TEXT("Ability at index %d in %s is invalid."),
Idx, *GetNameSafe(this));
continue;
}
const UClass* AbilityClass = Ability.LoadSynchronous();
if (!ensure(AbilityClass))
{
ABILITY_LOG(Error, TEXT("Ability class %s in %s is valid, but failed to load."),
*Ability.GetLongPackageName(), *GetNameSafe(this));
continue;
}
UGameplayAbility* CDO = AbilityClass->GetDefaultObject<UGameplayAbility>();
FGameplayAbilitySpec Spec(CDO, 1.f);
Spec.SourceObject = SourceObject;
// Assign input id if found
if (auto Input = FindInput(AbilityClass))
{
Spec.InputID = Input->InputKey;
}
const FGameplayAbilitySpecHandle Handle = AbilitySystem->GiveAbility(Spec);
if (OutGrantedHandles)
{
OutGrantedHandles->AddAbilitySpecHandle(Handle);
}
}
}
else
{
for (int32 Idx = 0; Idx < Abilities.Num(); ++Idx)
{
const auto& Ability = Abilities[Idx];
if (Ability.AbilityClass.IsNull())
{
ABILITY_LOG(Error, TEXT("Ability at index %d is invalid."), Idx);
continue;
}
const UClass* AbilityClass = Ability.AbilityClass.LoadSynchronous();
UGameplayAbility* CDO = AbilityClass->GetDefaultObject<UGameplayAbility>();
FGameplayAbilitySpec Spec(CDO, Ability.AbilityLevel);
Spec.SourceObject = SourceObject;
Spec.GetDynamicSpecSourceTags().AddTag(Ability.InputTag);
const FGameplayAbilitySpecHandle Handle = AbilitySystem->GiveAbility(Spec);
if (OutGrantedHandles)
{
OutGrantedHandles->AddAbilitySpecHandle(Handle);
}
}
}
// Grant the effects
for (int32 Idx = 0; Idx < GameplayEffects.Num(); ++Idx)
{
const auto& Effect = GameplayEffects[Idx];
if (Effect.GameplayEffect.IsNull())
{
ABILITY_LOG(Error, TEXT("Effect at index %d is invalid."), Idx);
continue;
}
const UGameplayEffect* EffectCDO = Effect.GameplayEffect.LoadSynchronous()->GetDefaultObject<UGameplayEffect>();
const FActiveGameplayEffectHandle Handle = AbilitySystem->ApplyGameplayEffectToSelf(EffectCDO, Effect.Level.GetValue(), AbilitySystem->MakeEffectContext());
if (OutGrantedHandles)
{
OutGrantedHandles->AddGameplayEffectHandle(Handle);
}
}
// Grant the attribute sets
for (int32 Idx = 0; Idx < AttributeSets.Num(); ++Idx)
{
const auto& AttributeSet = AttributeSets[Idx];
if (!IsValid(AttributeSet.AttributeSetClass))
{
ABILITY_LOG(Error, TEXT("Attribute set at index %d is invalid."), Idx);
continue;
}
UAttributeSet* NewSet = NewObject<UAttributeSet>(AbilitySystem->GetOwner(), AttributeSet.AttributeSetClass);
AbilitySystem->AddAttributeSetSubobject(NewSet);
if (OutGrantedHandles)
{
OutGrantedHandles->AddAttributeSet(NewSet);
}
}
}Inside your player controller (or whereever else your input lies), you should have a generic AbilityInputPressed/Released method that either takes in an Input ID or a GameplayTag.
E.g.,
void UBotaniAbilityHeroComponent::Input_AbilityInputPressed(EBotaniAbilityInputBinds::Type InputKey)
{
if (UModularAbilitySystemComponent* AbilitySystem = GetMyAbilitySystemComponent())
{
AbilitySystem->AbilityInputIdPressed(InputKey);
}
}
void UBotaniAbilityHeroComponent::Input_AbilityInputReleased(EBotaniAbilityInputBinds::Type InputKey)
{
if (UModularAbilitySystemComponent* AbilitySystem = GetMyAbilitySystemComponent())
{
AbilitySystem->AbilityInputIdReleased(InputKey);
}
}For GameplayTags, call the AbilityInputTagPressed/Released(Tag); version instead.
You still need to override the PostProcessInput method in the APlayerController to make the Ability System Component process the ability inputs.
E.g.,
void ABotaniPlayerController::PostProcessInput(const float DeltaTime, const bool bGamePaused)
{
if (UModularAbilitySystemComponent* ASC = GetMyAbilitySystemComponent())
{
ASC->ProcessAbilityInput(DeltaTime, bGamePaused);
}
Super::PostProcessInput(DeltaTime, bGamePaused);
}The UModularAttributeSet comes with a few helper macros to create additional properties and delegates among with your attributes.
| Macro | Description |
|---|---|
| ATTRIBUTE_ACCESSORS_NOTIFY | Same as ATTRIBUTE_ACCESSORS, but will also create a generic On##AttributeName##Changed delegate. |
| ATTRIBUTE_ACCESSORS_NOTIFY_DEPLETED | Same as ATTRIBUTE_ACCESSORS_NOTIFY,but will also create a generic OnOutOf##AttributeName## delegate |
They're useful for things like Health or Shield if you have a Health Component that should listen to attribute changes.
E.g.,
...
/** Attribute accessors */
ATTRIBUTE_ACCESSORS_NOTIFY_DEPLETED(ThisClass, Health)
ATTRIBUTE_ACCESSORS_NOTIFY_DEPLETED(ThisClass, Shield)
ATTRIBUTE_ACCESSORS_NOTIFY(ThisClass, MaxHealth)
ATTRIBUTE_ACCESSORS_NOTIFY(ThisClass, MaxShield)
...For the Health attribute, it would generate the following
// The things from ATTRIBUTE_ACCESSORS
...
mutable FGameplayAttributeEvent OnHealthChanged;
float CachedHealthBeforeAttributeChange;
mutable FGameplayAttributeEvent OnOutOfHealth;
bool bOutOfHealth;Which a Health Component could use like this:
UMyHealthComponent::InitializeWithAbilitySystem()
{
...
// Register to listen for attribute changes
HealthSet->OnHealthChanged.AddUObject(this, &ThisClass::HandleHealthChanged);
HealthSet->OnShieldChanged.AddUObject(this, &ThisClass::HandleShieldChanged);
HealthSet->OnMaxHealthChanged.AddUObject(this, &ThisClass::HandleMaxHealthChanged);
HealthSet->OnMaxShieldChanged.AddUObject(this, &ThisClass::HandleMaxShieldChanged);
HealthSet->OnOutOfHealth.AddUObject(this, &ThisClass::HandleOutOfHealth);
HealthSet->OnOutOfShield.AddUObject(this, &ThisClass::HandleOutOfShield);
Note
You still need to call the delegates yourself.
E.g.,
void UBotaniHealthSet::OnRep_Health(const FBotaniGameplayAttributeData& OldValue)
{
LAZY_ATTRIBUTE_REPNOTIFY(ThisClass, Health, OldValue)
// Call the change callback, which is not meant to change attributes on the client
const float CurrentHealth = GetHealth();
const float EstimatedMagnitude = CurrentHealth - OldValue.GetCurrentValue();
OnHealthChanged.Broadcast(nullptr, nullptr, nullptr, EstimatedMagnitude, OldValue.GetCurrentValue(), CurrentHealth);
if (bOutOfHealth && CurrentHealth <= 0.0f)
{
OnOutOfHealth.Broadcast(nullptr, nullptr, nullptr, EstimatedMagnitude, OldValue.GetCurrentValue(), CurrentHealth);
}
bOutOfHealth = CurrentHealth <= 0.0f;
}Listens to the specified Input Action and triggers the corresponding callbacks.
Runs local only !
The plugin also provides custom asset factories to quickly create new Ability System assets.

Common Gameplay Effect and Ability classes can be added in the project settings
ProjectSettings -> Editor -> Gameplay Ability Factory / Gameplay Effect Factory
If you like this project, leaving a star is much appreciated
Back to the top












