Skip to content

MajorTomAW/ModularGameplayAbilities

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

107 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Modular Gameplay Abilities

Overview

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.


Index

  1. Modular Gameplay Ability
      1.1 Activation
      1.2 Ability Input
      1.3 (Explicit) Cooldowns
      1.4 AI-Controlled Ability usage
      1.5 Utilies
  2. Modular Ability System Component
      2.1 Lazy-Loading the ASC
  3. Modular Ability Set
  4. Modular Attribute Set
  5. Ability Tasks
  6. Editor Integration

Modular Gameplay Ability

The UModularGameplayAbilty is an extended version of the base UGameplayAbility providing more functionality and customization options in the context of activation, failure, cooldowns, etc.


Activation

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.

image

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)

image


Ability Input

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);

image


(Explicit) Cooldowns

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.)

image


AI-Controlled Ability usage

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.

image

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.
image


Utilities

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...

General Utility

image

AI Utility

image


Modular Ability System Component

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


Lazy-Loading the ASC

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.

How it works

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.

image

Thanks to:


Modular Ability Set

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.

image

Abilities

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.

Gameplay Effects

Property Description
Gameplay Effect The gameplay effect class to be given.
Level The level of the granted effect.

Attributes

A list of attribute set classes to be spawned on the target.

Using Input ID's instead

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.

image

To actually use Input ID's you need to do several things.

  1. In the Modular Gameplay Abilties Settings you need to enable bEnableAlterAbilityInput.
  2. Create a new struct, that derives from FAbilityActivatedByInputData which contains your custom ability input enum.
    And override GetInputKey to 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;
	}
};
  1. 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
};
  1. 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);
		}
	}
}

Connecting Input with Abilities

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);
}

Modular Attribute Set

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;
}

Ability Tasks

Wait For Enhanced Input

image

Listens to the specified Input Action and triggers the corresponding callbacks.
Runs local only !


Editor Integration

The plugin also provides custom asset factories to quickly create new Ability System assets. image

Common Gameplay Effect and Ability classes can be added in the project settings
ProjectSettings -> Editor -> Gameplay Ability Factory / Gameplay Effect Factory

image image


If you like this project, leaving a star is much appreciated
Back to the top

About

Unreal Engine Plugin for extending the GAS. A bit inspired by Lyra.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors