Data-driven Enhanced Input
Instead of the character holding a variable per input action, a data asset holds a list of
"tag → input action" pairs. The character says "bind InputTag.Move to Input_Move".
A custom input component looks the tag up in the data asset and does the normal binding. That keeps the code
the same when the input actions change, and sets up binding abilities by tag later.
The Enhanced Input pieces
| Asset / class | What it is |
|---|---|
Input Action (IA_Move) | An abstract action ("move") and its value type (Vector2D). It knows nothing about keys. |
Input Mapping Context (IMC_Default) | The keys → actions table (WASD → IA_Move), plus modifiers (negate, swizzle) and triggers. |
UEnhancedInputLocalPlayerSubsystem | Per-player. You add mapping contexts to it with a priority. Swapping contexts is how you'd do "on foot" vs "menu" controls. |
UEnhancedInputComponent | Where you bind an action + trigger event to a function. |
Walkthrough of your code
1. The data asset
USTRUCT(BlueprintType)
struct FWarriorInputActionConfig {
UPROPERTY(EditDefaultsOnly, meta = (Categories = "InputTag"))
FGameplayTag InputTag;
UPROPERTY(EditDefaultsOnly)
UInputAction* InputAction = nullptr;
};
UCLASS()
class UDataAsset_InputConfig : public UDataAsset {
UPROPERTY(EditDefaultsOnly) UInputMappingContext* DefaultInputMappingContext;
UPROPERTY(EditDefaultsOnly, meta = (TitleProperty = "InputTag"))
TArray<FWarriorInputActionConfig> NativeInputActions;
UInputAction* FindNativeInputActionByTag(const FGameplayTag& InInputTag) const;
};
UDataAsset: a UObject whose only job is to hold data you edit in the Content Browser. There's no actor or level involved. Several characters can share one.TitlePropertyshows each array element by its tag in the Details panel instead of "Index [0]".FindNativeInputActionByTagis a plain linear search. It only runs at setup, over a handful of entries, so aTMapwould be over-engineering.
2. The custom input component and the template
template<class UserObject, typename CallBackFunc>
void BindNativeInputAction(const UDataAsset_InputConfig* InInputConfig,
const FGameplayTag& InInputTag, ETriggerEvent TriggerEvent,
UserObject* ContextObject, CallBackFunc Func)
{
checkf(InInputConfig, TEXT("Input Config data asset is null..."));
if (UInputAction* FoundAction = InInputConfig->FindNativeInputActionByTag(InInputTag))
BindAction(FoundAction, TriggerEvent, ContextObject, Func);
}
Why a template? BindAction must work for any class and any member function. A
template says "I'll fill in these types when you call me". The compiler works them out from the arguments:
UserObject = AWarriorHeroCharacter, CallBackFunc =
void (AWarriorHeroCharacter::*)(const FInputActionValue&) (a pointer to a member function). You
never type those out.
Why is its body in the header? A template isn't compiled until it's used. The .cpp that calls it needs the whole body visible to generate the real function. If you put the body in a .cpp, you get linker errors.
if (X* Found = ...) declares and tests in one line. Found only exists inside
the if.
3. The character wires it up
checkf(InputConfigDataAsset, TEXT("Forgot to assign..."));
Subsystem->AddMappingContext(InputConfigDataAsset->DefaultInputMappingContext, 0);
UWarriorInputComponent* WarriorInputComponent = CastChecked<UWarriorInputComponent>(PlayerInputComponent);
WarriorInputComponent->BindNativeInputAction(InputConfigDataAsset, WarriorGamePlayTags::InputTag_Move,
ETriggerEvent::Triggered, this, &ThisClass::Input_Move);
&ThisClass::Input_Move:ThisClassis a typedef thatGENERATED_BODY()gives you, equal toAWarriorHeroCharacter.&Class::Funcis a pointer to a member function.ETriggerEvent::Triggeredfires every frame while the input is held (for the default trigger). That's what you want for movement. For a button press you'd usually wantStarted.CastCheckedcrashes (in dev builds) if the cast fails, instead of returning null. It's used when a failure means the project is set up wrong, not something to handle at runtime.
Why is the PlayerInputComponent Unreal hands you a UWarriorInputComponent at all? Because
Config/DefaultInput.ini has
DefaultInputComponentClass=/Script/WadOfGore.WarriorInputComponent (Project Settings → Input →
Default Classes). If that ever gets reset, CastChecked crashes at startup. Remember this when you set up
a new project.
Why bother with the indirection?
- Designers can swap actions and mappings in a data asset without touching C++.
- The same pattern will bind abilities by tag: "whatever ability is tagged
InputTag.LightAttack, activate it on press". The character never needs to know which abilities exist. - It's the pattern Epic uses in Lyra (
ULyraInputConfig), so it's worth being able to talk about.
Quiz yourself
What's the difference between an Input Action and an Input Mapping Context?
The action is what (move, with a Vector2D value). The mapping context is which keys produce it, with modifiers/triggers. Contexts get added to the local player subsystem with a priority.
Why must BindNativeInputAction's body be in the header?
It's a template. The compiler generates the real function where it's called, so the full body must be visible there. Put it in a .cpp and you get unresolved externals.
What does &ThisClass::Input_Move evaluate to, and where does ThisClass come from?
A pointer to the member function AWarriorHeroCharacter::Input_Move. ThisClass is a typedef added by GENERATED_BODY().
The game crashes on CastChecked<UWarriorInputComponent> in a fresh project. What did you forget?
Setting Default Input Component Class to WarriorInputComponent (Project Settings → Input, saved in DefaultInput.ini).
Triggered vs Started: which for movement, which for an attack press?
Movement: Triggered (every frame while held). A single press action: usually Started (once, on press).
Why a linear search in FindNativeInputActionByTag instead of a TMap?
It runs once at setup over a handful of entries. A map adds complexity (and doesn't show nicely in the editor) for no measurable gain.