Lyra Enhanced Input Ability Activation

Hi there,

I am replicating Lyra’s GAS input system so that inputs are handled by GameplayTags instead of Integers.

My implementation is done and working but there is a bug I cant figure out how to fix. On Gameplay Abilities the “Wait Input Released” task is never triggered.

UDataAsset holding mappings between InputTag - InputAction:

/**
 * FTaggedInputAction
 *
 *	Struct used to map an input action to a gameplay input tag.
 */
USTRUCT(BlueprintType)
struct FTaggedInputAction
{
	GENERATED_BODY()

public:

	UPROPERTY(EditDefaultsOnly)
	const UInputAction* InputAction = nullptr;

	UPROPERTY(EditDefaultsOnly, Meta = (Categories = "InputTag"))
	FGameplayTag InputTag;
};

/**
 *
 */
UCLASS()
class MAZEGAME_API UMGInputConfig : public UDataAsset
{
	GENERATED_BODY()

public:
	// Returns the first Input Action associated with a given tag.
	const UInputAction* FindNativeInputActionForTag(const FGameplayTag& InputTag) const;
	// Returns the first Input Action associated with a given tag.
	const UInputAction* FindAbilityInputActionForTag(const FGameplayTag& InputTag) const;

public:
	// List of input actions used by the owner. These input actions are mapped to a gameplay tag and must be manually bound.
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (TitleProperty = "InputAction"))
	TArray<FTaggedInputAction> NativeInputActions;

	// List of input actions used by the owner. These input actions are mapped to a gameplay tag and must be manually bound.
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (TitleProperty = "InputAction"))
	TArray<FTaggedInputAction> AbilityInputActions;
};

Similar UDataAsset that maps InputTags and GameplayAbilities:

/**
 * FAbilityInputMapping
 *
 *	Struct used to map an gameplay input tag to an ability.
 */
USTRUCT(BlueprintType)
struct FAbilityInputMapping
{
	GENERATED_BODY()

public:

	UPROPERTY(EditDefaultsOnly, Meta = (Categories = "InputTag"))
	FGameplayTag InputTag;

	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UGameplayAbility> Ability;
};

/**
 * 
 */
UCLASS()
class MAZEGAME_API UMGAbilityInputConfig : public UDataAsset
{
	GENERATED_BODY()

public:
	// Returns the first Input Action associated with a given tag.
	const TSubclassOf<UGameplayAbility> FindAbilityForTag(const FGameplayTag& InputTag) const;
	const int FindAbilityIdForTag(const FGameplayTag& InputTag) const;

public:
	// List of input actions used by the owner. These input actions are mapped to a gameplay tag and must be manually bound.
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (TitleProperty = "InputAction"))
	TArray<FAbilityInputMapping> AbilityInputMappings;
};

Function in custom EnhancedInputSystemComponent to add bindings for abilities with tags

template<class UserClass, typename PressedFuncType, typename ReleasedFuncType>
void UMGEnhancedInputComponent::BindAbilityActions(const UMGInputConfig* InputConfig, UserClass* Object, PressedFuncType PressedFunc, ReleasedFuncType ReleasedFunc, TArray<uint32>& BindHandles)
{
	check(InputConfig);

	for (const FTaggedInputAction& Action : InputConfig->AbilityInputActions)
	{
		if (Action.InputAction && Action.InputTag.IsValid())
		{
			if (PressedFunc)
			{
				BindHandles.Add(BindAction(Action.InputAction, ETriggerEvent::Triggered, Object, PressedFunc, Action.InputTag).GetHandle());
			}

			if (ReleasedFunc)
			{
				BindHandles.Add(BindAction(Action.InputAction, ETriggerEvent::Completed, Object, ReleasedFunc, Action.InputTag).GetHandle());
			}
		}
	}
}

Bind inputs and grant abilities in my character class:

