내 뇌의 MAXWALKSPEED는 0... 누가 나 대신 좀 조종 좀 해줘~~(PlayerController, Enhanced Input IA, IMC,캐릭터 조작, 동작 구현)

정윤재·2026년 8월 15일
post-thumbnail

인삿말은 짧고 굵게 가겠습니다. 오늘 PlayerController, Enhanced Input IA, IMC,캐릭터 조작, 동작 구현 등을 하며 정말 많은 것을 했습니다. 예. 아무튼 먼저 플레이어 컨트롤러부터 살펴보겠습니다.


1.PlayerController


플레이어 컨트롤러를 한 번 만들어봅시다.

이렇게 생성되면 YoonjaePlayerController 헤더 파일과 소스파일이 생깁니다.

GameMode의 PlayerControllerClass를 추가하여 AYoonjaePlayerController::StaticClass()를 지정하여 게임에서 사용할 기본 PlayerController로 설정합니다. 그리고 이 것을 손쉽게 편집할 수 있게 블프에서 감싸줍시다.

그리고 blueprint를 따로 만들었으니 게임모드 블프에서 다시 설정을 바꿔줍시다.

그러면 게임을 플레이하면 플렝이거 컨트롤러가 바뀐 것을 확인할 수 있습니다.

2.Input


컨텐츠 브라우저에서 Input Action을 만들어봅시다.

Value Type부터 차례대로 한 번 알아봅시다.

1. Value Type

Input Action이 어떤 형태의 입력값을 받을지 결정합니다.

종류설명예시
Bool참/거짓의 On/Off 입력점프, 공격
Axis1D하나의 축 값을 입력 (-1 ~ 1)전진/후진, 게임패드 트리거
Axis2DX, Y 두 축을 입력WASD 이동, 마우스 이동
Axis3DX, Y, Z 세 축을 입력비행 시뮬레이션 3축 조작

2. Trigger

입력이 언제 활성화될지 결정하는 조건입니다.

  • Pressed → 키를 누르는 순간 실행
  • Hold → 키를 일정 시간 이상 누르고 있을 때 실행
  • Released → 키를 뗀 순간 실행

3. Modifier

입력된 값을 수정하거나 변환합니다.

  • Scale → 입력값에 배율을 적용
    → 마우스 감도 2배 등
  • Invert → 입력값을 반전
    → 마우스 상하 반전 등
  • Deadzone → 일정 범위 이하의 작은 입력을 무시
    → 게임패드 스틱의 미세한 떨림 방지


그럼 IA Move에서는 Value Type은 뭐로 설정해야 할까용? 예 맞습니다, 앞,뒤,왼,오를 가야하고 두 방향을 동시에 처리해야하므로 Axis2D로 설정합니다. 그럼 티리거나 모디파이어가 필요할까요? IA_Move는 항상 활성화 된 상태에서 WASD 입력만으로 동작해야하기 때문에 티리거나 모디파이어가 필요하지는 않습니다. 그럼 이제 점프도 만들어 봅시다.

스페이스바를 누르면 점프하는 간단한 방법이나 bool로 설정합니다. 또한, on off 동작하는 방식 때문에 키가 눌렸는지 여부만 중요하기에 모디파이어(추가적인 수치나 축 값)이 필요하지 않습니다. 시점 회전을 담당하는 IA_Look을 만들어 봅시다.

마우스 입력은 항상 가로와 세로가 동시에 필요하기에 Axis2D로 설정해줍시다. 다음 스프린트도 만들어 봅시다.

shift 키를 눌러서 설정할 것이기에 bool로 설정해줍시다.
그럼 이 모든 것들을 어떤 입력 키일지 정해줍시다. IMC로 넘어갑시다.


3.Input Mapping Context


먼저 IMC를 만들어줍시다. 이미 예전에 했었지만 복습 차원에서 다시 해보겠습니다.

여기서 모디파이어에서 Swizzle Input Axis Values,Negate랑 키를 설정해보겠습니다.

