1. 기존 입력 시스템의 한계
언리얼 엔진 4까지 사용되던 기존 입력 시스템은 Action Mapping과 Axis Mapping이라는 두 가지 방식으로 입력을 처리했습니다. Action Mapping은 점프나 공격처럼 눌림/해제만 판별하는 불리언 입력을, Axis Mapping은 캐릭터 이동이나 카메라 회전처럼 연속적인 값(-1.0 ~ 1.0)을 반환하는 입력을 담당했습니다.
이 방식은 간단한 프로젝트에서는 충분했지만, 프로젝트 규모가 커질수록 다음과 같은 문제가 드러났습니다.
첫째, 모든 입력 설정이 프로젝트 설정(DefaultInput.ini)에 중앙 집중되어 있었습니다. 하나의 파일에 모든 입력 바인딩이 모여 있으니, 여러 개발자가 동시에 작업하거나 다양한 입력 모드를 관리할 때 충돌이 빈번했습니다.
둘째, 입력 컨텍스트 전환이 번거로웠습니다. 예를 들어 "걷기 모드"에서 "탑승 모드"로 전환할 때, 기존 바인딩을 해제하고 새 바인딩을 수동으로 등록해야 했습니다. 이 과정에서 바인딩 누락이나 중복 등록 같은 버그가 쉽게 발생했습니다.
셋째, 홀드(Hold), 더블탭(Double Tap), 동시 입력(Chorded Action) 같은 복잡한 입력 조건을 구현하려면 별도의 블루프린트나 C++ 로직을 직접 작성해야 했습니다. 엔진 차원에서 제공하는 기능이 없었기 때문에, 프로젝트마다 입력 처리 코드가 제각각이었습니다.
이러한 한계를 근본적으로 해결하기 위해 언리얼 엔진 5에서는 Enhanced Input System이 도입되었습니다. UE 4.26에서 플러그인 형태로 처음 등장한 뒤, UE 5.1부터는 기본 입력 시스템으로 자리 잡았습니다.
2. Enhanced Input System의 핵심 개념
Enhanced Input System은 입력 처리를 세 가지 핵심 요소로 분리합니다. 각 요소가 독립적인 에셋으로 존재하기 때문에, 입력 설정을 데이터 드리븐(Data-Driven) 방식으로 관리할 수 있습니다.
2-1. Input Action (UInputAction)
Input Action은 "플레이어가 무엇을 하고 싶은가"를 추상적으로 정의하는 데이터 에셋입니다. 점프, 이동, 공격 등 각각의 행동 하나에 Input Action 하나가 대응됩니다. 중요한 점은, Input Action은 어떤 키가 이 행동을 실행하는지를 정의하지 않는다는 것입니다. 키 바인딩은 뒤에서 설명할 Input Mapping Context가 담당합니다.
Input Action에는 ValueType 속성이 있으며, 입력 데이터의 차원을 결정합니다.
| ValueType | 반환 타입 | 용도 예시 |
|---|---|---|
Boolean |
bool |
점프, 공격 등 온/오프 입력 |
Axis1D |
float |
트리거 버튼, 단일 축 입력 |
Axis2D |
FVector2D |
게임패드 스틱, WASD 이동 |
Axis3D |
FVector |
모션 컨트롤러 등 3D 입력 |
기존 시스템의 Action Mapping은 Boolean 타입에, Axis Mapping은 Axis1D 타입에 대응된다고 볼 수 있습니다. 하지만 Enhanced Input에서는 Axis2D와 Axis3D까지 지원하므로, 게임패드 스틱 입력을 하나의 Input Action으로 X/Y 값을 동시에 받을 수 있습니다. 기존 시스템에서는 MoveForward와 MoveRight를 별도의 Axis Mapping으로 나눠야 했던 것과 비교하면 큰 개선입니다.
2-2. Input Mapping Context (UInputMappingContext)
Input Mapping Context(IMC)는 "어떤 키가 어떤 Input Action을 실행하는가"를 정의하는 데이터 에셋입니다. 하나의 IMC 안에 여러 Input Action과 키 매핑을 등록할 수 있으며, 런타임에 IMC를 추가하거나 제거함으로써 입력 컨텍스트를 자유롭게 전환할 수 있습니다.
예를 들어, 도보 이동용 IMC_OnFoot, 차량 탑승용 IMC_Vehicle, UI 메뉴용 IMC_Menu를 각각 만들어 두고, 상황에 따라 활성화할 IMC만 교체하면 됩니다. 기존 시스템에서 바인딩을 하나씩 수동으로 해제하고 다시 등록하던 방식에 비해 훨씬 깔끔합니다.
IMC에는 Priority(우선순위) 값을 설정할 수 있습니다. 여러 IMC가 동시에 활성화되어 있을 때, 우선순위가 높은 IMC의 바인딩이 먼저 처리됩니다. 이를 통해 "기본 이동 입력 위에 특수 스킬 입력을 덮어씌우기" 같은 패턴을 간단하게 구현할 수 있습니다.
2-3. Modifier와 Trigger
Modifier는 원시(raw) 입력 값을 가공하는 전처리기입니다. 키 입력이 들어오면 Modifier가 먼저 값을 변환한 뒤, 그 결과가 Trigger에 전달됩니다. Modifier는 리스트에 등록된 순서대로 순차 실행되며, 이전 Modifier의 출력이 다음 Modifier의 입력이 됩니다.
대표적인 내장 Modifier는 다음과 같습니다.
| Modifier | 역할 |
|---|---|
Negate |
입력 값의 부호를 반전 (예: 1.0 → -1.0) |
Swizzle Input Axis Values |
축 순서를 변경 (예: X축 입력을 Y축으로 전환) |
Dead Zone |
일정 임계값 이하의 입력을 0으로 처리 |
Scalar |
입력 값에 스칼라 배수를 적용 (감도 조절) |
Smooth |
여러 프레임에 걸쳐 입력 값을 부드럽게 보간 |
Response Curve |
커브 에셋을 사용하여 비선형 입력 응답 적용 |
FOV Scaling |
시야각에 따라 카메라 회전 감도를 자동 보정 |
WASD 이동을 하나의 Axis2D Input Action으로 처리하는 경우를 예로 들어 보겠습니다. W 키는 Swizzle Modifier를 적용하여 X축 입력을 Y축으로 변환하고, S 키는 Negate와 Swizzle을 함께 적용하여 음의 Y축 값을 만들어냅니다. A 키는 Negate만 적용하여 음의 X축 값을, D 키는 Modifier 없이 양의 X축 값을 그대로 전달합니다.
Trigger는 Modifier를 거친 입력 값을 기반으로, Input Action이 실제로 활성화(fire)되어야 하는지를 판정합니다.
대표적인 내장 Trigger는 다음과 같습니다.
| Trigger | 동작 |
|---|---|
Down |
키가 눌려 있는 동안 매 프레임 활성화 |
Pressed |
키가 눌리는 순간 한 번 활성화 |
Released |
키가 놓이는 순간 한 번 활성화 |
Hold |
일정 시간 이상 키를 누르고 있을 때 활성화 |
Hold And Release |
일정 시간 누른 뒤 놓을 때 활성화 |
Tap |
짧게 눌렀다 놓을 때 활성화 |
Chorded Action |
다른 Input Action이 활성 상태일 때만 활성화 |
Trigger가 별도로 지정되지 않으면 기본적으로 Down과 유사한 동작을 합니다. 즉, 키가 눌려 있는 동안 매 틱마다 Input Action이 활성화됩니다.
3. ETriggerEvent — 입력 상태의 흐름
Input Action을 코드에서 바인딩할 때는 ETriggerEvent 열거형을 사용하여 어떤 시점에 콜백을 받을지 지정합니다. 이 열거형은 입력의 생명주기를 다섯 단계로 나눕니다.
| ETriggerEvent | 의미 |
|---|---|
Started |
Trigger 조건을 충족하기 시작한 순간 (1회) |
Ongoing |
Trigger 조건을 충족하는 중 (매 틱) |
Triggered |
Trigger 조건이 완전히 성립한 순간 |
Completed |
이전에 Triggered 되었던 액션이 비활성화된 순간 |
Canceled |
Trigger 조건을 충족하지 못하고 중단된 순간 |
가장 많이 사용하는 이벤트는 Triggered입니다. 이동이나 카메라 회전처럼 매 프레임 값을 받아야 하는 경우에 사용합니다. 점프처럼 한 번만 실행하는 입력에도 Triggered를 사용하면 키를 눌렀을 때 한 번 호출됩니다 (Boolean 타입 + 기본 Trigger 조합).
Hold Trigger를 사용하는 경우, Started는 키를 누르기 시작한 순간, Ongoing은 홀드 시간이 채워지기 전까지 매 틱, Triggered는 홀드 시간이 충족된 순간에 호출됩니다. 이처럼 Trigger 종류에 따라 ETriggerEvent의 발생 타이밍이 달라지므로, 원하는 동작에 맞는 조합을 선택하는 것이 중요합니다.
4. C++ 기본 설정 방법
Enhanced Input System을 C++ 프로젝트에서 사용하려면 몇 가지 설정 단계를 거쳐야 합니다.
4-1. 모듈 의존성 추가
프로젝트의 .Build.cs 파일에 EnhancedInput 모듈을 추가합니다.
// MyProject.Build.cs
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput" // Enhanced Input 모듈 추가
});
위의 코드에서 EnhancedInput 모듈을 PublicDependencyModuleNames에 추가하면, Enhanced Input 관련 헤더 파일과 클래스를 프로젝트 전체에서 사용할 수 있게 됩니다.
4-2. 캐릭터 헤더 파일 선언
캐릭터 클래스의 헤더에 Input Action과 Input Mapping Context에 대한 포인터를 선언합니다.
// MyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "MyCharacter.generated.h"
class UInputAction;
class UInputMappingContext;
UCLASS()
class MYPROJECT_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AMyCharacter();
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
// Input Mapping Context
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputMappingContext> DefaultMappingContext;
// Input Actions
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> LookAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> JumpAction;
private:
// 입력 콜백 함수
void OnMove(const FInputActionValue& Value);
void OnLook(const FInputActionValue& Value);
void OnJumpStarted(const FInputActionValue& Value);
};
위 코드에서 TObjectPtr을 사용한 이유는 UE5에서 권장하는 오브젝트 포인터 래퍼이기 때문입니다. UPROPERTY로 EditAnywhere를 지정했으므로, 에디터의 디테일 패널에서 에셋을 직접 지정할 수 있습니다. 각 Input Action은 에디터에서 미리 생성해 둔 데이터 에셋을 할당합니다.
4-3. Mapping Context 등록과 액션 바인딩
소스 파일에서 BeginPlay에서 Mapping Context를 등록하고, SetupPlayerInputComponent에서 각 Input Action에 콜백 함수를 바인딩합니다.
// MyCharacter.cpp
#include "MyCharacter.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
AMyCharacter::AMyCharacter()
{
PrimaryActorTick.bCanEverTick = true;
}
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
// 로컬 플레이어의 Enhanced Input Subsystem을 가져와 Mapping Context 등록
// Subsystem에 대한 자세한 내용은 공식 문서 참고:
// https://dev.epicgames.com/documentation/en-us/unreal-engine/programming-subsystems-in-unreal-engine
if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0); // Priority 0
}
}
}
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// UEnhancedInputComponent로 캐스팅
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
// 이동 — 매 틱 Triggered 이벤트로 값 수신
EnhancedInput->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyCharacter::OnMove);
// 카메라 회전 — 매 틱 Triggered 이벤트로 값 수신
EnhancedInput->BindAction(LookAction, ETriggerEvent::Triggered, this, &AMyCharacter::OnLook);
// 점프 — 키를 누르는 순간 한 번 실행
EnhancedInput->BindAction(JumpAction, ETriggerEvent::Triggered, this, &AMyCharacter::OnJumpStarted);
}
}
위 코드에서 AddMappingContext의 두 번째 인자는 우선순위(Priority)입니다. 숫자가 낮을수록 우선순위가 낮습니다. 여러 IMC가 동시에 활성화되어 있을 때, 높은 우선순위의 IMC가 먼저 입력을 소비합니다.
BindAction은 기존 시스템의 BindAction / BindAxis를 하나로 통합한 함수입니다. ETriggerEvent를 지정하여 어떤 시점에 콜백을 받을지 결정합니다.
4-4. 입력 콜백 함수 구현
각 콜백 함수에서 FInputActionValue를 통해 입력 값을 꺼내 사용합니다.
void AMyCharacter::OnMove(const FInputActionValue& Value)
{
// Axis2D 타입이므로 FVector2D로 변환
const FVector2D MovementVector = Value.Get<FVector2D>();
// 컨트롤러의 Yaw 회전 기준으로 이동 방향 계산
const FRotator ControlRotation = GetControlRotation();
const FRotator YawRotation(0.f, ControlRotation.Yaw, 0.f);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// 이동 입력 적용
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
void AMyCharacter::OnLook(const FInputActionValue& Value)
{
// Axis2D 타입이므로 FVector2D로 변환
const FVector2D LookAxisVector = Value.Get<FVector2D>();
// 컨트롤러에 Yaw/Pitch 회전 입력 추가
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
void AMyCharacter::OnJumpStarted(const FInputActionValue& Value)
{
// ACharacter의 Jump() 호출
Jump();
}
위 코드처럼 FInputActionValue::Get<T>()를 사용하여 Input Action의 ValueType에 맞는 타입으로 값을 가져옵니다. Boolean 타입이면 Get<bool>(), Axis1D이면 Get<float>(), Axis2D이면 Get<FVector2D>()를 사용합니다. 잘못된 타입으로 Get을 호출하면 런타임 경고가 발생하므로, Input Action의 ValueType 설정과 코드의 Get<T>() 타입을 반드시 일치시켜야 합니다.
5. Input Data Config 패턴 — 많은 Input Action 관리하기
프로젝트가 커지면 캐릭터 클래스에 수십 개의 UInputAction* 포인터가 나열될 수 있습니다. 이런 경우, UDataAsset을 상속한 전용 설정 클래스를 만들어 Input Action 참조를 한 곳에 모아두는 패턴이 권장됩니다.
// InputDataConfig.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "InputDataConfig.generated.h"
class UInputAction;
class UInputMappingContext;
UCLASS()
class MYPROJECT_API UInputDataConfig : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, Category = "Input")
TObjectPtr<UInputMappingContext> DefaultMappingContext;
UPROPERTY(EditDefaultsOnly, Category = "Movement")
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditDefaultsOnly, Category = "Movement")
TObjectPtr<UInputAction> LookAction;
UPROPERTY(EditDefaultsOnly, Category = "Movement")
TObjectPtr<UInputAction> JumpAction;
UPROPERTY(EditDefaultsOnly, Category = "Combat")
TObjectPtr<UInputAction> AttackAction;
UPROPERTY(EditDefaultsOnly, Category = "Combat")
TObjectPtr<UInputAction> AimAction;
UPROPERTY(EditDefaultsOnly, Category = "Interaction")
TObjectPtr<UInputAction> InteractAction;
};
위 코드처럼 UDataAsset을 상속한 UInputDataConfig를 만들면, 에디터에서 Content Browser를 통해 데이터 에셋 인스턴스를 생성하고, 모든 Input Action을 한 곳에서 관리할 수 있습니다. 캐릭터 클래스에서는 이 데이터 에셋 하나만 참조하면 되므로 헤더 파일이 깔끔해집니다.
// 캐릭터에서 사용 시
UPROPERTY(EditAnywhere, Category = "Input")
TObjectPtr<UInputDataConfig> InputConfig;
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
// InputConfig에서 각 Input Action을 가져와 바인딩
EnhancedInput->BindAction(InputConfig->MoveAction, ETriggerEvent::Triggered, this, &AMyCharacter::OnMove);
EnhancedInput->BindAction(InputConfig->JumpAction, ETriggerEvent::Triggered, this, &AMyCharacter::OnJumpStarted);
EnhancedInput->BindAction(InputConfig->AttackAction, ETriggerEvent::Triggered, this, &AMyCharacter::OnAttack);
}
}
위 코드처럼 InputConfig->MoveAction 형태로 접근하면, 입력 설정을 교체할 때 데이터 에셋만 바꾸면 되므로 유연성이 크게 높아집니다.
6. 런타임 컨텍스트 전환
Enhanced Input System의 가장 강력한 기능 중 하나는 런타임에 Input Mapping Context를 자유롭게 추가하거나 제거할 수 있다는 점입니다.
void AMyCharacter::EnterVehicle(AMyVehicle* Vehicle)
{
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
// 도보 이동 컨텍스트 제거
Subsystem->RemoveMappingContext(OnFootMappingContext);
// 차량 조작 컨텍스트 추가 (우선순위 1)
Subsystem->AddMappingContext(VehicleMappingContext, 1);
}
}
}
void AMyCharacter::ExitVehicle()
{
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
// 차량 조작 컨텍스트 제거
Subsystem->RemoveMappingContext(VehicleMappingContext);
// 도보 이동 컨텍스트 복원
Subsystem->AddMappingContext(OnFootMappingContext, 0);
}
}
}
위 코드처럼 AddMappingContext와 RemoveMappingContext만으로 입력 모드를 전환할 수 있습니다. 기존 시스템에서는 개별 바인딩을 하나씩 해제하고 다시 등록해야 했던 것과 비교하면, 코드량과 버그 발생 가능성이 크게 줄어듭니다.
7. 기존 Input 시스템과의 상세 비교
기존 시스템에서 Enhanced Input System으로의 전환을 이해하기 위해, 주요 차이점을 구체적으로 비교해 보겠습니다.
7-1. 바인딩 방식의 차이
기존 시스템에서는 SetupPlayerInputComponent에서 문자열 기반으로 액션과 축을 바인딩했습니다.
// 기존 시스템 — 문자열 기반 바인딩
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMyCharacter::Jump);
PlayerInputComponent->BindAxis("MoveForward", this, &AMyCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &AMyCharacter::MoveRight);
}
void AMyCharacter::MoveForward(float Value)
{
AddMovementInput(GetActorForwardVector(), Value);
}
void AMyCharacter::MoveRight(float Value)
{
AddMovementInput(GetActorRightVector(), Value);
}
위 코드에서 "Jump", "MoveForward" 같은 문자열은 프로젝트 설정에 등록된 이름과 정확히 일치해야 합니다. 오타가 있어도 컴파일 오류가 발생하지 않고 런타임에 바인딩이 실패하므로, 디버깅이 어려웠습니다.
Enhanced Input에서는 UInputAction 포인터를 직접 참조하므로, 컴파일 타임에 타입 안전성이 보장됩니다. 또한 전진/후진, 좌/우 이동을 별도의 Axis Mapping으로 나누지 않고, 하나의 Axis2D Input Action으로 통합할 수 있습니다.
7-2. 입력 값의 차원
기존 시스템의 Axis Mapping은 항상 float 하나만 반환했습니다. 게임패드 스틱처럼 2차원 입력이 필요한 경우에도, X축과 Y축을 별도의 Axis Mapping으로 분리해야 했습니다.
Enhanced Input에서는 Axis2D 타입의 Input Action 하나로 FVector2D 값을 직접 받을 수 있습니다. 이는 입력 처리 로직을 단순하게 만들 뿐 아니라, WASD 키보드 입력과 게임패드 스틱 입력을 하나의 Input Action에서 동일하게 처리할 수 있게 합니다.
7-3. 복잡한 입력 조건
기존 시스템에서 "3초 이상 눌러야 활성화" 같은 홀드 입력을 구현하려면 다음과 같은 수동 로직이 필요했습니다.
// 기존 시스템 — 수동 홀드 구현
void AMyCharacter::StartCharge()
{
bIsCharging = true;
ChargeStartTime = GetWorld()->GetTimeSeconds();
}
void AMyCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (bIsCharging)
{
float HeldTime = GetWorld()->GetTimeSeconds() - ChargeStartTime;
if (HeldTime >= 3.0f)
{
ExecuteChargedAttack();
bIsCharging = false;
}
}
}
위 코드처럼 시간 추적 변수와 Tick 로직을 직접 작성해야 했습니다. Enhanced Input에서는 Hold Trigger를 Input Action에 붙이고 Hold Time을 3.0으로 설정하는 것만으로 동일한 동작이 구현됩니다. 별도의 코드가 필요하지 않습니다.
8. 일반 C++과의 비교
Enhanced Input System의 입력 처리 파이프라인은 일반 C++에서의 입력 처리 방식과 대비됩니다.
일반 C++에서 키보드/마우스 입력을 처리하려면, 운영체제의 이벤트 루프에서 입력 이벤트를 직접 폴링(polling)하거나, SDL이나 GLFW 같은 라이브러리의 콜백을 사용합니다.
// 일반 C++ + SDL2 예시
SDL_Event event;
while (SDL_PollEvent(&event))
{
if (event.type == SDL_KEYDOWN)
{
if (event.key.keysym.sym == SDLK_SPACE)
{
Jump();
}
}
}
위 코드에서 볼 수 있듯이, 일반 C++에서는 물리적 키(SDLK_SPACE)와 게임 행동(Jump)이 코드 안에서 직접 연결됩니다. 키를 바꾸려면 코드를 수정해야 합니다.
Enhanced Input System은 이 관계를 데이터 레이어로 분리합니다. UInputAction은 행동을 정의하고, UInputMappingContext는 키 매핑을 정의하며, 두 가지 모두 코드가 아닌 에셋(데이터)으로 존재합니다. 이는 일반 C++ 프로그래밍에서 "설정 파일과 코드 로직을 분리하라"는 원칙을 엔진 차원에서 체계화한 것이라고 볼 수 있습니다.
또한 일반 C++에서는 입력 값의 전처리(데드존, 감도 보정, 축 변환 등)를 모두 직접 구현해야 하지만, Enhanced Input의 Modifier 체인은 이를 재사용 가능한 모듈로 제공합니다. 이는 디자인 패턴 중 Chain of Responsibility 패턴과 유사한 구조입니다.
9. 유니티 엔진과의 비교
유니티 엔진에도 언리얼의 Enhanced Input System과 유사한 목적을 가진 Input System 패키지(New Input System)가 존재합니다.
유니티의 Input System은 Input Action Asset에서 Action Map, Action, Binding을 정의합니다. 이는 언리얼의 Input Mapping Context + Input Action 조합과 기능적으로 유사합니다. 유니티의 Action Map이 언리얼의 Input Mapping Context에, 유니티의 Action이 언리얼의 Input Action에 대응됩니다.
주요 차이점은 다음과 같습니다.
첫째, 에셋 구조가 다릅니다. 유니티는 하나의 .inputactions 파일 안에 모든 Action Map과 Action을 함께 관리합니다. 반면 언리얼은 각 Input Action과 Input Mapping Context가 독립적인 에셋으로 존재합니다. 언리얼의 방식이 에셋 단위 버전 관리와 재사용에 유리합니다.
둘째, 값 전처리 방식이 다릅니다. 유니티는 Processor(Dead Zone, Normalize 등)와 Interaction(Hold, Tap 등)으로 분류합니다. 언리얼은 Modifier(값 가공)와 Trigger(활성화 조건)로 분류합니다. 개념적으로 유사하지만, 언리얼의 Modifier는 체이닝(순차 실행)이 명시적으로 설계되어 있어 복잡한 입력 가공 파이프라인을 구성하기 더 편리합니다.
셋째, 컨텍스트 전환 방식이 다릅니다. 유니티는 InputActionMap.Enable() / Disable()을 호출하여 Action Map 단위로 활성화/비활성화합니다. 언리얼은 AddMappingContext / RemoveMappingContext를 사용하며, 우선순위(Priority) 기반으로 여러 컨텍스트를 동시에 활성화할 수 있습니다. 우선순위 시스템 덕분에, 기본 입력 위에 특수 입력을 레이어링하는 패턴이 더 직관적입니다.
넷째, 언리얼의 Enhanced Input은 Player Mappable Key 기능(UE 5.3 이상)을 통해 런타임 키 리매핑을 엔진 차원에서 지원합니다. 유니티에서는 런타임 리바인딩을 위해 InputActionRebindingExtensions를 사용해야 하며, UI 구성까지 직접 해야 하는 부분이 더 많습니다.
10. 주의사항
10-1. EnhancedInput 모듈 의존성 누락
.Build.cs에 "EnhancedInput"을 추가하지 않으면 UEnhancedInputComponent나 UEnhancedInputLocalPlayerSubsystem 관련 헤더를 포함해도 링크 에러가 발생합니다. 빌드 오류 메시지가 명확하지 않은 경우가 있으므로, Enhanced Input을 사용한다면 가장 먼저 모듈 의존성을 확인하셔야 합니다.
10-2. DefaultInputComponentClass 설정
프로젝트의 DefaultInput.ini에서 기본 입력 컴포넌트 클래스가 UEnhancedInputComponent로 설정되어 있는지 확인해야 합니다. UE5 신규 프로젝트에서는 기본적으로 설정되어 있지만, UE4에서 마이그레이션한 프로젝트에서는 수동으로 변경해야 할 수 있습니다.
[/Script/Engine.InputSettings]
DefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent
위 설정이 빠져 있으면, SetupPlayerInputComponent에서 Cast<UEnhancedInputComponent>가 실패하여 nullptr을 반환합니다. 이 경우 바인딩 코드가 아무 오류 없이 무시되므로, 입력이 전혀 동작하지 않는 원인을 찾기 어려울 수 있습니다.
10-3. Mapping Context 등록 시점
AddMappingContext는 반드시 PlayerController와 LocalPlayer가 유효한 시점에 호출해야 합니다. BeginPlay에서 호출하는 것이 일반적이지만, 멀티플레이어 환경에서는 Controller가 아직 할당되지 않은 시점에 BeginPlay가 호출될 수 있습니다. 이 경우 OnPossessed 이벤트를 활용하는 것이 더 안전합니다.
10-4. ValueType과 Get() 불일치
Input Action의 ValueType을 Axis2D로 설정해 놓고 콜백에서 Value.Get<float>()를 호출하면, 값이 정상적으로 반환되지 않습니다. 엔진은 런타임에 경고 로그를 출력하지만 크래시가 발생하지 않기 때문에, 로그를 확인하지 않으면 원인 파악이 늦어질 수 있습니다. Input Action의 ValueType과 코드의 Get<T>() 호출이 항상 일치하는지 주의하셔야 합니다.
10-5. 기존 프로젝트 마이그레이션 시 주의
UE4 프로젝트를 UE5로 마이그레이션할 때, 기존의 BindAction과 BindAxis 호출을 한 번에 모두 Enhanced Input으로 전환하기 어려울 수 있습니다. 이 경우 Enhanced Input 플러그인 설정에서 레거시 입력과의 공존을 허용하는 옵션을 활용할 수 있지만, 최종적으로는 모든 입력을 Enhanced Input으로 전환하는 것이 권장됩니다. 두 시스템을 오래 혼용하면 입력 우선순위 충돌이나 예측 불가능한 동작이 발생할 수 있습니다.
Enhanced Input System은 입력 처리를 데이터 드리븐 방식으로 체계화하고, 복잡한 입력 조건을 코드 없이 구현할 수 있게 해주는 UE5의 핵심 시스템입니다. 특히 여러 입력 모드를 전환해야 하는 게임이나, 런타임 키 리매핑이 필요한 프로젝트에서 그 진가를 발휘합니다. 새 프로젝트를 시작한다면 처음부터 Enhanced Input을 사용하고, 기존 프로젝트라면 단계적으로 마이그레이션하는 것을 권장합니다.
'언리얼 엔진 5' 카테고리의 다른 글
| [UE5] 언리얼 인터페이스(UInterface) (0) | 2026.03.15 |
|---|---|
| [UE5] 직렬화와 세이브 시스템 (1) | 2026.03.15 |
| [UE5] GPU 최적화 (0) | 2026.03.08 |
| [UE5] Render Thread 최적화 (0) | 2026.03.05 |
| [UE5] Game Thread 최적화 (0) | 2026.03.04 |