나만의 작은 도서관

[TIL][C++] 251002 MMO 서버 개발 109일차: Input Action & Input Mapping Context 본문

Today I Learn

[TIL][C++] 251002 MMO 서버 개발 109일차: Input Action & Input Mapping Context

pledge24 2025. 10. 2. 21:37
주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다. 

Input Action 이란?

  • Input Action은 플레이어의 입력을 처리하기 위한 요소로, 마우스, 키보드, 게임 패드와 같은 입력 장치에서 발생한 원시 입력(Raw Input)을 게임 내에서 일어날 실제 행동과 연결하는 핵심적인 요소이다. 언리얼 공식 문서에서는 아래와 같이 설명한다.
Input Action은 향상된 입력 시스템(Enhanced Input System)과 프로젝트 코드 간의 통신 링크입니다.
Input Action은 데이터 에셋이라는 점만 다를 뿐, 액션 및 축(Axis) 매핑 이름과 개념적으로 같습니다. 각 입력 액션은 '웅크리기'나 '무기 발사(Fire)'같은 사용자가 할 수 있는 행동을 나타내야 합니다. Input Action의 상태(State)가 변경될 때 블루프린트 또는 C++에 입력 리스너(Input Listeners)를 추가할 수 있습니다.

Input Action은 여러 타입이 될 수 있으며, 타입에 따라 액션의 동작이 결정됩니다. 단순한 bool 액션을 만들 수도 있고, 더욱 복잡한 3D 축을 만들 수도 있습니다. 액션 타입이 그 값을 결정합니다. 

 

 

Input Action은 “행동의 정의”를 나타낸다.

  • Input Action은 게임 내의 행동을 나타낸다. 행동의 예로는 Move, Jump, Attack 등이 있다.
  • 여기서 헷갈리면 안 되는 부분이 있는데, Input Action는 어디까지나 특정 동작이나 행동을 후상화한 단위이므로, 정의한 행동의 실질적인 로직이 들어있지 않다는 것이다(인터페이스 자체에 로직이 들어있지 않는 것처럼). 그저 “이러이러한 행동이 있음”을 정의하는 것이다.

 

그래서 Input Action은 혼자 사용되지 않는다.

  • 그래서 Input Action은 혼자 사용되지 않는다. Input Action은 단순히 ”행동이 있음”을 나타낼 뿐이지 행동을 해야 할 때 어떤 로직을 실행해야 하는지, 어떤 입력으로 트리거 되는지 아무것도 정해진 게 없기 때문.
    • 여기서 “행동을 해야 할 때 어떤 로직을 실행해야 하는가?”는 BindAction과 같은 함수 바인딩으로,
    • “어떤 입력으로 트리거 되는가?”는 키 바인딩으로 결정된다.

 

Input Action의 Value Type

  • Input Action에 들어가서 디테일 패널을 보면 Value Type이라는 프로퍼티가 있는데, Value Type란 입력 장치로부터 들어온 조작의 결과를 어떤 타입으로 출력할 것인지를 의미한다.
  • 일반적으로 Value Type은 입력 장치의 입력 방식을 따라간다. 예를 들어 “딸깍”으로 입력하는 키보드의 키의 Value Type은 Digital, 스틱을 잡고 360도 돌릴 수 있는 조이스틱의 Value Type은 Axis2D에 해당한다.

 

각 Value Type 종류 설명

// Digital
[](키 안 누름) --> [O](키 누름) // 000000...111111111
  • Digital
    • Digital 타입은 0 또는 1의 값만 가지는 타입으로, 입력이 없으면 0, 있으면 1을 반환한다.
    • 단순히 “입력이 들어왔는가?”에 대한 결과를 출력하기 때문에 대부분의 키보드 키 입력에 사용된다.
// Axis1D
[<-----O---->](오른쪽으로 슬라이드) // -0.98 -0.97 -0.96 ... 0.98 1.0
  • Axis1D
    • Axis1 D 타입은 1차원 축에 대한 입력으로, “입력의 정도”에 따라 -1 ~ 1 사이의 float 값 하나가 반환된다.
    • 게임패드의 아날로그 스틱이나 마우스에서 특정 축(상하 또는 좌우)에 대한 움직임만 뜯어올 때 사용한다.
// Axis2D
[  ][^][  ]
[<-][O][->]
[  ][v][  ]
(Window + 마우스 입력 장치 기준 원점에서 오른쪽 대각선 이동) 
// (0, 0) (-0.2, 0.2) (-0.4, 0.4) ... (-0.1, 0.1)
// dx는 아래가 양(+)의 방향, dy는 오른쪽이 양(+)의 방향. 축 순서는 XY
  • AxisD
    • Axis2D 타입은 2차원 축에 대한 입력으로, “입력의 정도”에 따라 -1 ~ 1 사이의 float 값 2개가 들어있는 Vector2D 타입이 반환된다.
    • 두 축에 대한 값을 하나의 타입으로 표현하고 싶은 경우 Axis2D를 사용한다. 예를 들어, 1) 캐릭터의 360도 방향 이동, 2) 카메라 회전 등이 있다.
      • 여기서 헷갈리면 안 되는 부분이 있는데, 하나의 Value Type의 입력값을 반드시 하나의 입력 장치만 결정할 수 있는 건 아니라는 것이다. 물론, “게임패드의 아날로그 스틱 입력” 또는 “마우스의 상하좌우 입력”과 같이 하나의 입력장치가 Axis2D의 두 개의 축 값을 전부 결정지을 수 있겠지만, 키보드의 여러 키의 조합으로 Axis2D를 사용할 수도 있다. 이에 대해선 뒤에서 다룰 “Input Mapping Context와 Modifier”에서 자세히 다루겠다.
    • (참고로, 입력 장치에 들어오는 Raw Input 값은 언리얼 시스템과 상관없이 OS에 의해 결정된다. 즉 언리얼은 이 값을 “정하는” 것이 아니라, 읽어 들이는 것이므로, 값에 대한 음수/양수의 방향성, XY 순서가 맘에 들지 않는다면, Input Mapping Context의 Modifier에서 바꿔서 사용해야 한다.)

왼쪽은 HOTAS, 오른쪽은 VR Controller

  • Axis3D
    • Axis3D 타입은 3차원 축 입력으로, “입력의 정도”에 따라 -1 ~ 1 사이의 float값 3개가 들어있는 Vector 타입이 반환된다.
    • 세 축(대부분의 경우 3D 공간축)에 대한 값을 하나의 타입으로 표현하고 싶은 경우 Axis3D를 사용한다. 굉장히 드문 케이스이며, 예를 들면 1) 비행 시뮬레이션 , 2) VR 등이 있다.
      • Axis3D를 하나의 입력 장치에서 처리하는 경우는 특수 분야 컨트롤러인 경우가 많다. 스틱 자체에 3축 회전값을 제공하는 HOTAS나 VR 컨트롤러가 그 예시이다.

Input Mapping Context

  • Input Mapping Context는 입력 장치의 입력과 InputAction을 바인딩하는 Mapping 도구로써, Input Action은 이러한 Input Mapping Context에 의해 트리거 될 키가 정해지고, 실행될 로직 또한 정해진다.
  • Input Mapping Context는 조작이 게임에 반영되는데 굉장히 중요하다. Input Mapping Context를 추가(탑재?) 하지 않은 캐릭터는 아무리 키를 눌러도 움직이지 않는다. Input Mapping Context가 없으면 Input Action과 바인딩된 함수가 호출되지 않기 때문! 전체적인 과정을 예시로 설명하면 아래와 같다.

전체적인 과정 정리 예시(이동)

  1. 앞으로 이동하는 행동을 정의하기 위해 “MoveForward”라는 이름의 Input Action을 하나 만든다.
  2. W키 입력을 통해 앞으로 이동시키고 싶기 때문에 Input Mapping Context에서 W키와 MoveForward키를 바인딩한다. 이때, 필요하다면 Modifiers를 추가한다.
  3. W키를 꾹 눌렀을 때 앞으로 이동시키고 싶기 때문에 트리거 종류는 Triggered로 하여 미리 만들어둔 이동 함수 Move를 C++에서 BindAction으로 바인딩해 준다.
  4. 이제 W를 꾹 누를 때마다 프레임마다 Move가 실행된다.

 

 

Modifier

  • Modifier은 입력받은 값을 변환해 주는 장치이다. 입력 장치로 들어온 값의 순서 및 값의 방향(+ 또는 -)이 원하는 값으로 바꾸고 변환하고 싶을 때 사용한다.

 

Swizzle Input Axis Values

  • Swizzle Input Axis Values는 입력 장치로 들어온 값을 적용할 축을 바꿔주는 Modifier의 요소이다. Swizzle Input Axis Value는 Order라는 요소를 하나 들고 있는데, Order의 축 순서를 바꿔주면 주야장천 X축 값으로 들어가던 녀석을 Y축이나 Z축의 값으로 들어갈 수 있도록 할 수 있다.
  • Order는 들어온 값을 적용할 때 축의 우선순위를 설정하는 요소이다. 값이 반드시 3개일 필요는 없으며, 하나인 경우 가장 맨 앞에 있는 축의 값으로 결정된다. 따라서, XYZ면 X축 먼저, YZX면 Y축 먼저, ZXY면 Z 축 먼저 값을 적용한다. 값이 2개인 경우, 3개인 경우도 똑같이 Order에 적힌 축의 순서대로 값이 대응된다. 예를 들면 아래와 같다.
    (1.0, 2.0, 3.0)
    // xyz 인 경우: (1.0, 2.0, 3.0)
    // yzx 인 경우: (3.0, 1.0, 2.0) 
    // zxy 인 경우: (2.0, 3.0, 1.0)
    • digital 값의 경우 0 또는 1만 들어온다. 문제는 wasd 이동을 하기 위해선 음수로 이동을 해야 하는데, digital 값은 그 자체로는 음수로 표현할 수 없다
    • 또 하나 문제가 있는 게 모든 값은 x축의 값으로 들어온다는 것. 그래서 y축 이동이나 z 축 이동은 할 수 없다.

 

Negate

  • Negate는 입력 장치로 들어온 값에 -1을 곱해주는 요소이다. 이러한 Negate를 이용해 값을 양수에서 음수로, 음수에서 양수로 바꿀 수 있다.

대표적인 Modifier 사용 예시: wasd 이동

  • 키보드로부터 들어오는 값은 누르지 않음의 0, 눌렀음의 1만 들어온다. 값도 하나라서 축의 순서 정보 또한 없다. 문제는 이러한 키 입력으로 캐릭터의 상하좌우 또는 전후좌우로 이동해야 한다는 것이다.
  • 언리얼의 경우 기본 축의 순서가 XYZ로 되어 있기 때문에 키보드의 키 값은 전부 X축으로 들어갈 것이며, 음수도 없어서 음수 방향 이동도 못하는 상황. 이때 각 키 값에 대해 Modifier를 세팅하면 값을 음수로 바꿀 수도, 축도 바꿀 수도 있다.
  • 여기서 2가지 선택을 할 수 있다. 1) Modifier까지는 표준을 따르고 실제 로직을 반영하는 BindAction에서 언리얼 축 시스템에 맞출 것이냐, 2) Modifier에서부터 언리얼 축 시스템에 맞출 것이냐이다. 각각의 경우에 맞춰 나온 결과는 아래와 같다.

 

첫 번째로 결정. Modifier까지는 표준에 맞춘다.

  • 표준의 경우 양의 방향인 좌우이동이 x축, 앞뒤이동이 y축이 된다. 즉 wasd는 각각 아래와 같이 축 이동을 해야 한다.
    • W(앞으로 이동): y값 증가
    • S(뒤로 이동): y값 감소
    • A(왼쪽으로 이동): x값 감소
    • D(오른쪽으로 이동): x값 증가
  • 언리얼에서 제공하는 Input Mapping Context인 IMC_Default가 여기에 해당하며, 아래와 같이 세팅되어 있다.

  • 이 경우, 나중에 실행시킬 로직에서 Vector2D의 x값은 Y축에, y값은 X축에 적용하는, 즉, 뒤집어서 적용해야 한다는 불편함이 있다. (그래도 기능의 분리? 이런 거 생각하면 이게 맞다고 생각한다.)

 

두 번째로 결정. Modifier에서부터 언리얼 축 시스템에 맞춘다.

  • 언리얼 엔진은 왼손 좌표계 + z-up인 3D 좌표계이다. 즉 wasd는 각각 아래와 같이 축 이동을 해야 한다.
    • W(앞으로 이동): x값 증가
    • S(뒤로 이동): x값 감소
    • A(왼쪽으로 이동): y값 감소
    • D(오른쪽으로 이동): y값 증가

 

그냥 넣은 또 다른 예시: IA_Look

  • IA_Look은 마우스 입력에 따른 카메라 회전을 다루는 Input Action으로, IMC_Default에 Modifier가 어떻게 적용되었는지 보면 위와 같이 되어있다.
  • 여기서 “어? 왜 Negate가 붙었고, Y에만 적용되었지?”할 수 있는데, 위에서 잠깐 말한 “마우스가 아래로 내려가면 Y값이 증가하고, 오른쪽으로 이동하면 X값이 증가한다”와 관련이 있다. 언리얼에서 물체의 회전 방향이 시계 방향(Clockwise, 줄여서 CW)이라 회전값이 증가하면 시계방향으로 돌게 된다.
    • 따라서 마우스 입력값을 그대로 적용하면 X값은 문제가 없지만(마우스가 오른쪽으로 이동하면 → 카메라의 Yaw를 시계방향으로 회전 → 오른쪽을 바라봐서 문제없음),
    • Y값은 반대로 적용되어 문제가 발생한다. ( 마우스가 아래로 이동 → 카메라의 Pitch를 시계방향으로 회전 → 위를 바라봐서 문제 발생!)
    • 이러한 이유로 Y값만 Negate를 추가하는 것!

Input Action 트리거 시 발동될 함수를 바인딩하는 함수 BindAction(), BindAxis()

BindAction("IA_이름", 
"입력 트리거 타입", "타겟 객체", "실행할 멤버 함수 포인터")
  • 이제 마지막으로 Input Mapping Context에서 키와 바인딩된 Input Action을 로직과 연결할 차례이다. Input Action과 함수를 바인딩할 때 사용하는 함수는 BindAction과 BindAxis 함수가 있다.

BindAction, BindAxis 사용 예제

UEnhancedInputComponent->BindAction(JumpAction, 
ETriggerEvent::Triggered, this, &AMyCharacter::Jump);
UEnhancedInputComponent->BindAxis3D("Move3D",
 this, &AMyCharacter::Move3D);
UEnhancedInputComponent->BindAxis2D("Look"
, this, &AMyCharacter::Look);
// 게임패드의 아날로그 스틱을 이용해서 앞으로 이동.
UEnhancedInputComponent->BindAxis("MoveFoward",
this, &AMyCharacter::MoveForward);

 

 

바인딩하는 함수의 규칙

  • Input Action과 키를 바인딩하여 나온 결과물이 Value Type의 형태로 나온다고 했다. 여기서 나온 결괏값을 통해 로직이 실행시켜야 의미가 생기는데, 실행할 로직은 함수 포인터를 인자로 넘겨주는 방식으로 진행된다.
  • 여기서 우리가 생각해야 할 부분은 “어떤 Value Type은 bool이고, 어떤 건 Vector이고 이리 들쭉날쭉한데 그럼 함수도 이에 맞춘 매개변수 리스트를 가져야만 하는 걸까?”이다
  • 결론적으로 그렇지는 않고, 언리얼에선 FInputActionValue라는 일관된 타입으로 인자를 받고 함수 내부에서 타입을 캐스팅하여 쓰는 방식을 사용한다. 따라서, IA와 바인딩할 함수의 매개변수 리스트는 FInputActionValue의 왼쪽 레퍼런스 타입만 존재하면 된다. 아래와 같이 말이다.
void Input_Something(const FInputActionValue& InputValue)
{
	FVector2D MovementVector = Value.Get<FVector2D>();
}

 

 

조금 깊게 생각하기: FInputActionValue 사용이 강제되는가?

  • 그렇다면 FInputActionValue라는 모든 Value Type을 저장할 수 있는 범용 컨테이너의 사용이 강제되는 걸까? 그냥 Digit 타임의 Value Type이면 bool 타입을 함수의 매개변수로 사용하면 안 되는 걸까?
  • 안 되는 건 아니지만, FInputActionValue를 사용하는 것이 가장 일반적이고 권장되는 방식이다. 즉, 아래와 같지 곧바로 Value Type과 동일한 타입으로 매개변수를 지정해도 문제가 발생하진 않는다.
void Input_Something(const bool& value)
{
//...
}
  • 그런데 위 코드의 문제점은 Value Type과 다른 타입으로 매개변수를 지정했을 때 대응할 수 없다는 것이다. 그래서 범용으로 받은 다음, 안에서 Get을 통해 런타임 타입 검사를 진행하는 것이다.
    • FInputActionValue::Get은 런타임에 타입을 검사하여, 타입이 불일치하는 경우 ensureMsgf로 경고를 출력하고,
      • 디버그 모드에서는 ensure hit(에디터 경고 창)
      • Shipping 빌드에서는 기본값을 반환한다.

 

트리거 상태(Trigger State, ETriggerEvent 타입)

  • 트리거 상태(Trigger State)는 액션의 현재 상태를 나타낸다. 가장 자주 사용되는 상태는 'Triggered'이며, 트리거 상태는 C++과 블루프린트 둘 다에서 특정 상태에 바인딩할 수 있다.
  • 키의 입력을 구분하는 BindAction함수의 경우, 인자로 트리거 종류를 설정해야 한다. 트리거 종류를 설정하여 정확히 어떤 입력 이벤트가 들어왔을 때 함수를 실행할 것이지 설정할 수 있다. 트리거 종류는 아래와 같다.
    • Started: 입력 시작 시 트리거 발동(ButtonDown)
    • Triggered: 트리거 조건이 충족되어 액션이 실제로 실행 중일 때 발동
      • 유효한 축 입력이 들어올 때마다 반복되거나, 조건 충족 시 단발로 발동 
      • 참고로, Started가 발동하는 시점도 포함하므로, Started를 정의했다면 Triggered도 같이 발동한다.
    • Completed: 입력이 끝나는 경우(ButtonUp)
    • Canceled: 다른 조건으로 중단됨
    • None: 비활성 상태

 

트리거 상태 Triggered에 대해 추가 설명

  • Triggered는 Value Type이 digital이냐 그 외 나머지 Axis 시리즈이냐에 따라서 발동 조건이 다르다.
  • Input Action의 Value Type이 Digital인 경우 처음 키가 입력되는 시점(Pressed)에만 Triggered 상태로 평가되며,
  • Value Type이 Axis1D, Axis2D, Axis3D인 경우, 해당 키를 누르고 있는도안 Triggered 상태로 평가한다.
  • 따라서, Digital 타입은 한 번만 Triggered로 평가되며, Axis 타입은 누르고 있는 동안 매 Tick 마다 Triggered로 평가된다.

 

Q. Input Mapping Context에 있는 Trigger가 있는데… 이거랑 다른 건가?

  • 좀 찾아봤는데 그런 듯하다. 정확히는 모르겠지만 입력이 발생했을 때 보다 세밀하게 상황을 분류하여 제어하고 싶을 때 사용하는 듯하다. 예시는 아래와 같다.
    • 일정 세기 이상일 때만 발동(Threshold)
    • 지정된 시간 동안 누르고 있어야 발동(Hold)
    • 여러 입력을 조합하여 사용(Combo, Chord)
  • wasd와 같이 간단한 경우에는 사용하지 않아도 충분하다고 한다.

Q. 트리거 상태에 대한 확인 주기는 어떻게 될까?

  • 언리얼의 Enhanced Input 시스템은 매 프레임(Tick)마다 입력 상태를 평가한다. 즉, 따로 Tick함수에서 확인하는 코드를 추가하지 않아도 내부적으로 알아서 입력을 확인한다는 것
  • 즉, Trigger의 경우 입력이 유지되는 동안 1초에 프레임 수만큼 반복적으로 호출된다.
  • 매 프레임마다 확인한다는 특성 때문에 클라이언트의 FPS 설정에 영향을 받으며, 프레임이 높을수록 호출 수도 증가한다.

결론

  • 입력 이벤트 평가는 프레임 기반 반복 호출