Data-driven Enhanced Input

Course days 9–10 (input config) · DataAsset_InputConfig, WarriorInputComponent, WarriorHeroCharacter::SetupPlayerInputComponent · 10 min

In one breath

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 / classWhat 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.
UEnhancedInputLocalPlayerSubsystemPer-player. You add mapping contexts to it with a priority. Swapping contexts is how you'd do "on foot" vs "menu" controls.
UEnhancedInputComponentWhere 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;
};

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);
The hidden setting that makes it work

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?

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.