6 minute read

In this post we’ll see how to employ the Enhanced Input Plugin alongside the Gameplay Ability System Plugin in multiplayer games.

Introduction

If you haven’t used the Enhanced Input Plugin, I highly recommend that you do give it a try before reading this article.

This post isn’t a tutorial about the Enhanced Input Plugin, so I encourage readers to explore further on how its interface works. However, for the sake of completion I’ll highlight various features from the Plugin that are relevant for this post:

  • Input Action: Asset that defines an action in our game and interacts with the input system through Input Mapping Contexts.
  • BindAction: Binds an Input Action to a function and returns a handle.
  • RemoveBindingByHandle: Removes the binding associated to the passed handle.

Input binding with GAS

Besides having the Ability System Component in the PlayerState, it is common for each Avatar Actor pawn to have different inputs and skills. Therefore, the responsibility for binding and granting abilities should belong to the Avatar Pawn (or to a component in the Avatar).

Input should always be granted in the local client. However, as mentioned in our post about GAS network optimizations, abilities must be granted to our ASC on the server. Avatar Pawns generally grant abilities in the Pawn’s server PossessedBy(AController* NewController) function, however, they don’t reach the local client until the ASC gets a chance to replicate them, as displayed in the following flowchart of GiveAbility:

GiveAbility flowchart

As we can see above, OnGiveAbility is called when the local client receives the ability, so we can start manipulating it locally and therefore binding it to an InputAction.

Receiving abilities in the client

To bind an ability right when it’s received in the client, we can do the following:

  1. Create IAbilityBindingInterface.
  2. Override OnGiveAbility to call the interface function when the ability is received.
  3. Implement IAbilityBindingInterface in every pawn that should bind abilities to input actions.

Let’s first create IAbilityBindingInterface:

UINTERFACE(meta = (CannotImplementInterfaceInBlueprint))
class GAME_API UAbilityBindingInterface : public UInterface
{
	GENERATED_BODY()
};

class GAME_API IAbilityBindingInterface
{
	GENERATED_BODY()

public:

	virtual void BindAbility(struct FGameplayAbilitySpec& Spec) const = 0;

	virtual void UnbindAbility(struct FGameplayAbilitySpecHandle Handle) const = 0;
};

Now, let’s override OnGiveAbility to add the interface call:

void UMyAbilitySystemComponent::OnGiveAbility(FGameplayAbilitySpec& AbilitySpec)
{
	const IAbilityBindingInterface* ABI = Cast<IAbilityBindingInterface>(GetAvatarActor_Direct());
	if (ABI)
	{
		ABI->BindAbility(AbilitySpec);
	}

	Super::OnGiveAbility(AbilitySpec);
}

Finally, let’s implement IAbilityBindingInterface in every pawn that should bind abilities to input actions:

UCLASS()
class GAME_API AMyPlayerCharacter : public ACharacter,
				public IAbilityBindingInterface,
				...
{
...

void AMyPlayerCharacter::BindAbility(FGameplayAbilitySpec& Spec) const
{
	if (IsLocallyControlled()) AbilitySet->BindAbility(AbilityInputBindingComponent, Spec);
}

void AMyPlayerCharacter::UnbindAbility(FGameplayAbilitySpec& Spec) const
{
	if (IsLocallyControlled()) AbilitySet->UnbindAbility(AbilityInputBindingComponent, Spec);
}

At this stage, we can bind Input Actions to Gameplay Abilities. As you can see above, I am using a custom AbilitySet and a component that deals with the binding of the abilities.

AbilitySet to bind abilities

This Section provides a detailed description on how to do a binding once we receive an ability in the client.

The first thing to consider is our AbilitySet, which holds an array of FGameplayAbilityInfo, which is what we’ll configure to determine the Input Actions and Abilities granted to our Pawn:

GiveAbility flowchart

This is the implementation of the data asset:

USTRUCT()
struct FGameplayAbilityInfo
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, Category = BindInfo)
	TSoftClassPtr<class UGameplayAbility> GameplayAbilityClass;

	UPROPERTY(EditAnywhere, Category = BindInfo)
	TSoftObjectPtr<class UInputAction> InputAction;
};

