Unity·Unreal 실전

Unreal Enhanced Input 실전: Modifier와 Trigger로 입력 로직을 코드에서 데이터로 빼기

kr-gamedev 2026. 6. 15. 09:02

UE4 시절 프로젝트 세팅에 Action Mapping과 Axis Mapping을 등록하고, BindAxis("MoveForward", ...) 로 묶던 방식을 아직 쓰고 있다면 이 글이 도움이 될 것이다. 레거시 입력 시스템의 진짜 문제는 키 등록이 번거롭다는 게 아니라, "입력을 어떻게 해석할지"가 전부 C++ 코드 안으로 흘러들어온다는 점이다. 게임패드 스틱 데드존 처리, 감도 곱셈, 길게 누르기 판정, 더블탭 감지 같은 로직을 캐릭터 클래스 안에 if 문과 타이머로 직접 짜게 된다. 입력 한 줄 바꾸려고 재컴파일하고, 디자이너는 손도 못 댄다.

Enhanced Input은 UE5의 표준 입력 시스템으로, 입력을 "무엇을(Input Action)"과 "어떻게(Modifier, Trigger)"로 깔끔하게 분리한다. 핵심은 데드존이나 홀드 판정 같은 가공 로직을 코드가 아니라 에셋 데이터로 빼낸다는 것이다. 코드는 "이동 액션이 들어왔다"만 알면 되고, 그 값이 어떻게 다듬어졌는지는 알 필요가 없다.

이 글에서는 IMC와 IA 구조, C++ 바인딩, Modifier와 Trigger 활용, 그리고 실무에서 가장 자주 쓰는 매핑 컨텍스트 동적 전환까지 정리한다.

1. Input Action과 Mapping Context 구조

Enhanced Input은 두 종류의 에셋으로 구성된다. Input Action(IA) 은 "점프", "이동", "발사" 같은 의미 단위다. 값 타입(bool, Axis1D, Axis2D)만 가지며 어떤 키에 묶일지는 모른다. Input Mapping Context(IMC) 는 "키보드는 W/A/S/D를 이동 IA에, 스페이스를 점프 IA에 연결한다" 같은 매핑 묶음이다.

이 분리 덕분에 코드는 IA만 참조하고, 키 배치는 IMC 에셋에서 바꾼다. 컨트롤러용 IMC, 키보드용 IMC를 따로 두고 상황에 맞게 갈아끼우는 식의 설계가 자연스럽게 나온다.

2. C++에서 컨텍스트 등록과 액션 바인딩

먼저 폰이 빙의될 때 로컬 플레이어 서브시스템에 IMC를 추가한다. 그리고 UEnhancedInputComponent 로 캐스팅해 액션별 핸들러를 묶는다.

void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();

    if (APlayerController* PC = Cast<APlayerController>(GetController()))
    {
        if (auto* Subsystem = ULocalPlayer::GetSubsystem<
            UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
        {
            Subsystem->AddMappingContext(DefaultMappingContext, /*Priority=*/0);
        }
    }
}

void AMyCharacter::SetupPlayerInputComponent(UInputComponent* InputComponent)
{
    if (auto* Input = Cast<UEnhancedInputComponent>(InputComponent))
    {
        Input->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyCharacter::Move);
        Input->BindAction(JumpAction, ETriggerEvent::Started,   this, &ACharacter::Jump);
        Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
    }
}

void AMyCharacter::Move(const FInputActionValue& Value)
{
    const FVector2D Axis = Value.Get<FVector2D>();

    const FRotator YawRotation(0, GetControlRotation().Yaw, 0);
    const FVector Forward = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
    const FVector Right   = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

    AddMovementInput(Forward, Axis.Y);
    AddMovementInput(Right,   Axis.X);
}

여기서 ETriggerEvent 가 레거시와의 결정적 차이다. 같은 점프 액션이라도 Started(눌리는 순간), Triggered(조건 충족), Completed(끝남), Ongoing, Canceled 를 각각 다른 함수에 묶을 수 있다. 점프 시작과 가변 점프 높이를 위한 키 떼기를 코드에서 깔끔하게 나눌 수 있다.

3. Input Modifier: 입력 가공을 데이터로

Move 핸들러를 다시 보면 데드존 처리도, 감도 곱셈도 없다. 그 일을 IMC의 매핑마다 붙는 Modifier 가 대신한다. 자주 쓰는 것들은 다음과 같다.

  • Dead Zone: 아날로그 스틱의 미세 흔들림 제거
  • Scalar: 마우스 감도나 이동 속도 배율
  • Negate: 부호 반전(S키를 뒤로 이동으로)
  • Swizzle Input Axis Values: 1D 입력의 축 순서 재배치(W/S를 Y축에 매핑)
  • Smooth: 입력값 보간으로 부드러운 카메라

WASD 이동을 만들 때 보통 W/S 매핑에 Swizzle와 Negate를, 스틱 입력에는 Dead Zone을 붙인다. 감도를 바꾸고 싶으면 Scalar 값만 에디터에서 수정하면 끝이고, 재컴파일이 없다. 커스텀 Modifier가 필요하면 UInputModifier 를 상속해 ModifyRaw 만 오버라이드하면 된다.

4. Input Trigger: 홀드와 탭을 코드 없이

타이머로 짜던 "길게 누르기", "짧게 탭", "연타" 판정도 Trigger 가 데이터로 처리한다.

  • Hold: 지정한 시간 이상 누르면 발동(스킬 차징, 메뉴 길게 누르기)
  • Tap: 짧게 눌렀다 떼면 발동
  • Pulse: 누르고 있는 동안 주기적으로 발동(연사)
  • Chorded Action: 다른 액션이 활성일 때만 발동(Shift와 조합한 달리기)

예를 들어 상호작용 키에 Hold 트리거(0.5초)를 붙이면, 핸들러는 ETriggerEvent::Triggered 한 번만 받으면 된다. "얼마나 오래 눌렀나"를 세는 코드가 통째로 사라진다.

5. 매핑 컨텍스트 동적 전환

실무에서 가장 강력한 패턴이다. 도보 상태와 차량 탑승 상태에서 같은 키가 전혀 다르게 동작해야 할 때, 상태 플래그를 분기하는 대신 컨텍스트 자체를 갈아끼운다.

void AMyCharacter::EnterVehicle()
{
    if (auto* PC = Cast<APlayerController>(GetController()))
    {
        if (auto* Subsystem = ULocalPlayer::GetSubsystem<
            UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
        {
            Subsystem->RemoveMappingContext(OnFootContext);
            Subsystem->AddMappingContext(VehicleContext, /*Priority=*/0);
        }
    }
}

여러 컨텍스트를 동시에 쌓을 수도 있다. 이때 Priority 값이 높은 컨텍스트가 같은 키에 대해 우선권을 가진다. UI가 열리면 높은 우선순위의 UI 컨텍스트를 위에 얹어 게임플레이 입력을 자연스럽게 가리는 식으로 쓴다. 상태 머신과 입력 처리가 분리되어 코드가 훨씬 단순해진다.

정리

Enhanced Input의 핵심은 입력 처리의 책임을 코드에서 에셋으로 옮기는 것이다. Input Action은 의미만 정의하고, Modifier가 값을 가공하며, Trigger가 발동 조건을 판정하고, Mapping Context가 키 배치와 상황 전환을 담당한다. C++ 핸들러는 "이미 잘 다듬어진 값이 들어왔다"는 사실만 신뢰하면 된다.

결과적으로 데드존, 감도, 홀드, 연타 판정 코드가 캐릭터 클래스에서 빠지고, 디자이너가 에디터에서 입력 감을 직접 조율할 수 있게 된다. 키 리매핑과 컨트롤러 대응도 IMC 교체로 끝난다. 아직 레거시 Axis/Action Mapping을 쓰고 있다면, 작은 프로토타입부터 Enhanced Input으로 옮겨 이 구조의 차이를 직접 체감해 보길 권한다.