Swizzle Input Axis Values 정리

Swizzle Input Axis Values는 입력값의 축(X, Y, Z)을 다른 축으로 재배치하는 Modifier입니다.

WASD 이동을 Axis2D(X, Y)로 만들 때 주로 사용합니다.

Modifier
WX축+1없음
SX축-1Negate
AY축-1Swizzle + Negate
DY축+1Swizzle

쉽게 말하면:

W → X +1  (앞)
S → X -1  (뒤)

A → Y -1  (왼쪽)
D → Y +1  (오른쪽)

왜 Swizzle을 사용하는가?

각 키 입력은 기본적으로 하나의 축 값만 가지고 들어옵니다.

예를 들어 D를 누르면:

D → X = +1

처럼 들어오는데, 우리가 원하는 건 Y축 +1입니다.

그래서 Swizzle을 사용해서:

X → Y

로 축을 재배치합니다.

그리고 A는 반대 방향이므로:

Swizzle → X축 값을 Y축으로 이동
Negate → +1을 -1로 반전

결과적으로:

        Y+
        ↑
        D
        |
A ←──── + ────→
        |
        S
        ↓
        Y-

W/S는 X축
A/D는 Y축

이제 점프도 설정해줍시다.

점프는 스페이스바를 눌렀을 때 작동하게 하고 트러기너나 모디파이어 필요없기에 기본 상태로 둡니다.

마우스 움직임을 통해 시점 회전도 설정해봅시다. 마우스는 좌우와 상하 움직임을 동시에 받아야 하기 때문에 Axis2D로 설정합니다. 여기서 X축은 Yaw(좌우 회전), Y축은 Pitch(상하 회전)을 담당하게 됩니다.

기본적으로 마우스를 오른쪽으로 움직이면 X축에 + 값이 들어오고, 왼쪽으로 움직이면 - 값이 들어옵니다. 따라서 X축은 별도의 Modifier 없이 그대로 사용하면 됩니다.

그런데 Y축은 조금 다릅니다. 마우스를 위로 움직이면 + 값이 들어오지만, 카메라의 Pitch 회전에서는 위를 바라볼 때 - 방향으로 회전해야 하기 때문에 Negate를 사용해서 Y축 값을 반전시켜줍니다.

쉽게 정리하면:

마우스 X축
오른쪽 → + → Yaw +
왼쪽   → - → Yaw -

마우스 Y축
위     → + → Negate → - → Pitch -
아래   → - → Negate → + → Pitch +

따라서 IA_Look은 Axis2D로 설정하고, Y축에 Negate Modifier를 적용해주면 마우스 움직임에 맞게 자연스럽게 시점을 회전시킬 수 있습니다.

이제 마지막으로 IA_Sprint도 설정해봅시다. Shift 키를 눌렀을 때 달리도록 만들 것이기 때문에 Bool로 설정하고, 별도의 Trigger나 Modifier 없이 기본 상태로 사용하면 됩니다.
이제 스프린트도 설정해봅시다.

스프린트도 점프와 마찬가지로 on/off 형식이기에 기본으로 세팅해둡니다.


4.PlayerController에서 IMC

만든IA와 IMC를 C++ 멤버 변수로 선언하고, 언리얼 에디터에서 이를 지정할 수 있도록 리플렉션 처리를 하겠습니다.

컨트롤러 헤더파일에서 IMC, move, jump, look, sprint 포인터 변수를 선언합시다. UPROPERTY()를 이렇게 선언하면 해당 멤버 변수를 우리가 만든 IMC나 IA에 할당해줄 수 있습니다. 또한 순수 가상함수 BeginPlay를 추가해서 게임 시작시 움직일 수 있게 해줍시다.

포인터 변수이기 때문에 nullptr로 초기화해줍시다. 초기화하지 않으면 포인터가 어떤 주소를 가리킬지 알 수 없기 때문이죵.


그리고 제 컨트롤러에서 잊지 마시고 동작들을 추가합시다!(까먹어서 왜 안 되냐고 해보고 있었던 건 안 비밀)

EnhancedInputSubsystems.h는 로컬 플레이어가 맵에 있는 플레이어를 조종할 때 사용할 입력 설정을 관리하기 위한 Local Player Subsystem을 사용하기 위해 포함시켜줍시당.

Super::BeginPlay(); 부모 클래스의 기본 BeginPlay 동작을 먼저 깔끔하게 실행하게 설정 합시다.
if (ULocalPlayer LocalPlayer = GetLocalPlayer())를 통해 현재 PlayerController에 연결된 local player 객체를 가져옵시다. (LocalPlayer는 ULocalPlayer 클래스의 포인터에용)
if (UEnhancedInputLocalPlayerSubsystem
Subsystem =
LocalPlayer->GetSubsystem()) — Local Player에서 EnhancedInputLocalPlayerSubsystem을 가져와 봅시다. (UEnhancedInputLocalPlayerSubsystem은 ULocalPlayer의 서브시스템인데, 이것도 포인터로~~)
if (InputMappingContext) — 지정해 둔 InputMappingContext가 잘 들어와 있는지 확인해 줍시다.
Subsystem->AddMappingContext(InputMappingContext, 0); — 서브시스템을 통해 우리가 할당한 IMC를 활성화시켜 줍시다! 여기서 우선순위(Priority)는 0이 가장 높은 우선순위라는 거 잊지 마세용~(그래야 정상 작동합니다)

잘 연결이 되었는지 BP_YoonjaePlayerController, 이벤트 그래프를 잘 살펴봅시다.

등록이 잘 되었군용.

확인차로 print string으로 실험해봅시다.

잘 되네용~! 그럼 이제 제가 움직이도록 설정하러 갑시다.


5. Character 클래스에 액션 바인딩 추가

지금 제 컨트롤러는 키나 마우스 입력을 감지하고, 각 입력 액션을 활성화만 해줍니다. 그럼 동작이 이루어지도록 함수들을 바인딩해줍시다.

yoonjae 헤더 파일에서액션 값을 받을 구조체를 전방 선언 해줍시다.

입력 바인딩을 처리할 가상 함수와 move와 jump를 처리할 함수 원형을 만들어줍시다. FInputActionValue는 Enhanced Input에서 이동값, 마우스 이동량 등의 입력 값을 전달하는 구조체로, IA에서 설정한 Vauletype입니다.

그리고 일단 yoonjae 소스파일에서 입력값이 필요하니 컨트롤러랑 IA랑 함수를 연결해주는 헤더입니다.

그럼 움직임을 호출하여 묶는 겁니다.

캐릭터 클래스에서 입력 기능 묶기 (SetupPlayerInputComponent)
컨트롤러에서 입력 세팅을 다 끝냈으면, 이제 캐릭터 클래스(AYoonjae)에서 키를 눌렀을 때 실제로 어떤 함수가 동작할지 싹 묶어줘야겠죵? 그 역할을 하는 게 바로 이 SetupPlayerInputComponent 함수입니다.

부모 함수 호출부터 보겠습니다.

Super::SetupPlayerInputComponent(PlayerInputComponent);
일단 부모 클래스에 있는 기본 입력 세팅 기능을 먼저 깔끔하게 실행해 주고 시작합니다.

Enhanced Input Component로 캐스팅하겠습니다.

if (UEnhancedInputComponent* EnhancedInput = Cast(PlayerInputComponent))
기본 입력 컴포넌트를 우리가 사용하는 최신 Enhanced Input 시스템(UEnhancedInputComponent)으로 안전하게 캐스팅해 줍니다. 성공하면 중괄호 안으로 진입합시다~

컨트롤러 캐스팅해서 IA 가져옵니다.

if (AYoonjaePlayerController* PlayerController = Cast(GetController()))
아까 우리가 만든 컨트롤러(AYoonjaePlayerController)에 지정해 둔 Input Action(IA) 포인터들을 가져와야 하잖아용? 그래서 현재 캐릭터를 조종 중인 컨트롤러를 내 커스텀 컨트롤러로 싹 캐스팅해 주는 겁니당.

