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:
Name | Value |
---|---|
Action | IA_Ability_Dash |
TriggerEvent | ETriggerEvent::Triggered |
Object | Lyra_Hero i.e. the player object, a LyraPawnComponent |
PressedFunc | LyraHeroComponent::Input_AbilityInputTagPressed |
Action.InputTag | "InputTag.Ability.Dash" |