입력 처리(Input Mapping)
Enhanced Input의 Input Action과 Mapping Context를 구성하고 C++ 콜백에 플레이어 입력을 연결합니다.
Pawn, Character, Controller 이해에서 몸체와 두뇌를 나눴으니, 이제 그 몸체가 플레이어의 명령에 반응하게 만들 차례입니다. 같은 "앞으로 걷기"라도 키보드에서는 W 키, 게임패드에서는 왼쪽 스틱이고, 차량에 탔을 때는 W 키가 가속을 뜻할 수도 있습니다. 키와 동작을 코드에 직접 묶어 두면 이런 변화가 생길 때마다 코드를 고쳐야 하므로, 언리얼은 "어떤 행동인가"와 "어떤 키가 그 행동을 일으키는가"를 따로 정의하는 입력 시스템을 제공합니다.
이 글에서는 언리얼 엔진 5의 기본 입력 방식인 향상된 입력(Enhanced Input) 시스템으로 C++ 캐릭터의 이동, 시점 회전, 점프를 연결합니다. 입력 액션과 매핑 컨텍스트 에셋을 만들고, C++에서 콜백을 바인딩한 뒤, 입력 값이 실제 월드 방향으로 어떻게 바뀌는지 따라가 봅니다.
두 가지 입력 방식
언리얼 엔진의 입력 매핑 방식은 크게 두 가지입니다. 향상된 입력 시스템은 이 글에서 쓰는 방식으로, 언리얼 엔진 5.1부터 새 프로젝트의 기본 입력 방식이 되었습니다. 언리얼 엔진 4.27 문서에도 플러그인으로 소개되어 있으므로 UE5에서 처음 생긴 기능은 아닙니다. 입력의 가공과 상황별 전환을 유연하게 다룰 수 있어 복잡한 입력 처리에 유리합니다.
레거시 입력 시스템(Legacy Input System)은 언리얼 엔진 4에서 쓰던 기존의 Action, Axis 매핑 방식입니다. 아직 이 방식을 쓰는 프로젝트도 많지만, 언리얼 엔진 5에서는 사용 중단(deprecated)으로 표시되어 있으므로 새 프로젝트에서는 향상된 입력 시스템을 씁니다.
향상된 입력 시스템을 이루는 것
향상된 입력 시스템은 몇 가지 에셋과 객체로 이루어집니다. 먼저 입력 액션(Input Action, IA)은 점프, 걷기, 발사, 상호작용처럼 플레이어가 하려는 행동 하나를 정의합니다. 입력 액션은 입력 장치와 독립적이어서, 어떤 키를 누르든 점프라는 행동은 같은 점프 입력 액션으로 표현됩니다. 또 값 타입(Value Type)을 가지므로 버튼 누름은 bool, 축 값은 float, 이동 방향은 FVector2D처럼 행동에 맞는 값을 전달할 수 있습니다. 콘텐츠 브라우저에서 마우스 오른쪽 버튼을 눌러 Input 메뉴의 Input Action으로 만듭니다.
입력 매핑 컨텍스트(Input Mapping Context, IMC)는 특정 상황에서 어떤 장치의 입력, 예를 들어 키보드의 W 키를 어떤 입력 액션에 연결할지 정의합니다. 하나의 매핑 컨텍스트에 여러 입력 액션과 그 키, 버튼, 축 입력이 들어가며, 일반 플레이, UI 메뉴, 차량 운전처럼 게임 상태마다 다른 매핑 컨텍스트를 둘 수 있습니다. 매핑 컨텍스트에는 우선순위(Priority)가 있어서, 여러 매핑 컨텍스트가 같은 입력 액션을 매핑하면 우선순위가 가장 높은 쪽만 쓰입니다. 콘텐츠 브라우저의 Input 메뉴에서 Input Mapping Context로 만듭니다.
매핑 컨텍스트를 실제로 등록하고 활성화하는 것은 향상된 입력 로컬 플레이어 서브시스템(Enhanced Input Local Player Subsystem)입니다. 로컬 플레이어마다 하나씩 있으며, APlayerController를 거쳐 접근합니다. 보통 BeginPlay() 같은 초기화 단계에서 이 서브시스템에 매핑 컨텍스트를 추가해 활성화합니다.
마지막으로 입력 컴포넌트(Input Component)는 APawn이나 ACharacter 같은 액터에서, 특정 입력 액션이 일어났을 때 부를 함수를 바인딩합니다. 캐릭터에서는 SetupPlayerInputComponent() 함수 안에서 이 바인딩을 합니다.
UI 메뉴를 열거나 차량에 타는 것처럼 입력의 의미가 바뀌는 상황이 오면, 필요한 매핑 컨텍스트를 추가(Add)하고 그 상황이 끝날 때 제거(Remove)합니다. 우선순위는 같은 입력을 두 매핑이 함께 쓰려 할 때 어느 쪽을 쓸지 정할 뿐, UI 포커스나 입력 모드(Input Mode)까지 자동으로 바꿔 주지는 않습니다.
입력 액션과 매핑 컨텍스트 만들기
캐릭터가 앞뒤와 좌우로 움직이고, 시점을 돌리고, 점프하도록 만들어 봅시다. 콘텐츠 브라우저의 Content 폴더 안에 Input 폴더를 만들고, 그 안에 입력 액션 세 개를 만듭니다. IA_Move는 값 타입을 Axis2D (Vector2D)로 정하는데, X는 좌우 이동, Y는 앞뒤 이동에 씁니다. IA_Look도 Axis2D (Vector2D)로 정하고, X는 좌우 시점, Y는 상하 시점에 씁니다. IA_Jump는 눌렸는지만 알면 되므로 값 타입을 Digital (bool)로 둡니다.
같은 폴더에 IMC_Default라는 입력 매핑 컨텍스트를 만들고 더블클릭해 엽니다. 여기에 세 입력 액션을 차례로 매핑합니다. 매핑마다 + 버튼으로 키를 추가하고, 키마다 입력 값을 바꾸는 모디파이어(modifier)를 붙일 수 있습니다.
IA_Move를 추가하고 W, S, A, D 네 키를 매핑합니다. 키 하나는 1차원 값 1을 X 자리에 만들기 때문에, 2차원 액션의 원하는 축으로 옮기려면 모디파이어가 필요합니다. W에는 축의 순서를 바꾸는 Swizzle Input Axis Values를 YXZ로 붙여 값을 Y축으로 옮기고, S에는 값을 음수로 만드는 Negate와 같은 Swizzle Input Axis Values(YXZ)를 함께 붙입니다. A에는 Negate만 붙여 X축의 음수를 만들고, D는 모디파이어 없이 양의 X 값을 그대로 씁니다. 게임패드를 쓴다면 선택적으로 Gamepad Left Thumbstick 2D를 연결하고 필요한 데드 존(Dead Zone)을 설정합니다.
IA_Look을 추가하고 마우스 축을 매핑합니다. Mouse X는 그대로 X축 값으로 쓰고, Mouse Y에는 Swizzle Input Axis Values(YXZ)를 붙여 Y축에 둡니다. 시점의 위아래가 반대로 느껴지면 그 축에 Negate를 더합니다. 선택적으로 Gamepad Right Thumbstick 2D도 연결할 수 있는데, 마우스는 움직인 양을, 스틱은 기울인 정도를 보내므로 의미가 다릅니다. 그래서 장치별 감도와 시간 배율은 따로 정합니다.
IA_Jump를 추가하고 키보드의 Space Bar를 매핑합니다. 선택적으로 게임패드의 Gamepad Face Button Bottom(A 또는 X 버튼)도 연결할 수 있습니다. 매핑을 마치면 IMC_Default를 저장합니다.
C++에서 입력 연결하기
이 예제는 C++ 클래스 마법사에서 부모로 Character를 고르고 이름을 MyCharacter로 입력해 만든 AMyCharacter를 씁니다. 앞 장에서 만든 AMyAwesomeActor는 AActor 계열이므로 이 코드의 부모로 대신 쓸 수 없습니다.
코드를 쓰기 전에 세 가지를 확인합니다. Enhanced Input 플러그인이 켜져 있는지, 프로젝트 설정의 기본 입력 클래스가 EnhancedPlayerInput과 EnhancedInputComponent로 되어 있는지, 그리고 게임 모듈의 .Build.cs 의존성 목록에 EnhancedInput 모듈이 들어 있는지입니다. 프로젝트 이름이 다르면 헤더의 MYFIRSTCPPPROJECT_API 대신 마법사가 만든 실제 모듈의 API 매크로를 그대로 씁니다. 이처럼 클래스 구조를 처음 크게 바꾸는 실습에서는 에디터를 닫고 Development Editor 타깃을 빌드한 뒤 다시 열어 확인합니다.
헤더에는 블루프린트에서 지정할 입력 에셋 네 개와, 입력이 들어왔을 때 부를 함수 네 개를 선언합니다.
#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() override;
void StopJumping() override;
};입력 에셋을 가리키는 네 포인터에는 EditAnywhere를 붙여, 블루프린트의 클래스 기본값에서 실제 에셋을 지정할 수 있게 했습니다. Jump()와 StopJumping()은 ACharacter에 이미 있는 가상 함수를 재정의한 것이므로, 선언에 override를 붙여 컴파일러가 서명을 확인하게 했습니다.
소스 파일에서는 매핑 컨텍스트를 등록하고, 입력 액션을 함수에 바인딩하고, 각 함수에서 입력 값을 동작으로 바꿉니다.
#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의 기본 점프 정지 함수 호출
}BeginPlay에서는 이 캐릭터를 조종하는 APlayerController를 얻고, 그 컨트롤러의 로컬 플레이어에서 UEnhancedInputLocalPlayerSubsystem을 가져와 DefaultMappingContext를 우선순위 0으로 추가합니다. 우선순위는 숫자가 높을수록 앞섭니다. SetupPlayerInputComponent에서는 입력 컴포넌트를 UEnhancedInputComponent로 캐스팅한 뒤 BindAction으로 입력 액션과 함수를 연결합니다.
바인딩의 두 번째 인자는 트리거 이벤트(trigger event)로, 함수가 어느 시점에 불릴지를 정합니다. Triggered는 액션이 발동한 동안 불리고, Started는 트리거 평가가 시작될 때, Completed는 평가가 끝날 때 불립니다. 그래서 이동과 시점은 키를 누르고 있는 동안 계속 반응하도록 Triggered에, 점프는 누르는 순간 Jump, 떼는 순간 StopJumping을 부르도록 Started와 Completed에 연결했습니다.
Look은 입력 값의 X를 컨트롤러의 Yaw 입력에, Y를 Pitch 입력에 그대로 더합니다. Move는 조금 더 생각할 거리가 있습니다. 컨트롤러의 회전(ControlRotation)에서 Pitch와 Roll을 빼고 Yaw만 남긴 회전을 만든 뒤, 그 회전의 X축을 전방, Y축을 우측 방향으로 써서 평면 위의 이동 방향을 정합니다.
입력 축이 월드 방향이 되기까지
UNREAL C++ · CH3-2
입력 X·Y를 Yaw 기준 월드 방향에 연결합니다
Move 예제는 ControlRotation에서 Yaw만 꺼내 평면의 전방·우측 축을 만듭니다. 입력 성분 이름과 월드 좌표축 이름을 구분합니다.
Yaw=0°: 전방은 입력 Y, 우측은 입력 X에 곱합니다. 그림의 월드 +X는 오른쪽, +Y는 아래쪽입니다.
Yaw=90°: 전방은 입력 Y, 우측은 입력 X에 곱합니다. 그림의 월드 +X는 오른쪽, +Y는 아래쪽입니다.
| 입력 성분 | 원문이 구한 방향 | AddMovementInput의 크기 |
|---|---|---|
| 앞뒤 입력 Y | YawRotation의 전방 축 GetUnitAxis(X) | MovementVector.Y |
| 좌우 입력 X | YawRotation의 우측 축 GetUnitAxis(Y) | MovementVector.X |
- 앞뒤 입력 Y
- 원문이 구한 방향: YawRotation의 전방 축 GetUnitAxis(X)AddMovementInput의 크기: MovementVector.Y
- 좌우 입력 X
- 원문이 구한 방향: YawRotation의 우측 축 GetUnitAxis(Y)AddMovementInput의 크기: MovementVector.X
예시 · 입력 (X=0, Y=1), Yaw=0°
전방 월드 방향 (1, 0, 0)에 크기 1의 이동 입력을 더합니다.
같은 입력, Yaw=90°
전방 월드 방향이 (0, 1, 0)으로 바뀝니다. 입력의 Y=1이 월드 Y축 고정 이동을 뜻하지 않습니다.
AddMovementInput은 위치를 즉시 더하는 호출이 아닙니다. 누적 입력을 CharacterMovement가 이동 계산에 사용합니다. 실제 변위는 이동·충돌 설정과 시간 처리에 따라 달라집니다.
위 그림은 Move가 입력의 X, Y를 Yaw 기준의 월드 방향에 어떻게 연결하는지 보여 줍니다. 그림에서 월드 +X는 오른쪽, +Y는 아래쪽입니다. Yaw가 0°일 때 전방은 월드 +X이고 우측은 월드 +Y이며, 입력의 Y는 전방에, 입력의 X는 우측에 곱해집니다. Yaw가 90°가 되면 입력이 곱해지는 규칙은 같지만, 전방이 월드 +Y로, 우측이 월드 -X로 바뀝니다.
그림의 표는 코드와 같은 내용을 정리합니다. 앞뒤 입력 Y는 YawRotation의 전방 축인 GetUnitAxis(EAxis::X)에, 좌우 입력 X는 우측 축인 GetUnitAxis(EAxis::Y)에 곱해 AddMovementInput의 크기로 넘깁니다. 예를 들어 입력이 (X=0, Y=1)이고 Yaw가 0°이면 월드 방향 (1, 0, 0)에 크기 1의 이동 입력을 더하고, 같은 입력이라도 Yaw가 90°이면 전방이 (0, 1, 0)으로 바뀝니다. 그러니 입력의 Y=1이 월드 Y축으로 고정된 이동을 뜻하는 것은 아닙니다.
그림의 화살표는 벡터의 방향일 뿐, 액터가 즉시 그만큼 움직였다거나 호출 순서를 뜻하지 않습니다. AddMovementInput은 위치를 바로 더하는 호출이 아니라, 쌓인 입력을 CharacterMovementComponent가 이동 계산에 쓰게 하는 요청입니다. 실제로 몇 cm를 움직이는지는 이동과 충돌 설정, 시간 처리에 따라 달라집니다.
Yaw가 90°인 경우를 숫자로 따라가 봅시다. 이때 전방은 월드 (0, 1, 0), 우측은 (-1, 0, 0)입니다. W 키 하나가 만든 입력 (X=0, Y=1)은 전방 성분만 쓰므로 월드 +Y 방향으로 이동을 요청합니다. D 키 하나의 입력 (1, 0)은 우측 성분을 써서 월드 -X 방향으로 이동을 요청합니다. 이처럼 입력 액션의 X, Y는 이름이 같을 뿐 월드의 X, Y축과 항상 같은 축이 아닙니다.
블루프린트에서 입력 에셋 지정하기
앞에서 말한 대로 에디터를 닫은 채 Development Editor 타깃을 빌드하고 에디터를 다시 연 뒤, AMyCharacter를 부모로 하는 블루프린트가 이미 있으면 그것을 쓰고, 없다면 AMyCharacter를 부모로 블루프린트를 만들어 BP_MyCharacter로 저장합니다. 그 블루프린트를 열고 클래스 기본값(Class Defaults)의 디테일 패널에서 C++에서 선언한 Input 분류의 속성을 찾습니다. Default Mapping Context에는 IMC_Default를, Move Action에는 IA_Move를, Look Action에는 IA_Look을, Jump Action에는 IA_Jump를 지정합니다.
다음으로 게임 모드가 플레이어용 폰으로 이 캐릭터를 스폰하게 합니다. 여기서는 게임 모드가 폰을 만드는 경로를 쓰므로, 레벨에 미리 배치한 폰의 자동 빙의 설정과 섞지 않습니다. 게임 모드 블루프린트(예: BP_MyGameMode)를 열거나 새로 만들고, 디테일 패널의 Classes 항목에서 Default Pawn Class를 BP_MyCharacter로, Player Controller Class를 기본 PlayerController(직접 만든 컨트롤러가 있다면 그것)로 정합니다. 마지막으로 Window 메뉴의 World Settings를 열어 GameMode Override를 BP_MyGameMode로 바꿔, 현재 레벨이 이 게임 모드를 쓰게 합니다.
플레이하며 확인하기
Play를 누른 뒤에는 실제로 빙의된 BP_MyCharacter를 기준으로 확인합니다. 키를 눌러도 콜백이 불리지 않는다면, 로컬 플레이어에 활성화된 매핑 컨텍스트, 블루프린트에 지정한 에셋, UEnhancedInputComponent 캐스팅, 트리거 이벤트 바인딩을 차례로 봅니다.
이 예제는 BeginPlay에서 매핑 컨텍스트를 한 번만 등록합니다. 그 순간 컨트롤러나 로컬 플레이어가 아직 없거나, 나중에 빙의가 바뀌면 등록을 놓칠 수 있습니다. 그래서 기능을 넓힐 때는 로컬 플레이어가 준비된 시점에 등록하고, 조종이 끝날 때 해제하는 흐름을 함께 설계합니다.
Move의 입력 값과 실제 이동도 구분합니다. 콜백이 불렸는데 움직이지 않는다면, 캐릭터가 올바른 월드에 스폰되었는지와 충돌, 이동 설정이 유효한지를 확인합니다. 카메라를 붙이는 일은 다음 글에서 합니다. 점프의 Started와 Completed 바인딩은 여기서 쓴 단순한 버튼을 전제로 합니다. 길게 누르기(Hold) 같은 다른 트리거를 넣으면 트리거가 취소될 때 오는 Canceled를 포함한 종료 처리도 검토합니다.
더 깊이: 트리거는 입력을 어떻게 평가할까
키에서 들어온 값은 매핑에 붙인 모디파이어를 차례로 거친 뒤 트리거로 넘어갑니다. 트리거는 이 값을 평가해 실패(None), 진행 중(Ongoing), 성공(Triggered) 가운데 하나를 돌려주고, 바인딩한 트리거 이벤트는 이 결과에서 나옵니다. 예를 들어 Hold 트리거는 누른 시간이 HoldTimeThreshold초에 이를 때까지 진행 중 상태로 있다가 그때부터 성공을 돌려주며, 그 전에 버튼을 떼면 Canceled가 옵니다. 트리거를 하나도 붙이지 않은 매핑은 입력 값이 0이 아닌 동안 매 틱 발동합니다. 이 글의 IA_Move가 키를 누르고 있는 동안 Triggered를 계속 받는 것도 이 규칙 때문입니다.
한 매핑에 트리거를 여러 개 붙이면 트리거의 종류에 따라 결과를 합칩니다. 명시적(Explicit) 트리거끼리는 그중 하나만 성공하면 되고, 암시적(Implicit) 트리거는 모든 암시적 트리거가 함께 성공해야 합니다. 두 종류가 함께 붙어 있으면 암시적 트리거가 모두 성공하고 명시적 트리거도 하나 이상 성공해야 입력이 성공합니다. 차단(Blocker) 트리거는 자신이 성공하면 다른 트리거의 결과와 상관없이 입력을 실패시킵니다. 그래서 트리거를 더했는데 액션이 전혀 발동하지 않는다면, 암시적 트리거 가운데 하나가 실패하고 있거나 차단 트리거가 성공하고 있지 않은지 확인합니다. 트리거의 종류와 상태는 Enhanced Input 문서에 정리되어 있습니다.
정리
- 향상된 입력 시스템은 언리얼 엔진 5의 기본 입력 방식이며, 레거시 Action, Axis 매핑은 사용 중단으로 표시되어 있습니다.
- 입력 액션은 행동과 값 타입을, 매핑 컨텍스트는 현재 상황에서 어떤 키가 어떤 액션인지를 정의합니다. 같은 액션을 여러 컨텍스트가 매핑하면 우선순위가 가장 높은 쪽이 쓰입니다.
- 키 하나는 1차원 값을 만들므로, 2차원 이동 액션에서는
Negate와Swizzle Input Axis Values로 W, S, A, D를 원하는 축과 부호에 맞춥니다. - 매핑 컨텍스트는 로컬 플레이어 서브시스템에 추가하고, 입력 액션은
SetupPlayerInputComponent에서UEnhancedInputComponent::BindAction으로 트리거 이벤트와 함께 바인딩합니다. Move는 컨트롤러 회전의 Yaw로 전방과 우측을 만들어 입력을 월드 방향으로 바꾸며,AddMovementInput은 즉시 이동이 아니라 이동 요청입니다.- 블루프린트 기본값에 입력 에셋을 지정하고, 게임 모드의
Default Pawn Class와 월드 세팅의GameMode Override로 캐릭터를 스폰하게 합니다.
연습 문제
1. 뒤를 보고 있을 때
Yaw가 180°이고 Pitch와 Roll은 빼고 생각합니다. S만 눌렀을 때의 입력 (0, -1)과 D만 눌렀을 때의 입력 (1, 0)은 각각 어느 월드 방향의 이동 요청이 될까요? 이 결과만으로 한 틱의 실제 이동 거리까지 정할 수 있을까요?
해설 보기
Yaw가 180°이면 전방은 (-1, 0, 0), 우측은 (0, -1, 0)입니다. S는 전방에 -1을 곱하므로 (1, 0, 0) 방향이고, D는 우측에 +1을 곱하므로 (0, -1, 0) 방향입니다.
실제 이동 거리를 정하려면 CharacterMovementComponent의 속도, 가속, 경과 시간, 충돌 조건이 더 필요합니다. 이 단위 방향 벡터만으로는 몇 cm를 움직였는지 정할 수 없습니다.
2. 처음 한 번만 입력이 안 될 때
플레이어 컨트롤러가 게임 도중에 스폰된 캐릭터 B를 빙의했는데, B에서는 키를 눌러도 아무 반응이 없습니다. B는 이 플레이어가 처음 조종하는 AMyCharacter 캐릭터이고 입력 에셋도 지정되어 있습니다. 이 글의 코드에서 의심할 만한 곳은 어디일까요?
해설 보기
이 예제는 BeginPlay에서 한 번만 매핑 컨텍스트를 추가합니다. B의 BeginPlay가 실행되던 때 B를 조종하는 플레이어 컨트롤러가 없었다면, Cast<APlayerController>(Controller)가 실패해 매핑 컨텍스트가 등록되지 않습니다. 나중에 B를 빙의해도 이 코드는 다시 실행되지 않습니다. 게임 도중에 스폰한 액터는 스폰 과정에서 바로 BeginPlay가 불리고 빙의는 그 뒤에 이루어지므로, B처럼 스폰한 뒤에 빙의한 캐릭터에서 이 일이 생깁니다. 매핑 컨텍스트는 폰이 아니라 로컬 플레이어의 서브시스템에 등록되고 이 글의 코드에는 제거하는 곳이 없으므로, 그 전에 다른 AMyCharacter를 조종하면서 IMC_Default를 등록해 둔 적이 있다면 그 등록이 남아 있어 이 문제가 드러나지 않습니다.
로컬 플레이어의 서브시스템에 지금 어떤 매핑 컨텍스트가 활성화되어 있는지 확인하고, 빙의가 이루어진 시점처럼 컨트롤러가 준비된 때에 등록하도록 흐름을 바꿔야 합니다. 조종이 끝날 때는 등록한 매핑 컨텍스트를 제거하는 처리도 함께 둡니다.
입력 액션과 매핑 컨텍스트, 그리고 W, A, S, D에 Negate와 Swizzle을 붙이는 방법은 Epic의 Enhanced Input 문서에서 확인할 수 있습니다. 언리얼 엔진 4에서 소개된 이전 판본은 UE 4.27의 Enhanced Input 문서에 있습니다.
다음 글 컴포넌트 시스템 이해와 활용에서는 캐릭터에 스프링 암과 카메라를 붙이고, 직접 만든 컴포넌트로 기능을 나누는 방법을 살펴봅니다.