Skip to main content

Looking into Lyra - Action Mappings

How Lyra binds imput actions to gameplay abilities

About Action Bindings

The ULyraPawnData class is used to define a Lyra pawn.

The pawn contains mappings from an Input Action to either:

  • a gameplay tag, which will lead us to a gameplay ability
  • a native action, which is a function which is executed directly without using a gameplay ability.

The pawn class has a reference to a ULyraInputConfig like so:

This is used to load configuration data which populates the pawns action mappings.

For more information on how input actions are mapped to navite actions and gameplay abilities see Pawn Action Mappings.

Lyra contains a LyraInputConfig structure which holds a list of LyraInputAction items each of which links an gameplay tag with an Input Action:

USTRUCT(BlueprintType)
struct FLyraInputAction
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly)
const UInputAction* InputAction = nullptr;
UPROPERTY(EditDefaultsOnly, Meta = (Categories = "InputTag"))
FGameplayTag InputTag;
};

UCLASS(BlueprintType, Const)
class ULyraInputConfig : public UDataAsset
{
GENERATED_BODY()
public:
ULyraInputConfig(const FObjectInitializer& ObjectInitializer);
const UInputAction* FindNativeInputActionForTag(const FGameplayTag& InputTag, bool bLogNotFound = true) const;
const UInputAction* FindAbilityInputActionForTag(const FGameplayTag& InputTag, bool bLogNotFound = true) 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<FLyraInputAction> NativeInputActions;
// List of input actions used by the owner. These input actions are mapped to a gameplay tag and are automatically
// bound to abilities with matching input tags.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (TitleProperty = "InputAction"))
TArray<FLyraInputAction> AbilityInputActions;
};

A single ULyraInputConfig contains two lists of Input Action -> Gameplay Tag pairs. Each pair maps one input action to one gameplay tag.

There are 5 instances of the ULyraInputConfig used in Lyra:

Continuing with the dash action, we can look at the InputData_Hero data asset and see that the Input Action IA_Ability_Dash is mapped to the InputTag.Ability.Dash gameplay tag:

The ULyraPawnData class is used to define a Lyra pawn. This class has a reference to a ULyraInputConfig like so:

There are 5 LyraPawnData assets each of which uses a corresponding LyraInputConfig. This does not mean there is a one-to-one correspondence - a pawn of class ULyraHeroComponent can call AddAdditionalInputConfig() to add additional input config objects.

The LyraInputConfig object is used in ULyraHeroComponent::InitializePlayerInput(). This includes the code below:

if (const ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
{
if (const ULyraPawnData* PawnData = PawnExtComp->GetPawnData<ULyraPawnData>())
{
if (const ULyraInputConfig* InputConfig = PawnData->InputConfig)
{
const FLyraGameplayTags& GameplayTags = FLyraGameplayTags::Get();

// Register any default input configs with the settings so that they will be applied to the
// player during AddInputMappings
for (const FMappableConfigPair& Pair : DefaultInputConfigs)
{
FMappableConfigPair::ActivatePair(Pair);
}

ULyraInputComponent* LyraIC = CastChecked<ULyraInputComponent>(PlayerInputComponent);
LyraIC->AddInputMappings(InputConfig, Subsystem);
if (ULyraSettingsLocal* LocalSettings = ULyraSettingsLocal::Get())
{
LocalSettings->OnInputConfigActivated.AddUObject(this, &ULyraHeroComponent::OnInputConfigActivated);
LocalSettings->OnInputConfigDeactivated.AddUObject(this, &ULyraHeroComponent::OnInputConfigDeactivated);
}

TArray<uint32> BindHandles;
LyraIC->BindAbilityActions(InputConfig, this,
&ThisClass::Input_AbilityInputTagPressed, &ThisClass::Input_AbilityInputTagReleased, /*out*/ BindHandles);

LyraIC->BindNativeAction(InputConfig, GameplayTags.InputTag_Move,
ETriggerEvent::Triggered, this, &ThisClass::Input_Move, /*bLogIfNotFound=*/ false);
LyraIC->BindNativeAction(InputConfig, GameplayTags.InputTag_Look_Mouse,
ETriggerEvent::Triggered, this, &ThisClass::Input_LookMouse, /*bLogIfNotFound=*/ false);
LyraIC->BindNativeAction(InputConfig, GameplayTags.InputTag_Look_Stick,
ETriggerEvent::Triggered, this, &ThisClass::Input_LookStick, /*bLogIfNotFound=*/ false);
LyraIC->BindNativeAction(InputConfig, GameplayTags.InputTag_Crouch,
ETriggerEvent::Triggered, this, &ThisClass::Input_Crouch, /*bLogIfNotFound=*/ false);
LyraIC->BindNativeAction(InputConfig, GameplayTags.InputTag_AutoRun,
ETriggerEvent::Triggered, this, &ThisClass::Input_AutoRun, /*bLogIfNotFound=*/ false);
}
}
}

This introduces the distinction between native actions, which are implemented in c++ methods, and ability actions which are implemented as gameplay ability objects. So we can see from the above code that the some actions are implemented natively and some are not.

One odd part is the call to LyraIC->AddInputMappings(InputConfig, Subsystem) which would seem to be intended to add mappings from the InputConfig to the ULyraInputComponent LyraIC, but in fact the code in ULyraInputComponent::AddInputMappings() does not use the InputConfig that is passed in.

The code in ULyraHeroComponent::AddAdditionalInputConfig() actually starts the binding of ability actions. An abbreviated version is shown here:

void ULyraHeroComponent::AddAdditionalInputConfig(const ULyraInputConfig* InputConfig)
{
TArray<uint32> BindHandles;
const APawn* Pawn = GetPawn<APawn>();
ULyraInputComponent* LyraIC = Pawn->FindComponentByClass<ULyraInputComponent>();
const APlayerController* PC = GetController<APlayerController>();
const ULocalPlayer* LP = PC->GetLocalPlayer();
UEnhancedInputLocalPlayerSubsystem* Subsystem = LP->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();

if (const ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
{
LyraIC->BindAbilityActions(InputConfig, this, &ThisClass::Input_AbilityInputTagPressed,
&ThisClass::Input_AbilityInputTagReleased, /*out*/ BindHandles);
}
}

ULyraHeroComponent::AddAdditionalInputConfig() then calls LyraIC->BindAbilityActions() which binds the input actions to delegates:

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

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

template<class UserClass, typename FuncType>
void ULyraInputComponent::BindNativeAction(const ULyraInputConfig* InputConfig, const FGameplayTag& InputTag,
ETriggerEvent TriggerEvent, UserClass* Object, FuncType Func, bool bLogIfNotFound)
{
check(InputConfig);
if (const UInputAction* IA = InputConfig->FindNativeInputActionForTag(InputTag, bLogIfNotFound))
{
BindAction(IA, TriggerEvent, Object, Func);
}
}

The line

BindHandles.Add(BindAction(Action.InputAction, ETriggerEvent::Triggered, Object, PressedFunc, Action.InputTag).GetHandle());

creates a transient (i.e. it only exists for the life of the function call) BindAction object which binds the action:

template< class FuncType, class UserClass, typename... VarTypes >
FEnhancedInputActionEventBinding& BindAction(const UInputAction* Action,
ETriggerEvent TriggerEvent,
UserClass* Object,
FuncType Func,
VarTypes... Vars)

When this is called for the dash action, the parameters look like this:

NameValue
ActionIA_Ability_Dash
TriggerEventETriggerEvent::Triggered
ObjectLyra_Hero i.e. the player object, a LyraPawnComponent
PressedFuncLyraHeroComponent::Input_AbilityInputTagPressed
Action.InputTag"InputTag.Ability.Dash"

References