각각의 액션들과 함수 바인딩합시다.
여기서부터는 컨트롤러에 들어있는 액션(MoveAction, JumpAction 등)이 제대로 들어왔는지 if문으로 하나씩 체크하면서 바인딩을 걸어줍니다.

이동 기능 (Move)

if (PlayerController->MoveAction)//IA_Move 액션 키를 누르고 있는 동안 Move()호출
{
// BindAction()은 쉽게 말하면 이 입력이 발생하면 묶은 것들이 모인 이 함수를 실행해!
// 첫 번째 인자: 어떤 입력 액션을 사용할지 지정
// 두 번째 인자: 어떤 시점에 함수를 실행할지 지정(액션이 발생하는 트리거 이벤트)
// 세 번째 인자: 액션 발생 시 실행할 객체(this)와 함수 포인터
EnhancedInput->BindAction(PlayerController->MoveAction, ETriggerEvent::Triggered, this, &AYoonjae::Move);
}
IA_Move 액션 키를 누르고 있는 동안(Triggered) 계속 Move 함수가 호출되도록 묶어주는 겁니당.

점프 기능 (StartJump)

if (PlayerController->JumpAction)//IA_Jump 액션 키를 누르고 있는 동안 StartJump()호출
{
EnhancedInput->BindAction(PlayerController->JumpAction, ETriggerEvent::Triggered, this, &AYoonjae::StartJump);
}
점프 액션도 마찬가지로 키가 눌려 있는 동안 StartJump 함수랑 연결해 줍니다.

시점 회전 기능 (Look)

if (PlayerController->LookAction)//IA_Look 액션 마우스가 Look()호출
{
EnhancedInput->BindAction(PlayerController->LookAction, ETriggerEvent::Triggered, this, &AYoonjae::Look);
}
마우스 움직임으로 시점을 돌릴 수 있게 IA_Look 액션도 Look 함수랑 싹 묶어주고용~

스프린트 기능 (StartSprint)

if (PlayerController->SprintAction)//IA_Sprint 액션키를 누르고 있는 동안 StartSprint() 호출
{
EnhancedInput->BindAction(PlayerController->SprintAction, ETriggerEvent::Triggered, this, &AYoonjae::StartSprint);
}
마지막으로 Shift 키를 누르고 달릴 때 쓰는 스프린트도 StartSprint 함수랑 딱 연결해 주면 끝입니다.

이렇게 하면 컨트롤러에서 세팅한 입력들이 캐릭터의 실제 동작 함수들이랑 싹 연동되면서 게임 안에서 키를 누를 때마다 캐릭터가 내 뜻대로 움직이게 됩니다.

UFUNCTION(): 입력 바인딩 함수가 언리얼 리플렉션 시스템과 연동되도록 해줍니다.
BindAction(): InputAction이 특정 이벤트(Triggered, Completed 등)를 발생시키면 지정한 함수가 실행되도록 연결합니다.
세 번째/네 번째 인자: this는 현재 객체, &클래스::함수는 실행할 함수를 의미합니다.
점프/스프린트 분리: 키를 누를 때와 뗄 때의 동작이 다르기 때문에 각각 함수를 따로 바인딩합니다.


6. 캐릭터의 이동 함수 구현

그럼 이제 입력 기능을 구현하고 그에 따라 호출하게 설정을 했으니 움직임 값을 넣어줘서 실행이 되도록 해야겠죵? Move부터 합시다.

Get()로 2차원 벡터 형태로 입력값을 가져오는 함수입니다. x는 앞뒤, y는 왼오를 담당합니당. 그럼 이제 직접 언리얼 에디터에서 작동하는지 확인해보겠습니다.
다행히 잘 작동합니다. 그럼 이제 점프 함수를 구현해보겠습니다.


7. 캐릭터의 점프 함수 구현


