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.