// Called to bind functionality to input
void AMGCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	UMGEnhancedInputComponent* MGEnhancedInputComponent = Cast<UMGEnhancedInputComponent>(PlayerInputComponent);
	//Make sure to set your input component class in the InputSettings->DefaultClasses
	check(MGEnhancedInputComponent);
	const FMGGameplayTags& GameplayTags = FMGGameplayTags::Get();
	TArray<uint32> BindHandles;
	MGEnhancedInputComponent->BindAbilityActions(InputConfig, this, &ThisClass::InputAbilityInputTagPressed, &ThisClass::InputAbilityInputTagReleased, /*out*/ BindHandles);
}

void AMGCharacter::InputAbilityInputTagPressed(FGameplayTag InputTag)
{
	AbilitySystemComponent->AbilityInputTagPressed(InputTag);
}

void AMGCharacter::InputAbilityInputTagReleased(FGameplayTag InputTag)
{
	AbilitySystemComponent->AbilityInputTagReleased(InputTag);
}

void AMGCharacter::AddCharacterAbilities(FGameplayTag InputTag)
{
	for (FAbilityInputMapping AbilityMapping : AbilityConfig->AbilityInputMappings)
	{
		UGameplayAbility* AbilityCDO = AbilityMapping.Ability->GetDefaultObject<UGameplayAbility>();
		FGameplayAbilitySpec AbilitySpec(AbilityCDO, 1);
		AbilitySpec.SourceObject = this;
		AbilitySpec.DynamicAbilityTags.AddTag(AbilityMapping.InputTag);

		const FGameplayAbilitySpecHandle AbilitySpecHandle = AbilitySystemComponent->GiveAbility(AbilitySpec);
		AbilitySpecHandles.Add(AbilitySpecHandle);
		//AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(AbilityMapping.Ability, 1, AbilityConfig->FindAbilityIdForTag(AbilityMapping.InputTag), this));
	}
}

The custom AbilitySystemComponent that handles Pressed() and Released() (basically copied from Lyra):

void UMGAbilitySystemComponent::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);
			}
		}
	}
}

void UMGAbilitySystemComponent::AbilityInputTagReleased(const FGameplayTag& InputTag)
{
	if (InputTag.IsValid())
	{
		for (const FGameplayAbilitySpec& AbilitySpec : ActivatableAbilities.Items)
		{
			if (AbilitySpec.Ability && (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag)))
			{
				InputReleasedSpecHandles.AddUnique(AbilitySpec.Handle);
				InputHeldSpecHandles.Remove(AbilitySpec.Handle);
			}
		}
	}
}

