Gameplay Ability System - Custom Input Handling

Hi all,

I am currently getting my feet wet with the Gameplay Ability System (GAS). I want to do something simple:

  • Attach a TArray<FGameplayAbilitySpec> CurrentAbilities to my custom UPlayerAbilitySystemComponent
  • Have generic input bindings Ability1, Ability2, Ability3, … defined in the project settings
  • When the player presses the Ability<N> input key, I want to activate the Ability that is in position <N> of the CurrentAbilities TArray

What I have done so far is:

AMyCharacter.cpp

When calling SetupPlayerInputComponent I call a custom BindToInputComponent function in the AbilitySystemComponent:

void AMyCharacter::GiveDefaultAbilities() {
  if(HasAuthority() && AbilitySystemComponent) {
    for(int32 i = 0; i < Abilities.Num(); i++) {
      if(DefaultAbilities.IsValidIndex(i)) {
        TSubclassOf<class UGameplayAbilityBase>& DefaultAbility = DefaultAbilities[i];
        AssignAbility(i, DefaultAbility);
      }
    }
  }
}

void AMyCharacter::AssignAbility(int32 index, TSubclassOf<class UGameplayAbilityBase>& GameplayAbility) {
  if(GameplayAbility) {
    FGameplayAbilitySpec Spec = FGameplayAbilitySpec(GameplayAbility, 1, INDEX_NONE, this);
    FGameplayAbilitySpecHandle SpecHandle = AbilitySystemComponent->GiveAbility(Spec);
    AbilitySystemComponent->AssignAbility(index, Spec);
  }
}

void AMyCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) {
  Super::SetupPlayerInputComponent(PlayerInputComponent);
  AbilitySystemComponent->BindToInputComponent(InputComponent);
}

PlayerAbilitySystemComponent.h:

class ABILITYSYSTEM_API UPlayerAbilitySystemComponent : public UAbilitySystemComponent {
  UPROPERTY(BlueprintReadOnly, Category = "Abilities")
    TArray<FGameplayAbilitySpec> CurrentAbilities;

  virtual void AssignAbility(int32 index, FGameplayAbilitySpec& Spec);
  virtual void BindToInputComponent(UInputComponent* InputComponent) override;

  virtual void AbilityLocalInputPressed(int32 InputID) override;
  virtual void AbilityLocalInputReleased(int32 InputID) override;
}

PlayerAbilitySystemComponent.cpp:

The BindToInputComponent function binds my project settings inputs to my custom AbilityLocalInputPressed and AbilityLocalInputReleased functions.
These do essentially the same as the default functions from the AbilitySystemComponent except they retrieve the spec from my TArray CurrentAbilities

void UPlayerAbilitySystemComponent::BindToInputComponent(UInputComponent* InputComponent) {
  static const FName ConfirmBindName(TEXT("AbilityConfirm"));
  static const FName CancelBindName(TEXT("AbilityCancel"));
  static const FName Ability1BindName(TEXT("Ability1"));

  {
    FInputActionBinding AB(ConfirmBindName, IE_Pressed);
    AB.ActionDelegate.GetDelegateForManualSet().BindUObject(this, &UPlayerAbilitySystemComponent::LocalInputConfirm);
    InputComponent->AddActionBinding(AB);
  }

  {
    FInputActionBinding AB(CancelBindName, IE_Pressed);
    AB.ActionDelegate.GetDelegateForManualSet().BindUObject(this, &UPlayerAbilitySystemComponent::LocalInputCancel);
    InputComponent->AddActionBinding(AB);
  }

  {
    FInputActionBinding ABP(Ability1BindName, IE_Pressed);
    ABP.ActionDelegate.GetDelegateForManualSet().BindUObject(this, &UPlayerAbilitySystemComponent::AbilityLocalInputPressed, 0);
    InputComponent->AddActionBinding(ABP);
    FInputActionBinding ABR(Ability1BindName, IE_Released);
    ABR.ActionDelegate.GetDelegateForManualSet().BindUObject(this, &UPlayerAbilitySystemComponent::AbilityLocalInputReleased, 0);
    InputComponent->AddActionBinding(ABR);
  }
}

void UPlayerAbilitySystemComponent::InitializeComponent() {
  Super::InitializeComponent();

  CurrentAbilities.SetNumZeroed(10);
}

void UPlayerAbilitySystemComponent::AssignAbility(int32 index, FGameplayAbilitySpec& Spec) {
  if(CurrentAbilities.IsValidIndex(index)) {
    CurrentAbilities[index] = Spec;
  }
}

