Automate Enhanced Input and Gameplay Ability System (GAS) input bindings

Both, the Enhanced Input and the Gameplay Ability systems provide for more modularity and gameplay driven interactions in projects. Unreal Engine 5 even uses Enhanced Input as its default input system. But how do you make these two systems work together?


The Problem:

Enhanced Input uses Input Actions to bind to callbacks (i.e. the logic which should run for an input action, e.g. when it gets triggered)

The Gameplay Ability System on the other hand, associates int32 values (so called InputIDs) to Gameplay Abilities, to decide which ability gets activated or deactivated, when the Ability System Component receives input for the associated int32 value. (e.g. the value of 42 is associated with an ability, lets say GA_FireGun).

See, UAbilitySystemComponent::AbilityLocalInputPressed(int32 InputID) and,
UAbilitySystemComponent::AbilityLocalInputReleased(int32 InputID)


There is no easy way to get them to work together, or is there?

Yes there is! I will show you one of the best approaches to achieve automated Input Action to Gameplay Ability input bindings.

Before we do that, let's take a look at the approaches other people have taken to solve this problem, and how we can improve upon them.

  • Brunocoimbra's approach, makes use of enum values to bind Input Actions to Gameplay Abilities. First of all, a huge thanks to Bruno for coming up with and sharing their solution with us! I am always appreative of people who take their time to help others 😄.
    1. Create an enum for each ability, for example according to the type of ability.
    UENUM(BlueprintType)
    enum class EAbilityInputID : uint8
    {
        LeftGamepadButtonAbility,
        RightGamepadButtonAbility,
        UpGamepadButtonAbility,
        DownGamepadButtonAbility
    }
    
    1. Then use values of this enum to facilitate the Input Action (Enhanced Input) to int32 InputID (GAS) mappings.
    void AMyCharacter::SendAbilityLocalInput(const FInputActionValue& InValue, 
    const TSubclassOf<UGameplayAbility> InAbilityClass, 
    const EAbilityInputID InInputID)
    {
        //Assumes that the MyCharacter class 
        //implements the IAbilitySystemInterface
        
        TObjectPtr<UAbilitySystemComponent> ASC = GetAbilitySystemComponent();
        
        if(ASC && InAbilityClass)
        {
            if(InValue.Get<bool>())
            {
                ASC->AbilityLocalInputPressed(static_cast<int32>(InInputID));
            }else
            {
                ASC->AbilityLocalInputReleased(static_cast<int32>(InInputID));
            }
        }
    }
    
    1. And finally, bind the Input Action to a callback which in turn calls our SendAbilityLocalInput function from Step 2.
    // Callback function
    // LeftAbilityClass is a TSubclassOf<UGameplayAbility> type property
    
    void AMyCharacter::HandleLeftAbilityActionTriggered
    (const FInputActionValue& InValue)
    {
        SendAbilityLocalInput(InValue, LeftAbilityClass, 
        EAbilityInputID::LeftGamepadButtonAbility);
    }
    
    // Binding
    
    void AMyCharacter::SetupPlayerInputComponent
    (UInputComponent* InPlayerInputComponent)
    {
        Super::SetupPlayerInputComponent(InPlayerInputComponent);
        
        if(UEnhancedInputComponent* EIC 
            = Cast<UEnhancedInputComponent>(InPlayerInputComponent))
        {
            //LeftInputAction is a property of type UInputAction*)
            
            if(LeftInputAction)
            {
                //Bind to the callback
                
                EIC->BindAction(LeftInputAction, ETriggerEvent::Triggered,
                this, &AMyCharacter::HandleLeftAbilityActionTriggered);
            }
        }
    }
    

Now you might be thinking, how can we improve on Brunocoimbra's approach?

  • Creating the enum and manually assigning its values to each ability is not really scalable
  • int32 InputIDs for each Gameplay Ability should ideally be unique, to avoid conflicts. Using the same enum value for two different Gameplay Abilities breaks this uniqueness.
  • the Input Action needs to be done manually for each Gameplay Ability. This is not scalable if we have a lot of differeng abilities.

The solution I am about to show you, while getting rid of these issues, also completely automates the binding process for Gameplay Abilities. This is how it works:

  • for every Gameplay Ability, you associate an Input Action with it
    • NOTE: If you want unique IA to GA mappings, don't use the same IA for two or more abilities.
  • whenever a Gameplay Ability gets granted to the Ability System Component, it automatically binds its Input Action to that ability inside the ASC.

Now, the binding is automated for you!