void UMGAbilitySystemComponent::ProcessAbilityInput(float DeltaTime, bool bGamePaused)
{
	/*
	if (HasMatchingGameplayTag(TAG_Gameplay_AbilityInputBlocked))
	{
		ClearAbilityInput();
		return;
	}
	*/

	static TArray<FGameplayAbilitySpecHandle> AbilitiesToActivate;
	AbilitiesToActivate.Reset();

	//@TODO: See if we can use FScopedServerAbilityRPCBatcher ScopedRPCBatcher in some of these loops

	//
	// Process all abilities that activate when the input is held.
	//

	for (const FGameplayAbilitySpecHandle& SpecHandle : InputHeldSpecHandles)
	{
		if (const FGameplayAbilitySpec* AbilitySpec = FindAbilitySpecFromHandle(SpecHandle))
		{
			if (AbilitySpec->Ability && !AbilitySpec->IsActive())
			{
				const UMGGameplayAbility* AbilityCDO = CastChecked<UMGGameplayAbility>(AbilitySpec->Ability);

				
				if (AbilityCDO->GetActivationPolicy() == EMGAbilityActivationPolicy::WhileInputActive)
				{
					AbilitiesToActivate.AddUnique(AbilitySpec->Handle);
				}			
			}
		}
	}

	//
	// Process all abilities that had their input pressed this frame.
	//
	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 UMGGameplayAbility* AbilityCDO = CastChecked<UMGGameplayAbility>(AbilitySpec->Ability);

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

	//
	// Try to activate all the abilities that are from presses and holds.
	// We do it all at once so that held inputs don't activate the ability
	// and then also send a input event to the ability because of the press.
	//
	for (const FGameplayAbilitySpecHandle& AbilitySpecHandle : AbilitiesToActivate)
	{
		TryActivateAbility(AbilitySpecHandle);
	}

	//
	// Process all abilities that had their input released this frame.
	//
	for (const FGameplayAbilitySpecHandle& SpecHandle : InputReleasedSpecHandles)
	{
		if (FGameplayAbilitySpec* AbilitySpec = FindAbilitySpecFromHandle(SpecHandle))
		{
			if (AbilitySpec->Ability)
			{
				AbilitySpec->InputPressed = false;

				if (AbilitySpec->IsActive())
				{
					// Ability is active so pass along the input event.
					UMGUtils::PrintScreen(GetWorld(), FString::Printf(TEXT("AbilitySpecInputReleased")));
					AbilitySpecInputReleased(*AbilitySpec);
					//CancelAbilityHandle(SpecHandle);
				}
			}
		}
	}

	//
	// Clear the cached ability handles.
	//
	InputPressedSpecHandles.Reset();
	InputReleasedSpecHandles.Reset();
}

Image of a very simple gameplay ability that uses the “Wait Input Release” task. All other abilities work fine. And I know that AbilitySpecInputReleased() is called because my debug message fires.

2 Likes

Found the solution. For anyone struggeling with implementing this system, you also have to override the following two functions to handle correctly handle input pressed and released triggers. It’s all there in Lyra, I just didn’t copy enough of the component.

void UMGAbilitySystemComponent::AbilitySpecInputPressed(FGameplayAbilitySpec& Spec)
{
	Super::AbilitySpecInputPressed(Spec);

	// We don't support UGameplayAbility::bReplicateInputDirectly.
	// Use replicated events instead so that the WaitInputPress ability task works.
	if (Spec.IsActive())
	{
		// Invoke the InputPressed event. This is not replicated here. If someone is listening, they may replicate the InputPressed event to the server.
		InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec.Handle, Spec.ActivationInfo.GetActivationPredictionKey());
	}
}

void UMGAbilitySystemComponent::AbilitySpecInputReleased(FGameplayAbilitySpec& Spec)
{
	Super::AbilitySpecInputReleased(Spec);

	// We don't support UGameplayAbility::bReplicateInputDirectly.
	// Use replicated events instead so that the WaitInputRelease ability task works.
	if (Spec.IsActive())
	{
		// Invoke the InputReleased event. This is not replicated here. If someone is listening, they may replicate the InputReleased event to the server.
		InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputReleased, Spec.Handle, Spec.ActivationInfo.GetActivationPredictionKey());
	}
}
6 Likes

@Van_Hinten hey man, somehow my InputHeldSpecHandles, InputPressedSpecHandles always NULL, I’m assuming somehow I’m missing out some replication stuff. What could be the possible solution in minimal ?

Do you mind making a complete guide or tutorial on how you made this?

1 Like

I made one, if you’re still interested :smile:

4 Likes

Hello Sir, I have a question to ask. I use BindAction (Action. InputAction, ETriggerEvent:: Triggered, Object, PressedFunc, Action. InputTag); Bind an InputAction, but during the callback, the FGameplayTag in the PressedFunc function does not have a value. May I ask why this is? thanks

Same Problem. Have you found the problem yet?

@AlexBnew

InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec.Handle, Spec.ActivationInfo.GetActivationPredictionKey());

Dont forget to add them in this way

1 Like

Thx. I found the problem myself. For those who may face such a problem. Make sure the character gains the ability from the AbilitySet before the BindAbility occurs