Coding Babble  Code it once more, with feeling.

UE5 Enhanced Input in Pure C++

By Luke Simpson

Updated: Written:

Introduction

Most tutorials I’ve seen only cover Enhanced Input for blueprints, but in this post I’ll explain how to implement Enhanced Input in pure C++. We’ll create an input action and input mapping for WASD and Left Stick movement, then tie it all together in the player controller.

Enable Enhanced Input

In your Game.Build.cs file, add capability for Enhanced Input in PublicDependencyModuleNames:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" });

Create classes

  1. Launch the editor.

  2. Go to Tools > New C++ Class

  3. Click All Classes, select InputAction as the parent, and click Next.

  4. I named the class IA_Move.

    • I also put this in an “Input” folder in my project. If you do this, note that Unreal incorrectly adds “Input/” to the include paths for your IA_x and IMC_x files, which you’ll need to remove for the project to build correctly because these files already live in the “Input” folder.
  5. Click Create Class.

  6. Create another class with InputMappingContext as the parent. I called this one IMC_Earth. IMC_Default might also be a good name.

Input action

All I did for the input action class was add a constructor to specify the ValueType:

.h

public:
    UIA_Move();

.cpp

#include "IA_Move.h"

UIA_Move::UIA_Move()
{
	ValueType = EInputActionValueType::Axis2D;
}

We’ll use Axis2D since it will work for both gamepad and keyboard inputs.

Input mapping context

For the context, we add our action, some private variables for our inputs, and a constructor.

.h

#pragma once

#include "CoreMinimal.h"
#include "InputMappingContext.h"
#include "IA_Move.h"
#include "IMC_Earth.generated.h"

UCLASS()
class EARTH_API UIMC_Earth : public UInputMappingContext
{
	GENERATED_BODY()

private:
	UIA_Move *Move;

	UInputModifierNegate *Negate;
	UInputModifierSwizzleAxis *Swizzle;

	FEnhancedActionKeyMapping W;
	FEnhancedActionKeyMapping A;
	FEnhancedActionKeyMapping S;
	FEnhancedActionKeyMapping D;
	FEnhancedActionKeyMapping GamepadLeft2d;

public:
	UIMC_Earth();
};

.cpp

And here we bind our actions to inputs and add them to the Mappings array.

#include "IMC_Earth.h"
#include "IA_Move.h"

UIMC_Earth::UIMC_Earth()
{
	Move = NewObject<UIA_Move>();

	Negate = NewObject<UInputModifierNegate>();
	Swizzle = NewObject<UInputModifierSwizzleAxis>();
	Swizzle->Order = EInputAxisSwizzle::YXZ;

	W = FEnhancedActionKeyMapping(Move, EKeys::W);
	W.Modifiers.Add(Swizzle);
	DefaultKeyMappings.Mappings.Add(W);

	A = FEnhancedActionKeyMapping(Move, EKeys::A);
	A.Modifiers.Add(Negate);
	DefaultKeyMappings.Mappings.Add(A);

	S = FEnhancedActionKeyMapping(Move, EKeys::S);
	S.Modifiers.Add(Negate);
	S.Modifiers.Add(Swizzle);
	DefaultKeyMappings.Mappings.Add(S);

	D = FEnhancedActionKeyMapping(Move, EKeys::D);
	DefaultKeyMappings.Mappings.Add(D);

	GamepadLeft2d = FEnhancedActionKeyMapping(Move, EKeys::Gamepad_Left2D);
	DefaultKeyMappings.Mappings.Add(GamepadLeft2d);
}

To understand what Negate and Swizzle do, first take a look at the D key, which has no modifiers. When D is pressed, it will return a positive X-axis value of 1.0.

Configure the player controller

You could also set this up in the Character or Pawn classes, but I like using the Controller for input.

.h

  1. In your controller .h, import your IMC. In this case, we do include the “Input” folder because my PlayerController class is one directory above it:

    #include "Input/IMC_Earth.h"
    
  2. and add the following properties and methods:

    public:
        UPROPERTY(EditAnywhere, Category = "Input")
        TSoftObjectPtr<UInputMappingContext> InputMapping;
    protected:
        virtual void SetupInputComponent() override;
    private:
        virtual void MoveCallback(const FInputActionInstance& Instance);
    

.cpp

Next, we’ll add this #define at the top of our .cpp to conveniently print messages to the screen. We’ll use it to test if our callback works.

