Gameplay Ability System Course Project - Development Blog

Hello again everyone! Welcome to another entry in this on-going blog/development thread for my GAS Course Project. For more context of this project, please see the very first post in this thread, and as usual, you can find my socials here:

Twitch : Twitch
Discord : UE5 - GAS Course Project - JevinsCherries
Tiktok : Devin Sherry (@jevinscherriesgamedev) | TikTok
Youtube : Youtube
Twitter : https://twitter.com/jevincherries
Instagram : Jevins Cherries Gamedev (@jevinscherriesgamedev) • Instagram photos and videos


Today we are going to talk about Ability Gameplay Tag Relationship Mappings and how I use it for my GAS Course Project. In short, an Ability Gameplay Tag Relationship Mapping asset is a way to dictate how different types of abilities can interact with each other, by means of blocking or cancelling one another, as well as adding required and block tags for each ability from one central location.


What is an Ability Gameplay Tag Relationship Mapping

Additional Reading:

Abilities in Lyra

In order to understand what the Ability Gameplay Tag Relationship Mapping is, we first must discuss the primary tag container that exists within Gameplay Abilities in GAS.

Ability Tags - Abillity Tags are a labeling of this ability that can then be referenced by other aspects of GAS. For example, when using the function, Try Activate Abilities by Tag, are referring to the Ability Tags of the ability(abilities) to activate. A common misconception that I have seen is developers thinking these are tags that get applied to the owning character of the ability, but that is wrong; this is what the Activation Owned Tags represent.

Cancel Abilities with Tag - These are Ability Tags (referenced above) that should be cancelled when this ability is activated. I use this to block and cancel my hit reaction abilities when the death ability is activated, in order to prevent further reactions from happening.

Block Abilities with Tag - These are Ability Tags (referenced above) that should be blocked while this ability is active. I use this to block other ‘action’ type of abilities while the aim-cast type ability is active.

Activation Owned Tags - These are the gameplay tags that are applied to the owner’s ability system component while the ability is active. This is good to have some labeled gameplay tag present on the ability system component in case you need to query on the component. In the case of my aim cast ability, I use this to apply a status tag to block point & click movement.

Activation Required Tags - These are gameplay tags that are required to be present on the ability system component attempting to activate the ability.

Activation Blocked Tags - These are gameplay tags that, if present on the ability system component attempting to activate the ability, will prevent the ability from being active. I use this to prevent my hit reaction ability from being activated if the ability system component has the death gameplay tags (Reaction.Death|Status.Death).

[Note: The following set of gameplay tag containers I have not yet used in any projects I have worked on, but perhaps you’ll find a need for them]

When triggering an ability via a Gameplay Event, the optional payload data includes gameplay tag containers for both Instigator and Target tags, instigator correlating to Source and target relating to Target.

Source Required Tags - Tags in the Source Required Tags container must be present in the Instigator Tags container of the Send Gameplay Event function payload in order for the ability to activate.


Source Block Tags - Tags in the Source Blocked Tags container cannot be present in the Instigator Tags container of the Send Gameplay Event function payload in order for the ability to activate.

Target Required Tags - Tags in the Target Required Tags container must be present in the Target Tags container of the Send Gameplay Event function payload in order for the ability to activate.


Target Block Tags - Tags in the Target Blocked Tags container cannot be present in the Target Tags container of the Send Gameplay Event function payload in order for the ability to activate.

The Ability Gameplay Tag Relationship Mapping was developed for Lyra, by Epic Games, to allow broader control over how abilities interact with each other. Here is a snippet of what their TagRelationships_ShooterHero mapping looks like:


Below is the current setup of my Ability Gameplay Tag Relationship Mapping

The typical naming/hierarchal convention for Ability Tags that I try to follow come from the examples provided by Lyra. For the GAS Course Project, at the time of this writing, are set up in 3 primary categories:

Ability.Type.Action - These are for abilities that are active via input actions of the player, and will be present in the abilities UI of the example project. Sub-categories are the following:

Ability.Type.Action.AimCast | .Duration | .Instant

