Looking into Lyra - GAS - Inputs and the Gameplay Ability System

Introduction

The aim of this is to examine the source code from the Lyra example provided by Epic Games and to try and identify and document practices and approaches for using c++.

Here we look at use of Gameplay Tags and the Gameplay Ability System (GAS), and try and trace the use of gameplay tags from pressing a button to getting a response in the UI and in gameplay.

About Gameplay Tags

A Gameplay Tag is a string-like object with a number of parts delimited by "." characters, such as "Event.Movement.Dash". All of GAS depends on these tags. Gameplay tags can be created in different ways including using the Unreal Editor, loading from data tables, loading from .ini files, and using C++.

For more information on how tags are created see Gameplay Tags.

Stages in how Lyra uses the Gameplay Ability System

These are the major steps which happen when Lyra receives input from the user to when the consequences of that input are displayed.

Step 1: Input

User enters input

Step 2: From Input to Input Action using Input Mapping Context

Input is matched to an Input Mapping Context. An Input Mapping Context is like a table matching hardware inputs to Input Action objects.

Input Mapping Contexts are used by Lyra to assign a set of actions (for example matching a game mode such as deathmatch) or an individual action (matching an individual weapon) dynamically as game modes are selected and items are equipped and discarded.

In addition to matching an hardware input, an Input Mapping Context can modify input processing, for example by altering joystick sensitivity.

For more information on how tags are created and used see Input Mapping Contexts.

Step 3: From Input Action to Gameplay Tag

The ULyraPawnData class is used to define a Lyra pawn. The pawn contains mappings from an Input Action to either:

  • a gameplay tag, which will lead us to a gameplay ability
  • a native action, which is a function which is executed directly without using a gameplay ability.

The pawn class has a reference to a ULyraInputConfig like so:

This is used to load configuration data which populates the pawns action mappings.

For more information on how input actions are mapped to native actions and gameplay abilities see Pawn Action Mappings.

Step 4: From Gameplay Tag to Gameplay Ability

Gameplay abilities are assigned to player character (an other pawns) when they become available due to the game mode, game phase, or equipped items. The gameplay abilities which are assigned to a character are referred to by Lyra as "activatable" abilities.

The LyraAbilitySystemComponent, which is used as a component of both the part of the player character and the player state, maintains a list of activatable gameplay abilities. Each gameplay ability in the list is associated with one gameplay tag. This pairing of a gameplay ability and a gameplay tag is held in a FGameplayAbilitySpec structure.

The LyraAbilitySystemComponent has a list of these FGameplayAbilitySpec objects (in the member ActivatableAbilities), and once a user action is translated to a gameplay tag, this list is searched for items which match that gameplay tag.

Here is an example of the asset which associates the GA_Hero_Jump ability with the InputTag.Jump gameplay tag:

For more information about LyraAbilitySystemComponent and FGameplayAbilitySpec see Activatable Abilities

Step 5: Queueing a Gameplay Ability for Activation

When an action is initiated by the user and the LyraHeroComponent::Input_AbilityInputTagPressed() method is called, this forwards the call to the ULyraAbilitySystemComponent:

void ULyraHeroComponent::Input_AbilityInputTagPressed(FGameplayTag InputTag)
{
	if (const APawn* Pawn = GetPawn<APawn>())
	{
		if (const ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
		{
			if (ULyraAbilitySystemComponent* LyraASC = PawnExtComp->GetLyraAbilitySystemComponent())
			{
				LyraASC->AbilityInputTagPressed(InputTag);
			}
		}	
	}
}

The LyraAbilitySystemComponent component (which is part of the player class) holds a list of FGameplayAbilitySpec objects each of which holds (in addition to other things) a pointer to a UGameplayAbility object and a collection of one or more gameplay tags. These are the available abilities configured when the pawn was created. Each FGameplayAbilitySpec has a unique handle.

The code in ULyraAbilitySystemComponent::AbilityInputTagPressed() scans that list and if it finds a FGameplayAbilitySpec with a the required gameplay tag it adds the handle of the FGameplayAbilitySpec to two collections of handles called InputPressedSpecHandles and InputHeldSpecHandles. It does not execute the ability, it just updates the list of handles of abilities which are currently executing.

void ULyraAbilitySystemComponent::AbilityInputTagPressed(const FGameplayTag& InputTag)
{
	if (InputTag.IsValid())
	{
		for (const FGameplayAbilitySpec& AbilitySpec : ActivatableAbilities.Items)
		{
			if (AbilitySpec.Ability && (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag)))
			{
				InputPressedSpecHandles.AddUnique(AbilitySpec.Handle);
				InputHeldSpecHandles.AddUnique(AbilitySpec.Handle);
			}
		}
	}
}