#define print(text) if (GEngine) GEngine->AddOnScreenDebugMessage(-1, 1.5, FColor::White, text)
  1. Add the needed headers:

    #include <EnhancedInputComponent.h>
    #include <EnhancedInputSubsystems.h>
    
  2. If your player controller doesn’t have a constructor, go ahead and add one. Then initialize your InputMapping inside of it:

     AEarthPlayerController::AEarthPlayerController()
     {
         InputMapping = NewObject<UIMC_Earth>();
     }
    
  3. Add your input setup. This is where we bind our action to our callback function.

     void AEarthPlayerController::SetupInputComponent()
     {
         print("setup input component called");
         Super::SetupInputComponent();
    
         // only add IMCs for local player controllers
         if (IsLocalPlayerController())
         {
             print("we are a local player");
    
             if (UEnhancedInputLocalPlayerSubsystem *Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
             {
                 print("we have an input system");
    
                 if (!InputMapping.IsNull())
                 {
                     print("we have an input mapping");
    
                     Subsystem->AddMappingContext(InputMapping.LoadSynchronous(), 0);
    
                     FEnhancedActionKeyMapping move = InputMapping->GetMapping(0);
    
                     UEnhancedInputComponent *Input = Cast<UEnhancedInputComponent>(InputComponent);
                     Input->BindAction(move.Action, ETriggerEvent::Triggered, this, &AEarthPlayerController::MoveCallback);
                 }
             }
         }
     }
    

    In my case, I have a default game mode that specifies a default pawn and controller, and I have a Player Start actor sitting in my level. This means as soon as the game is launched, the controller will auto possess the pawn, and I will have a local player. Your case may be different, so just something to keep in mind.

  4. Add the callback for your action.

    void AEarthPlayerController::MoveCallback(const FInputActionInstance& Instance)
    {
        FVector2D Value = Instance.GetValue().Get<FVector2D>();
        print(Value.ToString());
    }
    

And that should do it. If you run the game, you should see the x/y values corresponding to your inputs printed to the screen.

Adding more actions

One important thing to note is that order matters when you add your mappings:

DefaultKeyMappings.Mappings.Add(W);

The key/action map of W lives at index 0 in the Mappings array. We retrieved this mapping in our player controller like so:

// Corresponds to the mapping of W.
FEnhancedActionKeyMapping move = InputMapping->GetMapping(0);

Note that we don’t really care we’re just grabbing the W key here. All we need is the action it’s associated with for the BindAction method.

Input->BindAction(move.Action, ETriggerEvent::Triggered, this, &AEarthPlayerController::MoveCallback);

Once we bind one input callback for IA_Move, any other inputs associated with IA_Move are bound as well, including the other keys and gamepad left stick.

For that reason, you might think we could just make UIA_Move *Move public in our IMC class and refer to the action directly, however I tried that and it didn’t work. I’m guessing the mappings are rebuilt when Subsystem->AddMappingContext is called.

In any case, if you were to add a new input for, say, jumping:

DefaultKeyMappings.Mappings.Add(GamepadFaceBottom);

The index would be 5, assuming we added it after our other move inputs.

FEnhancedActionKeyMapping jump = InputMapping->GetMapping(5);

In my opinion, it’s not a great API and a bit unwieldy since all we need is a reference to our IA_x actions, but it’s the best we have as far as I know. We just have to keep the order in mind and use the right index.

If your game supports gamepad and keyboard, you can organize things by putting all your gamepad inputs first, since each input on a gamepad usually corresponds to one action, whereas multiple keys on a keyboard can trigger the same action, ie., WASD for movement.

    // gamepad
    // 0
    GamepadLeft2d = FEnhancedActionKeyMapping(Move, EKeys::Gamepad_Left2D);
    GamepadLeft2d.Modifiers.Add(DeadZone);
    DefaultKeyMappings.Mappings.Add(GamepadLeft2d);

    // 1
    GamepadRight1d = FEnhancedActionKeyMapping(Zoom, EKeys::Gamepad_RightY);
    GamepadRight1d.Modifiers.Add(DeadZone);
    DefaultKeyMappings.Mappings.Add(GamepadRight1d);

    // 2
    GamepadFaceBottom = FEnhancedActionKeyMapping(Select, EKeys::Gamepad_FaceButton_Bottom);
    GamepadFaceBottom.Triggers.Add(Released);
    DefaultKeyMappings.Mappings.Add(GamepadFaceBottom);

    // 3
    GamepadFaceRight = FEnhancedActionKeyMapping(Pause, EKeys::Gamepad_FaceButton_Right);
    GamepadFaceRight.Triggers.Add(Released);
    DefaultKeyMappings.Mappings.Add(GamepadFaceRight);

    ...

    // keyboard inputs here

that way it’s easier to keep track of which action belongs to which index:

    FEnhancedActionKeyMapping move = InputMapping->GetMapping(0);
    FEnhancedActionKeyMapping zoom = InputMapping->GetMapping(1);
    FEnhancedActionKeyMapping select = InputMapping->GetMapping(2);
    FEnhancedActionKeyMapping pause = InputMapping->GetMapping(3);

Other tips

You can use triggers to modify how the action is triggered, such as on release instead of on press, or more complex interactions like hold and release.

Released = NewObject<UInputTriggerReleased>();
GamepadFaceBottom.Triggers.Add(Released);

You might have also noticed my use of the dead zone modifier, which I created like so:

DeadZone = NewObject<UInputModifierDeadZone>();
DeadZone->LowerThreshold = 0.08;
GamepadLeft2d.Modifiers.Add(DeadZone);

For my pause action, I had to add bTriggerWhenPaused in my IMC class so that I could unpause the game:

Pause = NewObject<UIA_Pause>();
Pause->bTriggerWhenPaused = true;

That’s it for now!

Support the blog! Buy a t-shirt or a mug! Or simply donate 🧋 if you found this post helpful. Thanks!


Check out my other projects:

Tags: