icon
3장 : 게임플레이 프로그래밍

입력 처리(Input Mapping)


이제 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를 통해 접근하여 사용합니다.
  • 특징: APlayerControllerBeginPlay() 같은 초기화 단계에서 IMC를 등록하고 푸시(Push)하여 활성화합니다.

입력 컴포넌트 (Input Component)

  • 역할: APawn 또는 ACharacter와 같은 액터에서 특정 입력 액션이 발생했을 때 호출될 함수를 바인딩(Bind)하는 역할을 합니다.
  • 특징: SetupPlayerInputComponent() 함수 내에서 InputComponent를 통해 바인딩을 수행합니다.

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

이제 실제 코드를 통해 향상된 입력 시스템을 설정해 보겠습니다. 우리의 캐릭터가 앞뒤로 움직이고 좌우로 회전하는 기능을 추가해 봅시다.

입력 액션 (Input Actions) 생성

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

IA_Move

  • Value TypeAxis2D (Vector2D)로 설정합니다. (X는 좌우, Y는 앞뒤 이동을 위해)

IA_Look

  • Value TypeAxis2D (Vector2D)로 설정합니다. (X는 좌우 시점 변경, Y는 상하 시점 변경을 위해)

IA_Jump

  • Value TypeBool로 설정합니다. (점프는 눌렀는지 여부만 알면 되므로)

입력 매핑 컨텍스트 생성

Input 폴더 안에 IMC_Default 라는 새 Input Mapping Context 에셋을 생성합니다. 더블 클릭하여 엽니다.

이제 이 IMC_DefaultIA_Move, IA_Look, IA_Jump를 매핑해 봅시다.

IA_Move 매핑

  • 'Add Mapping'을 클릭하고 IA_Move를 선택합니다.
  • '+' 버튼을 눌러 다음 키들을 추가합니다.
    • W (키보드): Swizzle Input Axis > Y to 1D (앞으로 이동)
    • S (키보드): Swizzle Input Axis > Y to 1D Magnitude: -1.0 (뒤로 이동)
    • A (키보드): Swizzle Input Axis > X to 1D Magnitude: -1.0 (왼쪽으로 이동)
    • D (키보드): Swizzle Input Axis > X to 1D (오른쪽으로 이동)
    • (선택) Gamepad Left Thumbstick: Swizzle Input Axis > Y-axis and X-axis (게임패드 스틱으로 이동)

IA_Look 매핑

  • 'Add Mapping'을 클릭하고 IA_Look를 선택합니다.
  • '+' 버튼을 눌러 다음 키들을 추가합니다.
    • Mouse X (마우스): Swizzle Input Axis > X to 1D (좌우 시점)
    • ModifiersInvert를 추가하여 필요에 따라 반전시킬 수 있습니다.
    • Mouse Y (마우스): Swizzle Input Axis > Y to 1D (상하 시점)
    • ModifiersInvert를 추가하여 필요에 따라 반전시킬 수 있습니다.
    • (선택) Gamepad Right Thumbstick: Swizzle Input Axis > X-axis and Y-axis (게임패드 스틱으로 시점 변경)

IA_Jump 매핑

  • 'Add Mapping'을 클릭하고 IA_Jump를 선택합니다.
  • '+' 버튼을 눌러 다음 키를 추가합니다.
    • Space Bar (키보드): (점프)
    • (선택) Gamepad Face Button Bottom: (게임패드 A/X 버튼)

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

C++ 코드에서 작업하기

이제 여러분의 APlayerController 클래스 (또는 커스텀 AGameMode에서 기본 PlayerController를 지정)와 ACharacter 클래스를 수정하여 이 입력 시스템을 연동합니다.

먼저, 프로젝트의 DefaultPawn 또는 ACharacter 클래스 (예: AMyAwesomeActor 또는 새롭게 만든 AMyCharacter)에 입력 액션을 받을 함수를 선언하고 정의해야 합니다.

AMyCharacter.h (또는 여러분의 Character/Pawn 헤더)

AMyCharacter.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputAction.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();
};

AMyCharacter.cpp (또는 여러분의 Character/Pawn 소스)

AMyCharacter.cpp
#include "MyCharacter.h"
#include "EnhancedInputSubsystems.h" // 핵심 서브시스템 포함
#include "EnhancedInputComponent.h"   // EnhancedInputComponent를 위해 포함
#include "GameFramework/SpringArmComponent.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은 우선순위 (높을수록 우선)
            }
        }
    }
}

// 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의 기본 점프 정지 함수 호출
}

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

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

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

  • Default Mapping Context에 이전에 생성한 IMC_Default 에셋을 할당합니다.
  • Move ActionIA_Move 에셋을 할당합니다.
  • Look ActionIA_Look 에셋을 할당합니다.
  • Jump ActionIA_Jump 에셋을 할당합니다.

게임 모드 설정 (선택 사항이지만 권장): 월드에 배치된 캐릭터가 플레이어에 의해 제어되도록 하려면, 게임 모드의 기본 폰 클래스(Default Pawn Class)플레이어 컨트롤러 클래스(Player Controller Class) 를 설정해야 합니다.

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

플레이 및 테스트

모든 설정이 완료되었다면 언리얼 에디터에서 Play 버튼을 눌러 게임을 실행합니다. 이제 WASD 키로 캐릭터가 앞뒤좌우로 이동하고, 마우스를 움직여 시점을 변경하며, 스페이스바를 눌러 점프하는 것을 확인할 수 있을 것입니다!


정리 및 다음 단계

향상된 입력 시스템은 처음에는 다소 복잡하게 느껴질 수 있지만, 한 번 익숙해지면 매우 강력하고 유연하게 입력 처리를 관리할 수 있게 해줍니다. Input Action으로 행동의 개념을, Input Mapping Context로 실제 키 매핑을 분리하여 관리함으로써, 게임의 입력 설정을 쉽게 변경하거나, 여러 입력 장치를 지원하거나, 특정 상황에 따른 다른 입력 동작을 구현하는 데 큰 이점을 제공합니다.

이제 여러분의 C++ 캐릭터가 플레이어의 입력에 반응하기 시작했습니다!