void UPlayerAbilitySystemComponent::AbilityLocalInputPressed(int32 InputID) {
  // Consume the input if this InputID is overloaded with GenericConfirm/Cancel and the GenericConfim/Cancel callback is bound
  if(IsGenericConfirmInputBound(InputID)) {
    LocalInputConfirm();
    return;
  }

  if(IsGenericCancelInputBound(InputID)) {
    LocalInputCancel();
    return;
  }

  // ---------------------------------------------------------

  ABILITYLIST_SCOPE_LOCK();
  {
    if(CurrentAbilities.IsValidIndex(InputID)) {
      FGameplayAbilitySpec Spec = CurrentAbilities[InputID];
      if(Spec.Ability) {
        Spec.InputPressed = true;
        if(Spec.IsActive()) {
          if(Spec.Ability->bReplicateInputDirectly && IsOwnerActorAuthoritative() == false) {
            ServerSetInputPressed(Spec.Handle);
          }

          AbilitySpecInputPressed(Spec);

          // 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());
        } else {
          // Ability is not active, so try to activate it
          UGameplayAbilityBase* GA = Cast<UGameplayAbilityBase>(Spec.Ability);
          if(GA && GA->bActivateOnInput) {
            // Ability is not active, so try to activate it
            bool active = TryActivateAbility(Spec.Handle);
            if(active) {
              UE_LOG(LogTemp, Warning, TEXT("Spec is active now!"));
              UE_LOG(LogTemp, Warning, TEXT("Spec.IsActive(): %i"), Spec.IsActive());
              UE_LOG(LogTemp, Warning, TEXT("Spec.ActiveCount: %i"), Spec.ActiveCount);
              UE_LOG(LogTemp, Warning, TEXT("Spec.GetAbilityInstances().Num(): %i"), Spec.GetAbilityInstances().Num());
              UE_LOG(LogTemp, Warning, TEXT("Spec.InputPressed: %i"), Spec.InputPressed);
            } else {
              UE_LOG(LogTemp, Warning, TEXT("Spec is NOT active!"));
            }
          }
        }
      }
    }
  }
}

void UPlayerAbilitySystemComponent::AbilityLocalInputReleased(int32 InputID) {
  ABILITYLIST_SCOPE_LOCK();
  {
    if(CurrentAbilities.IsValidIndex(InputID)) {
      FGameplayAbilitySpec Spec = CurrentAbilities[InputID];
      Spec.InputPressed = false;
      if(Spec.Ability) {
        UE_LOG(LogTemp, Warning, TEXT("Spec has Ability"));
        UE_LOG(LogTemp, Warning, TEXT("Spec.InputPressed: %i"), Spec.InputPressed);
        if(Spec.IsActive()) {
          UE_LOG(LogTemp, Warning, TEXT("Spec is active"));

          if(Spec.Ability->bReplicateInputDirectly && IsOwnerActorAuthoritative() == false) {
            ServerSetInputReleased(Spec.Handle);
          }

          AbilitySpecInputReleased(Spec);

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

          UE_LOG(LogTemp, Warning, TEXT("Spec should have gotten InputReleased signal by now"));
        } else {
          UE_LOG(LogTemp, Warning, TEXT("Spec is NOT active"));
        }
      }
    }
  }
}

Now, what happens when I press the Ability1 input key, the ability activates, TryActivateAbility is called. However, in that ability I have a WaitInputRelease task that ends the ability when the input is released. This does not work. The ability activates but the task will never recognize the inputReleased event, although it is fired in my PlayerAbilitySystemComponent.
The reason seems to be that if(Spec.IsActive()) does not return true when I use my custom input bindings.
Spec.IsActive() checks for Ability != nullptr && ActiveCount > 0 and the latter is always 0. After activation, I check this:

if(active) {
  UE_LOG(LogTemp, Warning, TEXT("Spec is active now!"));
  UE_LOG(LogTemp, Warning, TEXT("Spec.IsActive(): %i"), Spec.IsActive());
  UE_LOG(LogTemp, Warning, TEXT("Spec.ActiveCount: %i"), Spec.ActiveCount);
  UE_LOG(LogTemp, Warning, TEXT("Spec.GetAbilityInstances().Num(): %i"), Spec.GetAbilityInstances().Num());
  UE_LOG(LogTemp, Warning, TEXT("Spec.InputPressed: %i"), Spec.InputPressed);
}

This is the output:

LogTemp: Warning: Spec is active now!
LogTemp: Warning: Spec.IsActive(): 0
LogTemp: Warning: Spec.ActiveCount: 0
LogTemp: Warning: Spec.GetAbilityInstances().Num(): 0
LogTemp: Warning: Spec.InputPressed: 1

So, the ability activates but Spec.ActiveCount is not increased. Thus, Spec.IsActive() in AbilityLocalInputReleased will never be true and the WaitInputRelease task in the ability can never return.

So, the main issue boils down to why is Spec.ActiveCount not increased when using my custom input bindings?.

Thanks!
Nico