Ability.Type.Passive - These are for abilities that are not activated by input, and are activated passively by gameplay elements, such as gameplay status (burn, freeze, etc.)

Ability.Type.Reaction - These are for abilities that are not activated by input, and are activated passively by gameplay elements, as reactions; such as the following:

Ability.Type.Reaction.Death | .Hit

NOTE: It may be better to move reactions to under the Passive action type category, but for now, this is fine for my needs.


Now that we understand what these gameplay tag containers do inside of the Gameplay Ability class, we can now talk a bit about what the Ability Gameplay Tag Relationship Mapping data asset does. In short, it allows for developers to map all their ability tags, and how they interact with one another, in one data asset; however, it only covers the following interactions:

Ability Tags to Block - This is the same as the Block Abilities with Tag container found inside of gameplay abilities.

Ability Tags to Cancel - This is the same as the Cancel Abilities with Tag container found inside of gameplay abilities.

Activation Required Tags - This is the same as the Activation Required Tags container found inside of gameplay abilities.

Activation Blocked Tags - This is the same as the Activation Blocked Tags container found inside of gameplay abilities.

The idea is to have a more hollistic approach of how these abilities interact with each other, rather than having these interactions exist in isolation within each gameplay ability. It gives designers the bigger picture, instead of hiding this within each ability, and can be super useful for fast iteration, if you set your ability tag hierarchy in a way that is flexible and scalable.


Here is my AbilityTagRelationshipMapping data asset class:

GASAbilityTagRelationshipMapping.h

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

#pragma once

#include "Engine/DataAsset.h"
#include "Containers/Array.h"
#include "GameplayTagContainer.h"
#include "GASAbilityTagRelationshipMapping.generated.h"

class UObject;

/** Struct that defines the relationship between different ability tags */
USTRUCT()
struct FGASCourseAbilityTagRelationship
{
	GENERATED_BODY()

	/** The tag that this container relationship is about. Single tag, but abilities can have multiple of these */
	UPROPERTY(EditAnywhere, Category = Ability, meta = (Categories = "Gameplay.Action"))
	FGameplayTag AbilityTag;

	/** The other ability tags that will be blocked by any ability using this tag */
	UPROPERTY(EditAnywhere, Category = Ability)
	FGameplayTagContainer AbilityTagsToBlock;

	/** The other ability tags that will be canceled by any ability using this tag */
	UPROPERTY(EditAnywhere, Category = Ability)
	FGameplayTagContainer AbilityTagsToCancel;

	/** If an ability has the tag, this is implicitly added to the activation required tags of the ability */
	UPROPERTY(EditAnywhere, Category = Ability)
	FGameplayTagContainer ActivationRequiredTags;

	/** If an ability has the tag, this is implicitly added to the activation blocked tags of the ability */
	UPROPERTY(EditAnywhere, Category = Ability)
	FGameplayTagContainer ActivationBlockedTags;
};

/** Mapping of how ability tags block or cancel other abilities */
UCLASS()
class UGASAbilityTagRelationshipMapping : public UDataAsset
{
	GENERATED_BODY()

private:
	/** The list of relationships between different gameplay tags (which ones block or cancel others) */
	UPROPERTY(EditAnywhere, Category = Ability, meta=(TitleProperty="AbilityTag"))
	TArray<FGASCourseAbilityTagRelationship> AbilityTagRelationships;

public:
	/** Given a set of ability tags, parse the tag relationship and fill out tags to block and cancel */
	void GetAbilityTagsToBlockAndCancel(const FGameplayTagContainer& AbilityTags, FGameplayTagContainer* OutTagsToBlock, FGameplayTagContainer* OutTagsToCancel) const;

	/** Given a set of ability tags, add additional required and blocking tags */
	void GetRequiredAndBlockedActivationTags(const FGameplayTagContainer& AbilityTags, FGameplayTagContainer* OutActivationRequired, FGameplayTagContainer* OutActivationBlocked) const;

	/** Returns true if the specified ability tags are canceled by the passed in action tag */
	bool IsAbilityCancelledByTag(const FGameplayTagContainer& AbilityTags, const FGameplayTag& ActionTag) const;
	
};

GASAbilityTagRelationshipMapping.cpp

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


#include "Game/GameplayAbilitySystem/GASAbilityTagRelationshipMapping.h"

void UGASAbilityTagRelationshipMapping::GetAbilityTagsToBlockAndCancel(const FGameplayTagContainer& AbilityTags,
	FGameplayTagContainer* OutTagsToBlock, FGameplayTagContainer* OutTagsToCancel) const
{
	// Simple iteration for now
	for (int32 i = 0; i < AbilityTagRelationships.Num(); i++)
	{
		const FGASCourseAbilityTagRelationship& Tags = AbilityTagRelationships[i];
		if (AbilityTags.HasTag(Tags.AbilityTag))
		{
			if (OutTagsToBlock)
			{
				OutTagsToBlock->AppendTags(Tags.AbilityTagsToBlock);
			}
			if (OutTagsToCancel)
			{
				OutTagsToCancel->AppendTags(Tags.AbilityTagsToCancel);
			}
		}
	}
}

void UGASAbilityTagRelationshipMapping::GetRequiredAndBlockedActivationTags(const FGameplayTagContainer& AbilityTags,
	FGameplayTagContainer* OutActivationRequired, FGameplayTagContainer* OutActivationBlocked) const
{
	// Simple iteration for now
	for (int32 i = 0; i < AbilityTagRelationships.Num(); i++)
	{
		const FGASCourseAbilityTagRelationship& Tags = AbilityTagRelationships[i];
		if (AbilityTags.HasTag(Tags.AbilityTag))
		{
			if (OutActivationRequired)
			{
				OutActivationRequired->AppendTags(Tags.ActivationRequiredTags);
			}
			if (OutActivationBlocked)
			{
				OutActivationBlocked->AppendTags(Tags.ActivationBlockedTags);
			}
		}
	}
}

bool UGASAbilityTagRelationshipMapping::IsAbilityCancelledByTag(const FGameplayTagContainer& AbilityTags,
	const FGameplayTag& ActionTag) const
{
	// Simple iteration for now
	for (int32 i = 0; i < AbilityTagRelationships.Num(); i++)
	{
		const FGASCourseAbilityTagRelationship& Tags = AbilityTagRelationships[i];

		if (Tags.AbilityTag == ActionTag && Tags.AbilityTagsToCancel.HasAny(AbilityTags))
		{
			return true;
		}
	}

	return false;
}

Next, you will need to create a variable for the tag relationship mapping inside of your ability system component and override the function,

void GetAdditionalActivationTagRequirements(const FGameplayTagContainer& AbilityTags, FGameplayTagContainer& OutActivationRequired, FGameplayTagContainer& OutActivationBlocked) const;|

This handles the block and cancel tags container and ensures we use what is set inside of the tag relationship mapping:

GASCourseAbilitySystemComponent.h

/** Looks at ability tags and gathers additional required and blocking tags */
void GetAdditionalActivationTagRequirements(const FGameplayTagContainer& AbilityTags, FGameplayTagContainer& OutActivationRequired, FGameplayTagContainer& OutActivationBlocked) const;|

// If set, this table is used to look up tag relationships for activate and cancel
UPROPERTY()
TObjectPtr<UGASAbilityTagRelationshipMapping> AbilityTagRelationshipMapping;

GASCourseAbilitySystemComponent.cpp

void UGASCourseAbilitySystemComponent::GetAdditionalActivationTagRequirements(const FGameplayTagContainer& AbilityTags,
                                                                              FGameplayTagContainer& OutActivationRequired, FGameplayTagContainer& OutActivationBlocked) const
{
	if (AbilityTagRelationshipMapping)
	{
		AbilityTagRelationshipMapping->GetRequiredAndBlockedActivationTags(AbilityTags, &OutActivationRequired, &OutActivationBlocked);
	}
}

In your Gameplay Ability class, you will need to override the function,

bool UGASCourseGameplayAbility::DoesAbilitySatisfyTagRequirements(const UAbilitySystemComponent& AbilitySystemComponent,
const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags,
FGameplayTagContainer* OptionalRelevantTags) const

This handles the activation required and activation blocked tags container and ensures we use what is set inside of the tag relationship mapping:

bool UGASCourseGameplayAbility::DoesAbilitySatisfyTagRequirements(const UAbilitySystemComponent& AbilitySystemComponent,
	const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags,
	FGameplayTagContainer* OptionalRelevantTags) const
{
	// Specialized version to handle death exclusion and AbilityTags expansion via ASC

	bool bBlocked = false;
	bool bMissing = false;

	const UAbilitySystemGlobals& AbilitySystemGlobals = UAbilitySystemGlobals::Get();
	const FGameplayTag& BlockedTag = AbilitySystemGlobals.ActivateFailTagsBlockedTag;
	const FGameplayTag& MissingTag = AbilitySystemGlobals.ActivateFailTagsMissingTag;

	// Check if any of this ability's tags are currently blocked
	if (AbilitySystemComponent.AreAbilityTagsBlocked(AbilityTags))
	{
		bBlocked = true;
	}

	const UGASCourseAbilitySystemComponent* GASCourseASC = Cast<UGASCourseAbilitySystemComponent>(&AbilitySystemComponent);
	static FGameplayTagContainer AllRequiredTags;
	static FGameplayTagContainer AllBlockedTags;

	AllRequiredTags = ActivationRequiredTags;
	AllBlockedTags = ActivationBlockedTags;

	// Expand our ability tags to add additional required/blocked tags
	if (GASCourseASC)
	{
		GASCourseASC->GetAdditionalActivationTagRequirements(AbilityTags, AllRequiredTags, AllBlockedTags);
	}

	// Check to see the required/blocked tags for this ability
	if (AllBlockedTags.Num() || AllRequiredTags.Num())
	{
		static FGameplayTagContainer AbilitySystemComponentTags;
		
		AbilitySystemComponentTags.Reset();
		AbilitySystemComponent.GetOwnedGameplayTags(AbilitySystemComponentTags);

		if (AbilitySystemComponentTags.HasAny(AllBlockedTags))
		{
			if (OptionalRelevantTags && AbilitySystemComponentTags.HasTag(Status_Death))
			{
				// If player is dead and was rejected due to blocking tags, give that feedback
			}

			bBlocked = true;
		}

		if (!AbilitySystemComponentTags.HasAll(AllRequiredTags))
		{
			bMissing = true;
		}
	}

	if (SourceTags != nullptr)
	{
		if (SourceBlockedTags.Num() || SourceRequiredTags.Num())
		{
			if (SourceTags->HasAny(SourceBlockedTags))
			{
				bBlocked = true;
			}

			if (!SourceTags->HasAll(SourceRequiredTags))
			{
				bMissing = true;
			}
		}
	}

	if (TargetTags != nullptr)
	{
		if (TargetBlockedTags.Num() || TargetRequiredTags.Num())
		{
			if (TargetTags->HasAny(TargetBlockedTags))
			{
				bBlocked = true;
			}

			if (!TargetTags->HasAll(TargetRequiredTags))
			{
				bMissing = true;
			}
		}
	}

	if (bBlocked)
	{
		if (OptionalRelevantTags && BlockedTag.IsValid())
		{
			OptionalRelevantTags->AddTag(BlockedTag);
		}
		return false;
	}
	if (bMissing)
	{
		if (OptionalRelevantTags && MissingTag.IsValid())
		{
			OptionalRelevantTags->AddTag(MissingTag);
		}
		return false;
	}

	return true;
}

References:

Scalesculptor - Ability Tag Relationship Maps in Lyra

Kaos Spectrum - Gameplay Tag Relationships


Thank you for taking the time to read this post, and I hope you were able to take something away from it. Please let me know if I got any information wrong, or explained something incorrectly! Also add any thoughts or questions or code review feedback so we can all learn together :slight_smile:


Next Blog Post Topic:

Gameplay Ability Cooldown

3 Likes