UCLASS()
class GAME_API UAsset_GameplayAbility : public UDataAsset
{
	GENERATED_BODY()

	/** Abilities to add to the ASC */
	UPROPERTY(EditAnywhere, Category = AbilitySet)
	TArray<FGameplayAbilityInfo> Abilities;

public:

	// Must be called on the server
	void GiveAbilities(URBAbilitySystemComponent* AbilitySystemComponent) const;

	// Must be called on the server
	void RemoveAbilities(URBAbilitySystemComponent* AbilitySystemComponent) const;

	// Must be called on local controller
	void BindAbility(UAbilityInputBindingComponent* InputComponent, struct FGameplayAbilitySpec& Spec) const;

	// Must be called on local controller
	void UnbindAbility(UAbilityInputBindingComponent* InputComponent, struct FGameplayAbilitySpec& Spec) const;
};

This AbilitySet is employed to grant abilities and bind input, however, in this post we are only going to focus on the BindAbility and UnbindAbility functions called in the IAbilityBindingInterface functions implementation in our pawns.

Following next, we provide the implementation of both relevant methods:

void UAsset_GameplayAbility::BindAbility(UAbilityInputBindingComponent* InputComponent, FGameplayAbilitySpec& Spec) const
{
	check(Spec.Ability);
	check(InputComponent);

	for (const FGameplayAbilityInfo& BindInfo : Abilities) 
	{
		if (BindInfo.GameplayAbilityClass.LoadSynchronous() == Spec.Ability->GetClass()) 
		{
			InputComponent->SetInputBinding(BindInfo.InputAction.LoadSynchronous(), Spec);
		}
	}
}

void UAsset_GameplayAbility::UnbindAbility(UAbilityInputBindingComponent* InputComponent, FGameplayAbilitySpec& Spec) const
{
	check(InputComponent);
	InputComponent->ClearInputBinding(Spec);
}

As we can see FGameplayAbilitySpec contains information about the ability class, which is all we need to resolve the mapping in the client. The next Section showcases how the input component handles the Ability Binding internally to interact with the Enhanced Input Plugin.

Input component

Our custom InputComponent handles the Enhanced Input Plugin internal details and reacts to input presses and releases:

void UAbilityInputBindingComponent::SetInputBinding(UInputAction* InputAction, FGameplayAbilitySpec& AbilitySpec)
{
	using namespace AbilityInputBindingComponent_Impl;

	if (AbilitySpec.InputID == InvalidInputID)
	{
		AbilitySpec.InputID = GetNextInputID();
	}

	FAbilityInputBinding* AbilityInputBinding = MappedAbilities.Find(InputAction);
	if (!AbilityInputBinding)
	{
		AbilityInputBinding = &MappedAbilities.Add(InputAction);
	}

	AbilityInputBinding->BoundAbilitiesStack.AddUnique(AbilitySpec.Handle);

	if (InputComponent)
	{
		// Pressed event
		if (AbilityInputBinding->OnPressedHandle == 0)
		{
			AbilityInputBinding->OnPressedHandle = InputComponent->BindAction(InputAction, ETriggerEvent::Started, this, &UAbilityInputBindingComponent::OnAbilityInputPressed, InputAction).GetHandle();
		}

		// Released event
		if (AbilityInputBinding->OnReleasedHandle == 0)
		{
			AbilityInputBinding->OnReleasedHandle = InputComponent->BindAction(InputAction, ETriggerEvent::Completed, this, &UAbilityInputBindingComponent::OnAbilityInputReleased, InputAction).GetHandle();
		}
	}
}

This implementation follows partially the design principles about input binding explored in the Valley of the Ancient Sample by Epic Games.

Binding an Ability can be decomposed in the following steps:

  1. Use an increasing ID to assign an Input ID to the incoming AbilitySpec.
  2. Store the InputAction and the AbilitySpec Handle in the AbilityInputBinding map.
  3. Bind the press and release actions and store the handles in the current map entry.

The pressed and the released event are hooked to two functions also present in the Input Component:

void UAbilityInputBindingComponent::OnAbilityInputPressed(UInputAction* InputAction)
{
	// The AbilitySystemComponent may not have been valid when we first bound input... try again.
	if (!AbilityComponent)
	{
		RunAbilitySystemSetup();
	}

	if (AbilityComponent)
	{
		using namespace AbilityInputBindingComponent_Impl;

		FAbilityInputBinding* FoundBinding = MappedAbilities.Find(InputAction);
		if (FoundBinding)
		{
			for (FGameplayAbilitySpecHandle AbilityHandle : FoundBinding->BoundAbilitiesStack)
			{
				FGameplayAbilitySpec* FoundAbility = AbilityComponent->FindAbilitySpecFromHandle(AbilityHandle);
				if (FoundAbility != nullptr && ensure(FoundAbility->InputID != InvalidInputID))
				{
					AbilityComponent->AbilityLocalInputPressed(FoundAbility->InputID);
				}
			}
		}
	}
}

void UAbilityInputBindingComponent::OnAbilityInputReleased(UInputAction* InputAction)
{
	if (AbilityComponent)
	{
		using namespace AbilityInputBindingComponent_Impl;

		FAbilityInputBinding* FoundBinding = MappedAbilities.Find(InputAction);
		if (FoundBinding)
		{
			for (FGameplayAbilitySpecHandle AbilityHandle : FoundBinding->BoundAbilitiesStack)
			{
				FGameplayAbilitySpec* FoundAbility = AbilityComponent->FindAbilitySpecFromHandle(AbilityHandle);
				if (FoundAbility != nullptr && ensure(FoundAbility->InputID != InvalidInputID))
				{
					AbilityComponent->AbilityLocalInputReleased(FoundAbility->InputID);
				}
			}
		}
	}
}

As we can see, the assigned InputID will be used to trigger the ability using AbilityLocalInputPressed and AbilityLocalInputReleased.

To unbind abilities the process goes as follows:

void UAbilityInputBindingComponent::ClearInputBinding(FGameplayAbilitySpec& AbilitySpec)
{
	using namespace AbilityInputBindingComponent_Impl;

	TArray<UInputAction*> InputActionsToClear;
	for (auto& InputBinding : MappedAbilities)
	{
		if (InputBinding.Value.BoundAbilitiesStack.Find(AbilitySpec.Handle))
		{
			InputActionsToClear.Add(InputBinding.Key);
		}
	}

	for (UInputAction* InputAction : InputActionsToClear)
	{
		FAbilityInputBinding* AbilityInputBinding = MappedAbilities.Find(InputAction);
		if (AbilityInputBinding->BoundAbilitiesStack.Remove(AbilitySpec.Handle) > 0)
		{
			if (AbilityInputBinding->BoundAbilitiesStack.Num() == 0)
			{
				// NOTE: This will invalidate the `AbilityInputBinding` ref above
				RemoveEntry(InputAction);
			}
		}
	}

	AbilitySpec.InputID = InvalidInputID;
}

Which can be summarised in the following steps:

  1. Find all the entries that should be removed.
  2. Remove the Handle from the bound abilities array from the Ability binding map.
  3. If an entry of our map ends up with 0 bound abilities, we remove said entry of the map.

And with that, you should have a working Input system!

I encourage all my readers to take a look at the custom InputComponent implementation in the Valley of the Ancient Sample by Epic Games, since this article was heavily inspired by how they deal with input in that project.

Conclusion

Well… that was it from my side! I hope you learned something with this article!

Also! This is not the only way to do Input Binding with GAS, I always recommend to do your own research before blindly following any random internet post, so please, do so!

Feel free to ask any question or provide feedback for these posts, you can find me on twitter, DM’s are open! And by the way, there are more articles to come! I won’t spoil anything this time, but you guys will probably predict what it will be about!

Please let me know if I have forgotten something, there’s a substantial amount of code and information and I may have overlooked something.

As a final remark, I would like to highlight the invaluable work my friend Mirror did proofreading this article. If you are interested in literature and art, go support her stuff!

Enjoy, vori.