The Games DevProgramming and Game Development. Tips, Tricks and Tutorials.

Gameplay Ability Sets

· Updated Gameplay Ability System

Updated for UE 5.8: DynamicAbilityTags is deprecated (use GetDynamicSpecSourceTags()), GetSpawnedAttributes_Mutable() is now private (use RemoveSpawnedAttribute()), and the sample was missing a few pieces (the attribute set struct, the input tag, the handle’s attribute set storage) that are now included. The static helpers also had an interface type mismatch that is fixed.

Gameplay Ability Sets allow you to define a set of abilities, effects and attribute sets which are given to a target ASC. I use these to apply the default generic abilities a player has, and the default generic effects each player will have. They can also be used for things like perks, equipment, etc. The other benefit is they can be added and removed again via a simple handle, so you never have to remember what you granted. This is the same idea as ULyraAbilitySet in Epic’s Lyra sample, adapted to my own classes.

The ability set is a UPrimaryDataAsset you fill out in the editor. Granting it returns a handle which stores everything that was given, and you pass that handle back to remove it all. Only the authority can grant and take sets.

Here is the base class:

// Fill out your copyright notice in the Description page of Project Settings.

#include "AbilitySystem/Core/KaosAbilitySet.h"
#include "AbilitySystem/Abilities/KaosGameplayAbility.h"
#include "AttributeSet.h"
#include "GameplayEffect.h"
#include "KaosLogging.h"
#include "KaosAbilitySystemComponent.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(KaosAbilitySet)

namespace KaosAbilitySetHandle_Impl
{
	static int32 LastHandleId = 0;
	static int32 GetNextQueuedHandleIdForUse() { return ++LastHandleId; }
}

void FKaosAbilitySetHandle::AddAbilitySpecHandle(const FGameplayAbilitySpecHandle& Handle)
{
	if (Handle.IsValid())
	{
		AbilitySpecHandles.Add(Handle);
	}
}

void FKaosAbilitySetHandle::AddGameplayEffectHandle(const FActiveGameplayEffectHandle& Handle)
{
	if (Handle.IsValid())
	{
		GameplayEffectHandles.Add(Handle);
	}
}

void FKaosAbilitySetHandle::AddAttributeSet(UAttributeSet* Set)
{
	GrantedAttributeSets.Add(Set);
}

UKaosAbilitySet::UKaosAbilitySet(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
}

FKaosAbilitySetHandle UKaosAbilitySet::GiveAbilitySetTo(UKaosAbilitySystemComponent* ASC, UObject* OverrideSourceObject) const
{
	check(ASC);

	if (!ASC->IsOwnerActorAuthoritative())
	{
		// Must be authoritative to give or take ability sets.
		return FKaosAbilitySetHandle();
	}

	FKaosAbilitySetHandle OutHandle;
	OutHandle.HandleId = KaosAbilitySetHandle_Impl::GetNextQueuedHandleIdForUse();
	OutHandle.AbilitySystemComponent = ASC;
	
	// Grant the gameplay abilities.
	for (int32 AbilityIndex = 0; AbilityIndex < GrantedGameplayAbilities.Num(); ++AbilityIndex)
	{
		const FKaosAbilitySet_GameplayAbility& AbilityToGrant = GrantedGameplayAbilities[AbilityIndex];

		if (!IsValid(AbilityToGrant.Ability))
		{
			UE_LOG(LogKaosAbilitySystem, Error, TEXT("GrantedGameplayAbilities[%d] on ability set [%s] is not valid."), AbilityIndex, *GetNameSafe(this));
			continue;
		}

		FGameplayAbilitySpec AbilitySpec(AbilityToGrant.Ability, AbilityToGrant.AbilityLevel);
		AbilitySpec.SourceObject = OverrideSourceObject;
		if (AbilityToGrant.InputTag.IsValid())
		{
			AbilitySpec.GetDynamicSpecSourceTags().AddTag(AbilityToGrant.InputTag);
		}

		const FGameplayAbilitySpecHandle AbilitySpecHandle = ASC->GiveAbility(AbilitySpec);
		OutHandle.AddAbilitySpecHandle(AbilitySpecHandle);
	}

	// Grant the gameplay effects.
	for (int32 EffectIndex = 0; EffectIndex < GrantedGameplayEffects.Num(); ++EffectIndex)
	{
		const FKaosAbilitySet_GameplayEffect& EffectToGrant = GrantedGameplayEffects[EffectIndex];

		if (!IsValid(EffectToGrant.GameplayEffect))
		{
			UE_LOG(LogKaosAbilitySystem, Error, TEXT("GrantedGameplayEffects[%d] on ability set [%s] is not valid"), EffectIndex, *GetNameSafe(this));
			continue;
		}

		const UGameplayEffect* GameplayEffect = EffectToGrant.GameplayEffect->GetDefaultObject<UGameplayEffect>();
		const FActiveGameplayEffectHandle GameplayEffectHandle = ASC->ApplyGameplayEffectToSelf(GameplayEffect, EffectToGrant.EffectLevel, ASC->MakeEffectContext());
		OutHandle.AddGameplayEffectHandle(GameplayEffectHandle);
	}

	// Grant the attribute sets.
	for (int32 SetIndex = 0; SetIndex < GrantedAttributes.Num(); ++SetIndex)
	{
		const FKaosAbilitySet_AttributeSet& SetToGrant = GrantedAttributes[SetIndex];

		if (!IsValid(SetToGrant.AttributeSet))
		{
			UE_LOG(LogKaosAbilitySystem, Error, TEXT("GrantedAttributes[%d] on ability set [%s] is not valid"), SetIndex, *GetNameSafe(this));
			continue;
		}

		UAttributeSet* NewSet = NewObject<UAttributeSet>(ASC->GetOwner(), SetToGrant.AttributeSet);
		ASC->AddAttributeSetSubobject(NewSet);
		OutHandle.AddAttributeSet(NewSet);
	}

	return OutHandle;
}

