UE5 Enhanced Input in Pure C++
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
Launch the editor.
Go to Tools > New C++ Class
Click
All Classes, selectInputActionas the parent, and clickNext.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_xandIMC_xfiles, which you’ll need to remove for the project to build correctly because these files already live in the “Input” folder.
- 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
Click
Create Class.Create another class with
InputMappingContextas 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.
Ahas only theNegatemodifier, which will flip 1.0 to -1.0.Whas only theSwizzlemodifier, which turns an X-axis value to a Y-axis value since we “swizzled” the axes fromXYZtoYXZ.Swill return a negative Y-axis value because it has both aNegateand aSwizzle.
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
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"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)
Add the needed headers:
#include <EnhancedInputComponent.h> #include <EnhancedInputSubsystems.h>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>(); }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.
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:
- Emerald Geography - Learn geography on a 3D globe.
- Word Rummage - Random word generator, word finder, and dictionary.
- Color Changer web extension for Firefox and Chrome - Change colors of websites to make them easier to read.