기본적으로 캐릭터 클래스에서 Jump함수와 StopJumping은 기본으로 제공해줍니다. bool 값으로 가져와서 true(키가 눌림), false(키가 눌리지 않음)으로 설정해 각각 점프와 점프 중지를 해줍시다.


8.캐릭터의 기본 시점 회전 구현


Yaw(롤 잘해요?갸우뚱)는 수평 회전을 변경하며 Pitch(피치볼 하쉴? 끄덕끄덕)는 수직 회전을 변경합니다. 실제 어느 방향으로 얼마나 회전할지는 프로젝트 세팅-Input-Mouse Sensitivity나 LookInput에 곱해줄 스케일(Modifiers)를 통해 조정이 가능합니당. 그럼 되즌지 확인해보겠습니다.

9.스프린트 동작 구현하기 및 트러블 슈팅

이제 스프린트 할 차례죠? 언리얼 엔진의 ChracterMovementComponent에는 MaxWalkSpeed라는 속성이 있습니다. MaxWalkSpeed 값을 변경하면 캐릭터의 이동 속도가 즉시 바뀝니다. Shift키를 누르는 순간 이것을 빠른 속도로 하고 떼면 기본 속도로 돌려놓으면 스프린트 효과를 낼 수 있겠죠? 그러면 먼저 윤재 헤더 파일에 관련 멤버 변수를 추가해줍시다.

기본 걷기 속도, multiplier를 통한 속도 가속화 정도, 그리고 실제 달리기 속도 변수들을 선언해줍시다.

그 다음에 소스파일에서 속도 관련 헤더 파일을 추가해주고

생성자 기본값을 걷기 속도로 설정해줍시다. 여기서 GetCharacterMovement()함수는 캐릭터 클래스에 기본 내장 함수로 UCharacterMovementComponent* 포인터를 반환합니다. 그럼 이제 shift키를 눌렀을 때 바뀌게 해야겠죠? 설정해줍시다.

이제 한 번 실험해보러 가겠습니다. 근데 저희가 애니메이션이 없어서 구별이 힘들고 확실한 숫자가 있으면 좋잖아용? Tick에 나오게 표시합시당.

언리얼 엔진 기본 함수인 AddOnScreenDebugMessage을 사용하여 출력하게 설정합시다.
어? 근데 제대로 실행이 안 됩니다. 한 번 shift 키를 누르면 속도가 900에서 고정이 됩니다. 이 문제 해결하러 가봅시다.

일단 키 누를시 함수 호출하는 것을 제가 빼먹었군용.....일단 추가해줍시다. 그리고 실행해도 똑같군용.....

속도 확인하는 김에 내친김으로 한 번 떨어져 봤습니다.......4100으로 속도가 최대로 설정이 되어있군요. 이게 메쉬 자체의 설정값인지 궁금했었는데 캐릭터 클래스의 움직임 컴포넌트의 MaxWalkSpeed 설명을 읽어보니 이 친구가 속도를 제한한다고 쓰여있군용.

아무튼 혹시나 제가 IMC 설정에서 잘못 설정했나 확인해봤습니다. 기본 설정으로 되어 있어서 누르고 있을 땐 되고 떼면 해제되게끔 설정이 되어 있습니다. 여기는 아닌 거 같군용.

헤더 파일에서도 다 정상적으로 설정되어 있습니다. 혹시 몰라서 기존 sprintspeed에서 블프 수정도 가능하게 바꿔봤습니다. 아마 안 될 거 같긴한데 실험은 해보겠습니다.

예, 안 됩니다. 그럴 줄 알았습니다. 그래서 컨트롤러 쪽 문제일 거 같아서 살펴보니 제가 stopsprint를 빼먹었네요....야근과 늙음 이슈였습니다. 추가했습니다.

어...? 근데 이번에는 속도가 600에서 고정되어서 안 바뀝니다. 아무래도 startsprint가 stopsprint를 위에서 계속 먼저 나오고 잡아(?)먹어서 stopsprint가 씹힌 거 같습니다. 아무래도 ETriggeredEvent가 문제이지 않을까 싶습니다.


Triggered를 Completed로 바꾸겠습니다.

이제야 제대로 적용이 되군용. 근데 눈에 안 띄일까봐 속도 차이 숫자로 한 번 봅시다.

보이십니까? 아름답게 바뀌는 이 숫자! 뿌듯하네용.


10. 과정 중 배운(복습한) 개념 및 문법 정리

1.PlayerController
1-1 Playercontroller란?
사용자가 키보드, 마우스, 게임패드 등에서 입력을 받으면 해석하여 동작을 명령하는 핵심 클래스
플레이어 입력은 PlayerController에서 처리한다!
처리 기본 흐름
1.사용자 조작 신호 2. 해석 3.소유하고 있는 폰에게 구체적인 명령 전달
멀티플 개별로 관리

1-2 주요 기능
입력 처리
-입력 장치의 이벤트를 처리, Enhanced Input 시스템 사용, 액션/축 매핑을 보다 체계적으로 설정
-C++에서 SetupInputComponent() 함수를 오버라이드하여, 블프에서 이벤트 그래프를 통해 입력 로직을 구현
카메라 제어 로직
-시점 회전이나 줌 인/아웃 같은 카메라 동작 수행 가능
HUD 및 UI과 상호작용
-UMG(Unreal Motion Graphic) 기반 UI를 통해 버튼 클릭 등 이벤트를 PlayerController에서 받을 수 있음
Possess / UnPoessessPlayerController는 특정 폰에 빙의여 폰을 제어
필요할 때 UnPossess() 함수를 호출하여 폰과 연결을 해제한 뒤, 다른 폰과 바꿔 탈 수 있음
멀티플 시 각 플레이어마다 고유의 PlayerController가 있고 이 컨트롤러가 특정 Pawn을 소유함으로써 서로 다른 캐릭터 조작이 가능

2.Enhanced Input System의 이해 및 Input Action 설정
2-1 Enhanced Input System이란?
언리얼 5 이전에서 사옹하던 전역 Project Settings->Input 시스템이 대체하거나 확장하기 위해 Enhanced Input 시스템이 제공
Enhanced Input은 입력 설정을 입력 맵(Input Mapping Context,IMC)과 입력 액션(Input Action,IA)으로 나누어 관리함 (예전에 했었습니다..예)
2-2 Input Action(IA)
Input Action은 캐릭터의 동작을 추상화한 단위

3.Input Mapping Context
3-1 IMC 생성
IMC는 여러 개의 IA들을 한데 모아놓은 매핑 설정 파일입니다.(전에 했던 거 바로 그거입니다!)자세한 설명은 생략하겠습니다.

문법
Super는 바로 부모 클래스(Superclass)를 가리키는 편의용 키워드
추상 클래스 → 공통적인 부모 "너는 나의 종류다."
인터페이스 → 특정 기능을 제공한다는 계약 "너는 이 기능을 할 수 있다." 구현 내용 없이 기능의 규칙만 정의


11. 마무리

오늘은 PlayerController부터 Enhanced Input, 입력 바인딩, 이동·점프·시점 회전, 스프린트까지 쭉 구현해봤습니다.

특히 스프린트가 안 돼서 한참 헤맸는데, 알고 보니 StopSprint()를 빼먹은 것도 모자라 Triggered로 잘못 바인딩해놓은 제 실수였습니다.

역시 언리얼은 잘못이 없습니다. 잘못은 항상 제 코드에 있습니다.

그래도 직접 삽질하면서 하나씩 해결하다 보니 입력 시스템의 흐름도 확실하게 이해할 수 있었습니다.

야근과 늙음 이슈로 인해 뇌의 MaxWalkSpeed가 0이 되어가고 있거든요.

오늘은 여기까지 하고, 다음에는 다시 힘차게 달려보겠습니다!

profile
게임 개발자 꿈나무

0개의 댓글