FKaosAbilitySetHandle UKaosAbilitySet::GiveAbilitySetToInterface(TScriptInterface<IAbilitySystemInterface> AbilitySystemInterface, UObject* OverrideSourceObject) const
{
	UKaosAbilitySystemComponent* KaosASC = AbilitySystemInterface ? Cast<UKaosAbilitySystemComponent>(AbilitySystemInterface->GetAbilitySystemComponent()) : nullptr;
	if (!KaosASC)
	{
		return FKaosAbilitySetHandle();
	}
	return GiveAbilitySetTo(KaosASC, OverrideSourceObject);
}

void UKaosAbilitySet::TakeAbilitySet(FKaosAbilitySetHandle& AbilitySetHandle)
{
	if (!AbilitySetHandle.IsValid())
	{
		return;
	}
	
	UKaosAbilitySystemComponent* ASC = AbilitySetHandle.AbilitySystemComponent.Get();
	if (!ASC->IsOwnerActorAuthoritative())
	{
		// Must be authoritative to give or take ability sets.
		return;
	}

	for (const FGameplayAbilitySpecHandle& Handle : AbilitySetHandle.AbilitySpecHandles)
	{
		if (Handle.IsValid())
		{
			ASC->ClearAbility(Handle);
		}
	}

	for (const FActiveGameplayEffectHandle& Handle : AbilitySetHandle.GameplayEffectHandles)
	{
		if (Handle.IsValid())
		{
			ASC->RemoveActiveGameplayEffect(Handle);
		}
	}

	for (UAttributeSet* Set : AbilitySetHandle.GrantedAttributeSets)
	{
		ASC->RemoveSpawnedAttribute(Set);
	}

	AbilitySetHandle.Reset();
}

and the corresponding header file

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "AbilitySystemInterface.h"
#include "ActiveGameplayEffectHandle.h"
#include "GameplayAbilitySpecHandle.h"
#include "GameplayTagContainer.h"
#include "Engine/DataAsset.h"
#include "KaosAbilitySet.generated.h"

class UAttributeSet;
class UGameplayEffect;
class UKaosAbilitySystemComponent;
class UKaosGameplayAbility;

/** Data used by the ability set to grant a gameplay ability. */
USTRUCT(BlueprintType)
struct FKaosAbilitySet_GameplayAbility
{
	GENERATED_BODY()

public:
	/** Gameplay ability to grant. */
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UKaosGameplayAbility> Ability = nullptr;

	/** Level of ability to grant. */
	UPROPERTY(EditDefaultsOnly)
	int32 AbilityLevel = 1;

	/** Tag added to the granted spec's dynamic source tags, used to bind the ability to an input. */
	UPROPERTY(EditDefaultsOnly, meta = (Categories = "InputTag"))
	FGameplayTag InputTag;
};

/** Data used by the ability set to grant a gameplay effect. */
USTRUCT(BlueprintType)
struct FKaosAbilitySet_GameplayEffect
{
	GENERATED_BODY()

public:
	/** Gameplay effect to grant. */
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UGameplayEffect> GameplayEffect = nullptr;

	/** Level of gameplay effect to grant. */
	UPROPERTY(EditDefaultsOnly)
	float EffectLevel = 1.0f;
};

/** Data used by the ability set to grant an attribute set. */
USTRUCT(BlueprintType)
struct FKaosAbilitySet_AttributeSet
{
	GENERATED_BODY()

public:
	/** Attribute set to grant. */
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UAttributeSet> AttributeSet;
};

/** Stores handles to everything a granted ability set gave to an ASC, so it can be removed again. */
USTRUCT(BlueprintType)
struct FKaosAbilitySetHandle
{
	GENERATED_BODY()

	/** Returns true if this handle refers to a set that is still granted to a live ASC. */
	bool IsValid() const
	{
		return AbilitySystemComponent.IsValid() && HandleId != 0;
	}

private:
	friend class UKaosAbilitySet;

	void AddAbilitySpecHandle(const FGameplayAbilitySpecHandle& Handle);
	void AddGameplayEffectHandle(const FActiveGameplayEffectHandle& Handle);
	void AddAttributeSet(UAttributeSet* Set);

	void Reset()
	{
		AbilitySpecHandles.Reset();
		GameplayEffectHandles.Reset();
		GrantedAttributeSets.Reset();
		AbilitySystemComponent.Reset();
		HandleId = 0;
	}

	/** Handles to the granted abilities. */
	UPROPERTY()
	TArray<FGameplayAbilitySpecHandle> AbilitySpecHandles;

	/** Handles to the granted gameplay effects. */
	UPROPERTY()
	TArray<FActiveGameplayEffectHandle> GameplayEffectHandles;

	/** Attribute sets created by the grant, kept referenced so they are not garbage collected before removal. */
	UPROPERTY()
	TArray<TObjectPtr<UAttributeSet>> GrantedAttributeSets;

	/** Unique id of this grant, 0 means invalid. */
	int32 HandleId = 0;

	/** The ASC the set was granted to. */
	TWeakObjectPtr<UKaosAbilitySystemComponent> AbilitySystemComponent = nullptr;
};

/** Data asset that defines a group of abilities, effects and attribute sets which can be granted to and removed from an ASC together. */
UCLASS(BlueprintType, Const)
class KAOSGAME_API UKaosAbilitySet : public UPrimaryDataAsset
{
	GENERATED_BODY()
public:

	UKaosAbilitySet(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

	const TArray<FKaosAbilitySet_GameplayAbility>& GetGrantedGameplayAbilities() const { return GrantedGameplayAbilities; }
	const TArray<FKaosAbilitySet_GameplayEffect>& GetGrantedGameplayEffects() const { return GrantedGameplayEffects; }
	const TArray<FKaosAbilitySet_AttributeSet>& GetGrantedAttributes() const { return GrantedAttributes; }

	/** Grants everything in this set to the ASC and returns a handle for removing it. Authority only, returns an invalid handle otherwise. */
	FKaosAbilitySetHandle GiveAbilitySetTo(UKaosAbilitySystemComponent* ASC, UObject* OverrideSourceObject = nullptr) const;

	/** Same as GiveAbilitySetTo, but finds the ASC through IAbilitySystemInterface. */
	FKaosAbilitySetHandle GiveAbilitySetToInterface(TScriptInterface<IAbilitySystemInterface> AbilitySystemInterface, UObject* OverrideSourceObject = nullptr) const;

	/** Removes everything granted by the handle and resets it. Authority only. */
	static void TakeAbilitySet(FKaosAbilitySetHandle& AbilitySetHandle);
protected:

	/** Gameplay abilities to grant when this ability set is granted. */
	UPROPERTY(EditDefaultsOnly, Category = "Gameplay Abilities", meta=(TitleProperty=Ability))
	TArray<FKaosAbilitySet_GameplayAbility> GrantedGameplayAbilities;

	/** Gameplay effects to grant when this ability set is granted. */
	UPROPERTY(EditDefaultsOnly, Category = "Gameplay Effects", meta=(TitleProperty=GameplayEffect))
	TArray<FKaosAbilitySet_GameplayEffect> GrantedGameplayEffects;

	/** Attribute sets to grant when this ability set is granted. */
	UPROPERTY(EditDefaultsOnly, Category = "Attribute Sets", meta=(TitleProperty=AttributeSet))
	TArray<FKaosAbilitySet_AttributeSet> GrantedAttributes;
};

The Ability Sets can be granted by calling AbilitySet->GiveAbilitySetTo, or you can make some BP statics to do that like the example below:

void UKaosAbilityStatics::UnequipKaosAbilitySet(FKaosAbilitySetHandle& AbilitySetHandle)
{
	if (AbilitySetHandle.IsValid())
	{
		UKaosAbilitySet::TakeAbilitySet(AbilitySetHandle);
	}
}

FKaosAbilitySetHandle UKaosAbilityStatics::EquipKaosAbilitySet(TScriptInterface<IAbilitySystemInterface> AbilitySystemInterface, const UKaosAbilitySet* AbilitySet, UObject* OverrideSourceObject)
{
	return AbilitySet ? AbilitySet->GiveAbilitySetToInterface(AbilitySystemInterface, OverrideSourceObject) : FKaosAbilitySetHandle();
}

Notes and gotchas:

  • Store the returned handle somewhere (on the equipment, the player state, etc.). Without it you cannot take the set back off.
  • Attribute sets are added with AddAttributeSetSubobject and removed with RemoveSpawnedAttribute. The old sample used GetSpawnedAttributes_Mutable(), which is private in 5.8 and would not compile; RemoveSpawnedAttribute also unregisters the replicated subobject for you.
  • Granting only works on the authority. The specs and effects replicate to clients as usual.
  • The spec constructor taking a TSubclassOf<UGameplayAbility> is used above, so there is no need to fetch the CDO yourself.
  • I carry the input tag on the spec’s dynamic source tags (as Lyra does) and look it up when input is pressed. If you use the older InputID approach, pass the id as the third spec constructor argument instead.