Let us take a look at how to implement this.

  1. Create a subclass of UInputAction

    /**
     * Input Action subclass which has a mechanism to provide unique int32 values 
     * for use with the Ability System
     */
    UCLASS()
    class AUTOMATEDBINDING_API UCustomInputAction : public UInputAction
    {
        GENERATED_BODY()
    
    protected:
        //FGuids are globally unique.
        UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GUID")
        FGuid InputActionGuid;
    
    public:
        /**
         * @brief Creates a unique int32 value from the GUID of this Input Action
         * @return InputID which can be used with the Ability System
         */
        int32 GetInputID() const
        {
            //FGuids are globally unique. They are made out of four int32 values
            //i.e. the quadruple of these int32 values is unique
            //and so adding them up together, 
            //returns a globally unique int32 value
            
            return static_cast<int32>(InputActionGuid.A)
            + static_cast<int32>(InputActionGuid.B)
            + static_cast<int32>(InputActionGuid.C)
            + static_cast<int32>(InputActionGuid.D);
        }
    };
    
  2. Create a UGameplayAbility subclass

    /**
     * 
     */
    UCLASS()
    class AUTOMATEDBINDING_API UCustomGameplayAbility : public UGameplayAbility
    {
        GENERATED_BODY()
    
    public:
        UCustomGameplayAbility();
    
        /**
         * @brief the Input Action which will be used to activate this ability
         */
        UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input")
        TObjectPtr<UCustomInputAction> InputAction;
    
        FORCEINLINE
        /**
         * Get the InputID for this InputAction
         */
         int32 GetInputID() const 
         { 
             return InputAction ? InputAction->GetInputID() : -1;
         }
    
        void OnInputCallback(const FInputActionValue& InValue);
    
        int32 InputBindingHandle;
        
        // We override this function, to set our private 
        // AbilitySystemComponent property (see below)
        virtual void OnGiveAbility(const FGameplayAbilityActorInfo* ActorInfo, 
        const FGameplayAbilitySpec& Spec) override;
        
    private:
        int32 InputID;
    
        UPROPERTY()
        UAbilitySystemComponent* AbilitySystemComponent;
    };
    
    //CustomGameplayAbility.cpp file
    
    void UCustomGameplayAbility::OnGiveAbility
    (const FGameplayAbilityActorInfo* ActorInfo, 
    const FGameplayAbilitySpec& Spec)
    {
        Super::OnGiveAbility(ActorInfo, Spec);
    
        AbilitySystemComponent = ActorInfo->AbilitySystemComponent;
    }
    
    void UCustomGameplayAbility::OnInputCallback
    (const FInputActionValue& InValue)
    {
    
        if(!AbilitySystemComponent)
        {
            return;
        }
    
        if(InValue.Get<bool>())
        {
            AbilitySystemComponent->AbilityLocalInputPressed(InputID);
        }else
        {
            AbilitySystemComponent->AbilityLocalInputReleased(InputID);
        }
    }
    
  3. Create a UAbilitySystemComponent subclass

    /**
     * 
     */
    UCLASS()
    class AUTOMATEDBINDING_API UCustomAbilitySystemComponent 
        : public UAbilitySystemComponent
    {
        GENERATED_BODY()
        
        public:
            //InputAction from the Gameplay Ability will be bound here
            virtual void OnGiveAbility
            (FGameplayAbilitySpec& AbilitySpec) override;
    }
    
    //CustomAbilitySystemComponent.cpp file
    void UCustomAbilitySystemComponent::OnGiveAbility
    (FGameplayAbilitySpec& AbilitySpec)
    {
        Super::OnGiveAbility(AbilitySpec);
    
        UCustomGameplayAbility* Ability 
            = Cast<UCustomGameplayAbility>(AbilitySpec.Ability);
        ACharacter* Character = Cast<ACharacter>(GetAvatarActor());
    
        if(!Ability || !Character)
        {
            return;
        }
    
        UEnhancedInputComponent* EnhancedInputComponent 
            = Cast<UEnhancedInputComponent>(Character->InputComponent);
    
        if(EnhancedInputComponent 
            && Ability->InputAction && Ability->bNeedsInput)
        {
            Ability->InputBindingHandle 
                = EnhancedInputComponent
                ->BindAction(Ability->InputAction, ETriggerEvent::Triggered,
                Ability, &UCustomGameplayAbility::OnInputCallback).GetHandle();
        }
    }
    
    //Important Note:
    //Override the OnRemoveAbility function to unbind the IA
    //when an ability gets removed from the ASC
    //
    //For this, you can use the InputBindingHandle int32 value stored in the
    //UCustomGameplayAbility class
    //
    //See the RemoveBindingByHandle function of the Enhanced Input Component
    

    Done!


This solution is by no means perfect, but does achieve a certain level of automation and works with multiplayer as well.

Hope this helped you! Do subscribe to my blog for more Unreal Engine related content and comment if you have any questions or feedback.