Input Step 4: Activating a Queued Gameplay Ability

What happens next is that the game executes the tick function for the player controller and these functions are called:

  • APlayerController::TickActor() calls
  • ALyraPlayerController::PlayerTick() eventually calls
  • ULyraAbilitySystemComponent::ProcessAbilityInput()

This iterates through all the FGameplayAbilitySpec handles added since last tick (i.e. added in step 3 above) and collects them into the AbilitiesToActivate collection:

for (const FGameplayAbilitySpecHandle& SpecHandle : InputPressedSpecHandles)
{
	if (FGameplayAbilitySpec* AbilitySpec = FindAbilitySpecFromHandle(SpecHandle))
	{
		if (AbilitySpec->Ability)
		{
			AbilitySpec->InputPressed = true;

			if (AbilitySpec->IsActive())
			{
				// Ability is active so pass along the input event.
				AbilitySpecInputPressed(*AbilitySpec);
			}
			else
			{
				const ULyraGameplayAbility* LyraAbilityCDO = CastChecked<ULyraGameplayAbility>(AbilitySpec->Ability);

				if (LyraAbilityCDO->GetActivationPolicy() == ELyraAbilityActivationPolicy::OnInputTriggered)
				{
					AbilitiesToActivate.AddUnique(AbilitySpec->Handle);
				}
			}
		}
	}
}

Immediately after that the abilities are activated:

for (const FGameplayAbilitySpecHandle& AbilitySpecHandle : AbilitiesToActivate)
{
	TryActivateAbility(AbilitySpecHandle);
}

Reference

Epic: Gameplay Tags
Epic: Using Gas
Epic: GAS Attributes and Effects
Epic: GAS
Epic: Games Input

MOVE

Lyra's design supports multiple game modes (such as the Arena and ShooterGame) and different phases within each game mode (such as warmup, play, post-game). This adds some complexity to Lyra's use of the Gameplay Ability System.

This intent here is that gameplay abilities, gameplay effects, and gameplay attributes are grouped into sets, and that these sets can be associated with different game modes, different phases of the game, and different items of equipment. A player might gain a specific ability (i.e. have a gameplay ability become available to his Actor object) by stepping into a room or picking up weapon, and lose that ability once he leaves the room or puts down the weapon.

Using Gameplay Tags as an interface

A gameplay tags is a string like "Player.Weapon.Shotgun". Lyra uses gameplay tags to separate parts of the game. When the user does an input action such as pressing a key this executes a series of steps eventually producing a single gameplay tag which is used

"Extension"

Lyra labels the concept of separating the player pawn object from the player state (which holds the available gameplay abilities) "Extension". The player pawn object is extended by the LyraPawnExtensionComponent component.

Show more details... An example of this is forwarding a request for the ability system component from the Lyra character to its pawn extension component:
UAbilitySystemComponent* ALyraCharacter::GetAbilitySystemComponent() const
{
	return PawnExtComponent->GetLyraAbilitySystemComponent();
}

References

Epic: Asset Management (https://docs.unrealengine.com)
Epic: Asset Loading Best Practices (https://youtu.be/)
Tom Looman: Async Asset Loading (https://www.tomlooman.com)
tranek/GASDocumentation

Feedback

Please leave any feedback about this article here