안동민 개발노트

본문 시작

입력 처리(Input Mapping)

Enhanced Input의 Input Action과 Mapping Context를 구성하고 C++ 콜백에 플레이어 입력을 연결합니다.

이제 Pawn, Character, Controller가 게임플레이를 구성하는 핵심 요소임을 이해했습니다.

이들이 실제로 움직이고 상호작용하려면 플레이어의 명령, 즉 입력(Input)이 필요합니다.

언리얼 엔진은 매우 강력하고 유연한 입력 처리 시스템을 제공하며, 이를 통해 키보드, 마우스, 게임패드 등 다양한 장치의 입력을 여러분의 게임 로직과 연결할 수 있습니다.

이번 절에서는 언리얼 엔진의 입력 매핑 시스템에 대해 자세히 알아보고, 여러분의 C++ 캐릭터가 플레이어의 명령에 반응하도록 만들어 보겠습니다.

언리얼 엔진은 크게 두 가지 방식의 입력 매핑을 제공합니다.

향상된 입력 시스템 (Enhanced Input System): 언리얼 엔진 5에서 도입된 최신 방식입니다.

더 유연하고 강력하며, 복잡한 입력 처리에 훨씬 유리합니다.

이 책에서는 이 방식을 주로 다룰 것입니다.

레거시 입력 시스템 (Legacy Input System): 언리얼 엔진 4부터 사용되던 기존 방식입니다.

아직도 많은 프로젝트에서 사용되지만, 새로운 프로젝트에서는 향상된 입력 시스템을 사용하는 것이 권장됩니다.

우리는 새로운 프로젝트이므로, 향상된 입력 시스템을 기준으로 설명하겠습니다.


향상된 입력 시스템의 핵심 요소

향상된 입력 시스템은 몇 가지 주요 에셋과 개념으로 구성됩니다.

입력 액션 (Input Action, IA)
  • 역할: 플레이어가 수행하려는 특정 행동의 개념을 정의합니다. 예를 들어, 점프, 걷기, 발사, 상호작용 등이 입력 액션이 됩니다.
  • 특징: 입력 장치(키보드, 마우스 버튼 등)와 독립적입니다. 즉, 어떤 키를 누르든 점프라는 행동은 점프 입력 액션에 해당합니다. 데이터 유형(bool, float, FVector2D 등)을 가질 수 있어 버튼 누름(bool), 이동 방향(FVector2D), 축 값(float) 등을 표현할 수 있습니다.
  • 생성: 콘텐츠 브라우저에서 Input > Input Action을 선택하여 생성합니다.
입력 매핑 컨텍스트 (Input Mapping Context, IMC)
  • 역할: 특정 상황에서 어떤 입력 장치의 입력(예: 키보드의 W 키)을 어떤 입력 액션(예: 앞으로 걷기)에 매핑할지 정의합니다. 게임의 여러 상태(예: 일반 플레이, UI 메뉴, 차량 운전)에 따라 다른 입력 매핑을 적용할 수 있습니다.
  • 특징: 여러 입력 액션과 그에 연결된 키/버튼/축 입력을 포함합니다. 우선순위(Priority)를 가질 수 있어 여러 IMC가 동시에 활성화될 때 어떤 IMC의 매핑이 우선적으로 적용될지 결정할 수 있습니다.
  • 생성: 콘텐츠 브라우저에서 Input > Input Mapping Context를 선택하여 생성합니다.
입력 로컬 플레이어 서브시스템 (Input Local Player Subsystem)
  • 역할: 실제로 IMC를 등록하고 활성화하며, 입력 이벤트를 처리하는 엔진의 핵심 컴포넌트입니다. APlayerController를 통해 접근하여 사용합니다.
  • 특징: APlayerController의 BeginPlay() 같은 초기화 단계에서 IMC를 등록하고 푸시(Push)하여 활성화합니다.
입력 컴포넌트 (Input Component)
  • 역할: APawn 또는 ACharacter와 같은 액터에서 특정 입력 액션이 발생했을 때 호출될 함수를 바인딩(Bind)하는 역할을 합니다.
  • 특징: SetupPlayerInputComponent() 함수 내에서 InputComponent를 통해 바인딩을 수행합니다.

UI 메뉴·차량 탑승처럼 의미가 바뀌면 필요한 Mapping Context를 Add하고 종료 시 Remove합니다. 우선순위는 입력 소비 충돌에 관여하며, UI 포커스나 Input Mode까지 자동 변경하는 스위치는 아닙니다.


입력 시스템 설정 실습 (C++ 중심)

이제 실제 코드를 통해 향상된 입력 시스템을 설정해 보겠습니다.

우리의 캐릭터가 앞뒤로 움직이고 좌우로 회전하는 기능을 추가해 봅시다.

입력 액션 (Input Actions) 생성

콘텐츠 브라우저에서 Content 폴더 안에 Input이라는 새 폴더를 만들고, 그 안에 다음 세 가지 Input Action 에셋을 생성합니다.

IA_Move
  • Value Type을 Axis2D (Vector2D)로 설정합니다. (X는 좌우, Y는 앞뒤 이동을 위해)
IA_Look
  • Value Type을 Axis2D (Vector2D)로 설정합니다. (X는 좌우 시점 변경, Y는 상하 시점 변경을 위해)
IA_Jump
  • Value Type을 Bool로 설정합니다. (점프는 눌렀는지 여부만 알면 되므로)

입력 매핑 컨텍스트 생성

Input 폴더 안에 IMC_Default라는 새 Input Mapping Context 에셋을 생성합니다.

더블 클릭하여 엽니다.

이제 이 IMC_Default에 IA_Move, IA_Look, IA_Jump를 매핑해 봅시다.

IA_Move 매핑
  • Add Mapping을 클릭하고 IA_Move를 선택합니다.
  • + 버튼을 눌러 다음 키들을 추가합니다.
    • W: Swizzle Input Axis Values를 YXZ로 설정해 1D 값을 Y축으로 옮깁니다.
    • S: Negate로 음수화한 뒤 Swizzle Input Axis Values(YXZ)를 적용합니다.
    • A: Negate로 X축 값을 음수화합니다.
    • D: 추가 축 변환 없이 양의 X값을 사용합니다.
    • (선택) Gamepad Left Thumbstick 2D: 2D 스틱 입력을 연결하고 필요한 Dead Zone을 설정합니다.
IA_Look 매핑
  • Add Mapping을 클릭하고 IA_Look를 선택합니다.
  • + 버튼을 눌러 다음 키들을 추가합니다.
    • Mouse X: X축 값으로 연결합니다.
    • Mouse Y: Swizzle Input Axis Values(YXZ)로 Y축에 둡니다.
    • 반전이 필요하면 해당 축에 Negate를 적용합니다.
    • (선택) Gamepad Right Thumbstick 2D: 2D 입력으로 연결합니다. 마우스 델타와 스틱의 회전 속도는 의미가 다르므로 장치별 감도와 시간 스케일 정책은 별도로 정합니다.
IA_Jump 매핑
  • Add Mapping을 클릭하고 IA_Jump를 선택합니다.
  • + 버튼을 눌러 다음 키를 추가합니다.
    • Space Bar (키보드): (점프)
    • (선택) Gamepad Face Button Bottom: (게임패드 A/X 버튼)

IMC 설정을 마치면 저장합니다.

C++ 코드에서 작업하기

이 예제는 마법사에 MyCharacter를 입력해 만든 ACharacter 파생 클래스 AMyCharacter를 사용합니다. 앞 절의 AMyAwesomeActor는 AActor 계열이므로 이 코드의 부모로 대신 사용할 수 없습니다.

Enhanced Input 플러그인과 프로젝트 입력 기본 클래스(EnhancedPlayerInput·EnhancedInputComponent)를 확인하고, 게임 모듈의 Build.cs 의존성에 EnhancedInput을 포함합니다.

MyCharacter.h (또는 여러분의 Character/Pawn 헤더)
MyCharacter.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputAction.h"
#include "InputActionValue.h" // FInputActionValue의 명시적 정의
#include "MyCharacter.generated.h"

// 미리 선언 (Forward Declarations)
class UInputMappingContext;
class UInputAction;

UCLASS()
class MYFIRSTCPPPROJECT_API AMyCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    AMyCharacter();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;

    // 입력 설정 함수를 오버라이드
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

protected:
    // =============== Input System 관련 UPROPERTY 변수들 ===============
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    UInputMappingContext* DefaultMappingContext;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    UInputAction* MoveAction;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    UInputAction* LookAction;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    UInputAction* JumpAction;

    // =============== Input Action 바인딩될 함수들 ===============
    void Move(const FInputActionValue& Value);
    void Look(const FInputActionValue& Value);
    void Jump();
    void StopJumping();
};
MyCharacter.cpp (또는 여러분의 Character/Pawn 소스)
MyCharacter.cpp
#include "MyCharacter.h"
#include "EnhancedInputSubsystems.h" // 핵심 서브시스템 포함
#include "EnhancedInputComponent.h"   // EnhancedInputComponent를 위해 포함
#include "GameFramework/SpringArmComponent.h" // 카메라 컴포넌트 추가 시 필요
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerController.h"
#include "Engine/LocalPlayer.h"

AMyCharacter::AMyCharacter()
{
    PrimaryActorTick.bCanEverTick = true;

    // CharacterMovementComponent 설정 (ACharacter는 기본으로 가짐)
    GetCharacterMovement()->bOrientRotationToMovement = true; // 이동 방향으로 회전
    bUseControllerRotationYaw = false; // 컨트롤러 회전 사용 안 함 (캐릭터가 직접 회전하도록)

    // ... 다른 컴포넌트 설정 ...
}

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

    // PlayerController를 가져와서 Enhanced Input Subsystem에 접근
    if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
    {
        if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
        {
            // DefaultMappingContext가 유효하다면 추가
            if (DefaultMappingContext)
            {
                Subsystem->AddMappingContext(DefaultMappingContext, 0); // 0은 우선순위 (높을수록 우선)
            }
        }
    }
}

void AMyCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

// InputComponent에 입력 액션 바인딩
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    // EnhancedInputComponent로 캐스팅
    if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
    {
        // Move Action 바인딩
        if (MoveAction)
        {
            EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyCharacter::Move);
        }

        // Look Action 바인딩
        if (LookAction)
        {
            EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AMyCharacter::Look);
        }

        // Jump Action 바인딩
        if (JumpAction)
        {
            EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AMyCharacter::Jump);
            EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AMyCharacter::StopJumping);
        }
    }
}

// 이동 함수 구현
void AMyCharacter::Move(const FInputActionValue& Value)
{
    // 입력 값 (Vector2D)을 가져옵니다.
    FVector2D MovementVector = Value.Get<FVector2D>();

    if (Controller != nullptr)
    {
        // 컨트롤러의 YAW 회전을 기준으로 앞/뒤 방향을 가져옵니다.
        const FRotator Rotation = Controller->GetControlRotation();
        const FRotator YawRotation(0, Rotation.Yaw, 0);

        // 앞 방향 (Forward Vector)
        const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
        AddMovementInput(ForwardDirection, MovementVector.Y);

        // 오른쪽 방향 (Right Vector)
        const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
        AddMovementInput(RightDirection, MovementVector.X);
    }
}

// 시점 변경 함수 구현
void AMyCharacter::Look(const FInputActionValue& Value)
{
    FVector2D LookAxisVector = Value.Get<FVector2D>();

    if (Controller != nullptr)
    {
        AddControllerYawInput(LookAxisVector.X);
        AddControllerPitchInput(LookAxisVector.Y);
    }
}

// 점프 함수 구현
void AMyCharacter::Jump()
{
    Super::Jump(); // ACharacter의 기본 점프 함수 호출
}

void AMyCharacter::StopJumping()
{
    Super::StopJumping(); // ACharacter의 기본 점프 정지 함수 호출
}

Move는 Pitch와 Roll을 제외한 ControlRotation의 Yaw로 평면의 전방·우측 축을 만듭니다.

Move의 입력 축과 월드 방향

입력의 X·Y와 월드의 X·Y는 컨트롤러 Yaw를 거치므로 같은 축이 아닙니다.

Move의 입력 축과 월드 방향
입력 성분원문이 구한 방향AddMovementInput의 크기
앞뒤 입력 YYawRotation의 전방 축 GetUnitAxis(X)MovementVector.Y
좌우 입력 XYawRotation의 우측 축 GetUnitAxis(Y)MovementVector.X
앞뒤 입력 Y
원문이 구한 방향: YawRotation의 전방 축 GetUnitAxis(X)
AddMovementInput의 크기: MovementVector.Y
좌우 입력 X
원문이 구한 방향: YawRotation의 우측 축 GetUnitAxis(Y)
AddMovementInput의 크기: MovementVector.X

AddMovementInput은 위치를 즉시 더하는 호출이 아닙니다. 누적 입력을 CharacterMovement가 이동 계산에 사용합니다.

블루프린트에서 입력 에셋 할당

이제 C++ 코드를 컴파일(Ctrl + Shift + B 또는 언리얼 에디터의 컴파일 버튼)한 후, AMyCharacter에서 만든 BP_MyCharacter 블루프린트를 엽니다.

클래스 기본값(Class Defaults) 설정: 블루프린트 에디터에서 클래스 기본값(Class Defaults) 또는 디테일 패널에서 여러분이 C++에서 선언한 Input 관련 UPROPERTY 변수들을 찾습니다.

  • Default Mapping Context에 이전에 생성한 IMC_Default에셋을 할당합니다.
  • Move Action에 IA_Move에셋을 할당합니다.
  • Look Action에 IA_Look에셋을 할당합니다.
  • Jump Action에 IA_Jump에셋을 할당합니다.

게임 모드에서 스폰할 캐릭터 설정: 여기서는 게임 모드가 플레이어용 Pawn을 생성하는 경로를 사용합니다. 이미 배치한 Pawn의 빙의 설정과 섞지 않습니다.

  • 여러분의 게임 모드 블루프린트(예: BP_MyGameMode)를 열거나, 새로 만듭니다.
  • 디테일 패널에서 Classes 섹션을 찾습니다.
  • Default Pawn Class를 BP_MyCharacter로 설정합니다.
  • Player Controller Class를 기본 PlayerController 클래스(또는 커스텀 APlayerController를 만들었다면 그것으로)로 설정합니다.
  • 마지막으로, 현재 레벨에서 이 게임 모드를 사용하도록 월드 세팅(Window > World Settings)에서 GameMode Override를 여러분의 BP_MyGameMode로 설정합니다.

플레이 및 확인

Play에서 실제 빙의된 BP_MyCharacter를 기준으로 확인합니다.

  • 콜백이 없으면 로컬 플레이어의 활성 IMC, 에셋 할당, EnhancedInputComponent 캐스트, TriggerEvent 바인딩을 확인합니다.
  • 이 원문은 BeginPlay에서 한 번 IMC를 등록합니다. 그때 Controller·LocalPlayer가 없거나 이후 빙의가 바뀌면 등록을 놓칠 수 있으므로, 확장 시 로컬 플레이어가 준비된 시점의 등록과 해제를 함께 설계합니다.
  • Move의 입력값과 실제 이동을 구분합니다. 캐릭터가 올바른 월드에 스폰되고 충돌·이동 설정이 유효해야 합니다. 카메라 부착은 다음 절에서 추가합니다.
  • Jump의 Started/Completed 바인딩은 여기의 단순 버튼을 전제로 합니다. Hold 등 다른 트리거를 넣으면 Canceled를 포함한 종료 처리도 검토합니다.

Input Action은 행동과 값 타입을, Mapping Context는 현재 상황의 키 해석을 정의합니다.