Unreal C++를 이용한 플랫폼 게임 3 - 캐릭터 입력과 애니메이션 재생
Enhanced Input의 Mapping Context와 Input Action을 PlayerController와 Character에 연결하고, 이동 결과가 Animation Blueprint의 애니메이션 재생으로 이어지는 구조를 정리했습니다.
입력부터 캐릭터 애니메이션까지
이번 프로젝트의 플레이어는 ATpsPlayerController와 ATpsPlayer로 나뉜다. Controller는 어떤 입력 설정과 액션을 사용할지 보관하고, Character는 액션이 발생했을 때 실제 이동, 시점 회전, 점프와 상호작용을 수행한다.
애니메이션은 C++에서 PlayAnimation() 같은 함수를 직접 호출하지 않는다. C++이 캐릭터를 움직이면 CharacterMovementComponent의 속도와 공중 상태가 바뀌고, 캐릭터 Blueprint에 연결된 Animation Blueprint가 그 값을 읽어 Idle, Walk, Run, Jump 애니메이션을 선택하는 구조다.
Enhanced Input 구성 요소
Enhanced Input을 사용하려면 크게 두 종류의 에셋이 필요하다.
UInputMappingContext: 실제 키나 마우스 입력과 액션의 대응 관계를 모은다.UInputAction: Move, Look, Jump, Interact처럼 게임에서 사용할 의미 단위의 입력이다.

프로젝트에는 IA_Move, IA_Look, IA_Jump, IA_Interact 네 개의 Input Action과 이를 실제 입력 장치에 연결하는 IMC_Default Mapping Context를 만들었다. 액션 에셋은 입력의 의미와 값의 형태를 정의하고, Mapping Context는 그 액션을 키보드, 마우스, 게임패드 같은 물리 입력에 연결한다. 이 둘을 분리하면 C++ 코드는 특정 키를 몰라도 Move나 Jump라는 의미만 처리할 수 있다.
Input Action의 값 형식

IA_Move의 Value Type은 Axis2D (Vector2D)다. 하나의 액션 값에서 X축과 Y축을 함께 받아 좌우와 앞뒤 이동을 표현한다. Character의 이동 콜백은 이 2차원 값을 읽어 Right Vector와 Forward Vector에 각각 적용한다.

IA_Look도 Axis2D (Vector2D)를 사용한다. 마우스나 오른쪽 스틱의 가로 입력은 Yaw 회전, 세로 입력은 Pitch 회전으로 전달한다. Move와 같은 자료형이지만, 값이 캐릭터 위치가 아니라 Controller의 시점 회전에 사용된다는 점이 다르다.

IA_Jump는 누름 여부만 필요하므로 Digital (bool) 형식이다. 액션에 등록한 Trigger가 입력 조건을 판정하고, 조건을 만족해 Triggered 이벤트가 발생하면 OnJump()가 실행된다.

IA_Interact 역시 Digital (bool) 형식이며 하나의 Trigger를 사용한다. 이동이나 시점처럼 연속적인 축 값이 필요하지 않고, 상호작용 입력이 활성화됐는지만 Character에 전달하면 되기 때문이다.
Mapping Context에서 키 조합 만들기

IMC_Default에서는 IA_Jump를 Space Bar에 연결하고, IA_Move에는 W, A, S, D 키를 등록했다. Mapping Context가 이 물리 키 입력을 액션 단위로 변환하므로 Character는 Space Bar나 W 키를 직접 검사하지 않고 Jump와 Move의 결과만 받는다.
IA_Move처럼 2차원 값을 만드는 액션은 키별 Modifier를 사용한다. 키보드의 각 키는 기본적으로 하나의 축 값만 만들기 때문에 Swizzle Input Axis Values로 값을 X축 또는 Y축에 배치하고, 반대 방향 키에는 Negate를 추가해 부호를 뒤집는다. 화면에서 S에는 두 개의 Modifier가 설정되어 전후 축으로 바꾼 값을 반전하고, A에는 Negate를 적용해 D와 반대 방향의 값을 만든다. 이렇게 조합된 최종 Vector2D 값만 OnMove()에 전달되므로, 이동 코드는 어떤 키가 눌렸는지 개별적으로 검사할 필요가 없다.
ATpsPlayerController는 이 에셋들을 프로퍼티로 가진다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Properties")
TObjectPtr<UInputMappingContext> DefaultMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Properties")
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Properties")
TObjectPtr<UInputAction> LookAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Properties")
TObjectPtr<UInputAction> JumpAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Properties")
TObjectPtr<UInputAction> InteractAction;
코드에는 에셋 경로를 고정하지 않았다. Controller를 상속한 Blueprint에서 실제 Mapping Context와 Input Action 에셋을 지정할 수 있다.
Mapping Context 등록하기
ATpsPlayerController::BeginPlay()는 로컬 플레이어의 Enhanced Input Subsystem에 기본 Mapping Context를 추가한다.
void ATpsPlayerController::BeginPlay()
{
Super::BeginPlay();
if (!DefaultMappingContext)
{
UE_LOG(LogTemp, Warning,
TEXT("Default Mapping context is invalid"));
return;
}
if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
}
}
마지막 인자 0은 Mapping Context의 우선순위다. 여러 Context가 같은 키를 사용할 때 더 높은 우선순위의 설정이 먼저 적용된다.
이 등록이 빠지면 Input Action 에셋이 존재하더라도 실제 키 입력과 연결되지 않는다.
Character에서 액션 바인딩하기
ATpsPlayer::SetupPlayerInputComponent()는 기본 UInputComponent를 UEnhancedInputComponent로 캐스팅한다. 이어 현재 Controller가 ATpsPlayerController인지 확인해 그 안의 액션을 가져온다.
UEnhancedInputComponent* EnhancedInput =
Cast<UEnhancedInputComponent>(PlayerInputComponent);
ATpsPlayerController* CastedController =
Cast<ATpsPlayerController>(GetController());
두 캐스팅이 성공하면 액션과 콜백 함수를 연결한다.
EnhancedInput->BindAction(
CastedController->MoveAction,
ETriggerEvent::Triggered,
this,
&ATpsPlayer::OnMove);
EnhancedInput->BindAction(
CastedController->LookAction,
ETriggerEvent::Triggered,
this,
&ATpsPlayer::OnLook);
EnhancedInput->BindAction(
CastedController->JumpAction,
ETriggerEvent::Triggered,
this,
&ATpsPlayer::OnJump);
Triggered는 액션의 트리거 조건이 충족되는 동안 콜백을 실행한다. 이동과 시점 회전처럼 계속 값을 받아야 하는 입력에 어울린다.
이동 입력 처리하기
Move 액션은 2차원 값을 전달한다. Y 값은 앞뒤, X 값은 좌우 이동에 사용한다.
void ATpsPlayer::OnMove(const FInputActionValue& Value)
{
AddMovementInput(
GetActorForwardVector(),
Value.Get<FVector2D>().Y);
AddMovementInput(
GetActorRightVector(),
Value.Get<FVector2D>().X);
}
AddMovementInput()은 Actor의 위치를 즉시 바꾸는 함수가 아니다. 이동 방향과 크기를 Character에 전달하고, UCharacterMovementComponent가 가속도, 최대 속도, 충돌과 바닥 상태를 반영해 실제 이동을 처리한다.
현재 코드는 캐릭터의 Forward와 Right 벡터를 기준으로 한다. 따라서 캐릭터가 바라보는 방향에 상대적으로 이동한다. 카메라가 바라보는 방향을 기준으로 이동하려면 Controller의 Yaw 회전으로 Forward와 Right 벡터를 다시 계산하는 방식이 필요하다.
시점 회전과 점프
Look 액션의 X와 Y는 Controller의 Yaw와 Pitch 입력에 전달한다.
void ATpsPlayer::OnLook(const FInputActionValue& Value)
{
AddControllerYawInput(Value.Get<FVector2D>().X);
AddControllerPitchInput(Value.Get<FVector2D>().Y);
}
플레이어의 카메라는 Spring Arm 아래에 붙어 있다.
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
SpringArm->SetupAttachment(GetCapsuleComponent());
Camera->SetupAttachment(SpringArm);
Spring Arm은 3인칭 카메라의 거리를 유지하면서 벽과 충돌할 때 카메라가 지형 안으로 들어가는 문제를 줄이는 데 사용한다. Blueprint에서 Spring Arm이 Pawn Control Rotation을 사용하도록 설정하면 Controller 회전 입력이 카메라 시점으로 이어진다.
점프는 ACharacter가 제공하는 기능을 사용한다.
void ATpsPlayer::OnJump(const FInputActionValue& Value)
{
if (CanJump())
{
Super::Jump();
}
}
CanJump()로 현재 점프할 수 있는 상태인지 확인한 뒤 부모 클래스의 Jump()를 호출한다. 실제 수직 속도와 착지 판정은 Character Movement가 처리한다.
이동 코드가 애니메이션 재생으로 이어지는 과정
이 프로젝트의 C++ 소스에는 애니메이션 클립을 직접 선택하는 코드가 없다. 캐릭터의 Skeletal Mesh와 Animation Blueprint는 Character를 상속한 Blueprint 에셋에서 설정하는 방식이다.
연결 흐름은 다음과 같다.
- Mapping Context가 키 입력을 Move나 Jump Input Action으로 변환한다.
ATpsPlayer의 콜백이AddMovementInput()또는Jump()를 호출한다.CharacterMovementComponent가 캐릭터의 속도와 이동 모드를 갱신한다.- Animation Blueprint가 소유 Pawn의 속도와 공중 여부를 읽는다.
- Anim Graph의 State Machine 또는 Blend Space가 재생할 애니메이션을 결정한다.
Animation Blueprint에서는 보통 다음과 같은 값을 만든다.
Speed = Velocity의 XY 길이
IsInAir = CharacterMovement.IsFalling
Speed가 0에 가까우면 Idle, 값이 커지면 Walk나 Run을 재생할 수 있다. IsInAir가 참이면 Jump 상태로 전환하고, 착지하면 다시 지상 이동 상태로 돌아간다. Blend Space를 사용하면 Speed 값에 따라 Idle, Walk, Run 사이를 부드럽게 섞을 수 있다.
Event Graph에서 이동 상태 계산하기

Event Blueprint Update Animation은 Animation Blueprint가 갱신될 때마다 실행된다. 먼저 Try Get Pawn Owner로 이 Anim Instance를 사용하는 Pawn을 가져오고, Pawn의 Movement Component를 CharacterMovementComponent로 캐스팅해 Character Movement 변수에 저장한다. 이후 애니메이션 계산은 이 컴포넌트가 제공하는 실제 이동 결과를 기준으로 진행한다.
Get Last Update Velocity가 반환한 속도 벡터는 두 곳에서 사용한다. Vector Length XY는 높이 방향인 Z값을 제외한 수평 속도의 크기를 구하고, 그 결과를 Planar Speed에 저장한다. 이 값은 Locomotion State Machine의 Idle과 Walk/Run 전환 및 Blend Space의 재생 속도를 결정한다. 입력값을 직접 사용하지 않고 Movement Component의 최종 속도를 읽기 때문에 관성이나 외부 힘으로 움직이는 상황도 같은 애니메이션 규칙에 반영할 수 있다.

속도 계산 뒤에는 Sequence를 사용해 애니메이션에 필요한 상태 변수를 차례로 갱신한다.
Is Moving on Ground의 반대값을Is Jump Started에 저장해 지면에서 벗어난 상태를 판단한다.Is Moving on Ground결과를Is Ground에 저장해 지상 이동 여부를 기록한다.Is Falling결과를Is Falling변수에 저장해 낙하 상태 전환에 사용한다.- 소유 Pawn의 Forward Vector와 이동 속도 사이의 부호 있는 각도를
Get Signed Degree로 계산해Direction에 저장한다.
Direction은 단순히 캐릭터가 바라보는 각도가 아니라 전방과 실제 이동 방향 사이의 각도다. 따라서 캐릭터가 앞을 본 채 옆이나 뒤로 이동해도 그 차이를 Orientation Warping에 전달할 수 있다. Event Graph는 이처럼 Character Movement의 현재 상태를 애니메이션용 변수로 변환하고, Anim Graph와 State Machine은 계산된 값을 소비해 최종 포즈를 선택한다.
Animation Blueprint의 전체 구성

Anim Graph는 이동 애니메이션을 선택하는 부분과 최종 자세를 보정하는 부분으로 나뉜다. Locomotion State Machine에서 만든 포즈는 로컬 공간에서 컴포넌트 공간으로 변환된 뒤 Leg IK를 거쳐 다시 로컬 공간으로 돌아온다. 별도의 Movement State Machine 결과에는 Control Rig를 적용하며, Is Falling이 거짓일 때만 Should Do IKTrace가 참이 되도록 연결했다. 공중에서는 바닥을 찾을 필요가 없으므로 발 IK 추적을 끄고, 지상에서만 발이 표면에 맞도록 보정하는 구조다.
Leg IK 노드에는 왼발과 오른발의 IK 본과 FK 본을 각각 지정했다.

왼발은 ik_foot_l과 foot_l, 오른발은 ik_foot_r과 foot_r을 연결하고 각 다리를 두 개의 본으로 설정했다. 이 정보가 있어야 노드가 허벅지와 종아리의 굽힘을 계산하면서 발 위치를 보정할 수 있다.
지상 이동: Idle과 Walk/Run

Locomotion State Machine은 Idle과 Walk/Run 두 상태로 구성했다. 진입할 때는 Idle에서 시작하며, 이동 속도가 기준값보다 커지면 Walk/Run으로 전환하고 속도가 다시 낮아지면 Idle로 돌아간다.
Transition Rule로 상태 전환하기

State Machine의 상태 사이에 있는 Transition Rule은 현재 상태에서 다음 상태로 넘어갈 수 있는지를 Boolean 값으로 결정한다. 위 그래프는 Idle에서 Walk/Run으로 전환할 때 사용하는 조건이다. Event Graph에서 계산한 Planar Speed가 Walk and Run Threshold보다 크거나 같으면 비교 결과가 참이 되고, Can Enter Transition이 활성화되어 Walk/Run 상태로 진입한다.
Planar Speed >= Walk and Run Threshold
속도 기준을 Walk and Run Threshold 변수로 분리했기 때문에 Transition Graph 안에 숫자를 직접 고정하지 않고도 전환 시점을 조절할 수 있다. 반대 방향인 Walk/Run에서 Idle로 돌아가는 전환에서는 Planar Speed가 기준값보다 작아졌는지를 검사하면 된다. 두 전환이 같은 기준값을 사용하면 이동 시작과 정지 조건을 일관되게 관리할 수 있다.

Idle 상태에서는 MM_Idle 시퀀스를 그대로 출력한다. 이동하지 않는 동안 불필요한 분기 없이 대기 애니메이션 하나만 재생하는 가장 단순한 상태다.

Walk/Run 상태에서는 Planar Speed를 BS_WalkAndRun Blend Space에 전달한다. 속도에 따라 걷기와 달리기 포즈가 부드럽게 섞인다. 이후 Orientation Warping에 이동 방향인 Direction을 전달해 애니메이션이 바라보는 방향과 실제 이동 방향의 차이를 보정한다.

Orientation Warping은 spine_01부터 spine_05까지의 척추 본에 회전 보정을 분배하고, ik_foot_root, ik_foot_l, ik_foot_r을 기준으로 하체와 발의 방향을 함께 맞춘다. 회전축은 Z축이며 보간 속도를 적용해 방향이 급격히 꺾이지 않도록 했다.
점프, 낙하, 착지 상태

공중 이동을 포함한 Movement State Machine은 Locomotion, Jump, Falling, Landing 상태로 구성했다. ToFalling과 ToLand는 State Alias다. 여러 상태에서 같은 목적지로 향하는 전환을 하나로 묶어, 상태마다 동일한 전환선을 반복해서 만드는 일을 줄인다.

ToFalling은 Landing과 Locomotion을 전환 출발 상태로 선택한다. 지상에서 발이 떨어지거나 착지 동작 중 다시 낙하하는 경우 동일한 낙하 조건을 재사용할 수 있다.

ToLand는 Jump와 Falling을 출발 상태로 선택한다. 상승 중 천장에 부딪히거나 정상적으로 낙하한 뒤 바닥에 닿는 경우 모두 같은 착지 상태로 연결된다.

Movement의 Locomotion 상태는 앞에서 계산해 둔 Locomotion Cached Pose를 재사용한다. Idle과 Walk/Run 계산을 다시 만들지 않고 지상 이동 결과를 그대로 가져오는 방식이다.

Jump 상태에서는 MM_Jump 시퀀스를 재생해 점프가 시작되는 상승 동작을 표현한다. 상승이 끝나고 수직 속도가 낙하 방향으로 바뀌면 Falling 상태로 넘어간다.

Falling 상태는 MM_Fall_Loop을 반복 재생한다. 공중에 머무는 시간이 달라져도 루프 애니메이션이 유지되며, Character Movement가 지면 접촉을 감지하면 Landing으로 전환한다.

Landing에서는 Cached Locomotion 포즈를 Base로 사용하고 MM_Land를 Additive 포즈로 더한다. 착지 충격은 표현하면서도 현재 이동 포즈를 유지하므로, 착지 직후 걷거나 달릴 때 동작이 갑자기 끊기는 현상을 줄일 수 있다.
이 방식의 장점은 입력 코드와 애니메이션 코드가 직접 의존하지 않는다는 점이다. 키보드 입력, 게임패드 입력, 밀려나는 물리 효과처럼 이동 원인이 달라도 최종 속도가 같으면 Animation Blueprint가 같은 이동 애니메이션을 선택한다.
템플릿 캐릭터와 현재 캐릭터의 차이
프로젝트에는 Unreal Third Person 템플릿의 ATP_ThirdPersonCharacter도 함께 들어 있다. 템플릿은 Controller의 Yaw만 사용해 카메라 기준 이동 방향을 만든다.
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector ForwardDirection =
FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection =
FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
반면 ATpsPlayer는 Actor의 Forward와 Right를 바로 사용한다. 어느 쪽이 맞다기보다 원하는 조작 방식의 차이다. 일반적인 3인칭 게임처럼 카메라가 보는 방향을 기준으로 움직이려면 템플릿 방식이 더 자연스럽고, 캐릭터 자체의 전방을 기준으로 조작하려면 현재 방식이 단순하다.
구현 결과
결과 영상에서는 입력이 캐릭터 이동과 애니메이션으로 이어지는 전체 흐름을 확인할 수 있다. 캐릭터가 멈추면 Idle 상태를 유지하고, 이동을 시작하면 Planar Speed에 따라 Walk/Run Blend Space가 재생된다. 이동 방향이 바뀔 때는 Orientation Warping이 상체와 하체의 방향 차이를 보정해 방향 전환이 자연스럽게 이어진다.
점프 입력 후에는 Jump, Falling, Landing 상태가 차례로 전환된다. 상승이 끝난 뒤에는 낙하 루프가 유지되고, 지면에 닿으면 Additive 착지 동작을 거쳐 다시 Locomotion으로 복귀한다. 지상에서는 Leg IK와 Control Rig가 발 위치를 바닥에 맞추고, 공중에서는 IK 추적을 비활성화해 점프 애니메이션을 방해하지 않도록 했다.
이 결과를 통해 C++이 개별 애니메이션을 직접 재생하지 않아도, Character Movement가 만든 속도와 공중 상태만으로 Animation Blueprint가 현재 상황에 맞는 포즈를 선택할 수 있음을 확인했다.
정리
입력 처리에는 세 단계가 있다. Player Controller가 Mapping Context와 Input Action을 보관하고, Enhanced Input Subsystem이 키 설정을 활성화하며, Character가 액션을 실제 동작에 연결한다.
애니메이션은 네 번째 단계에서 간접적으로 이어진다. C++이 애니메이션을 명령하는 대신 Character Movement가 만든 결과를 Animation Blueprint가 관찰한다. 입력과 이동, 애니메이션 표현을 분리했기 때문에 각 부분을 독립적으로 수정하기 쉬운 구조다.
마지막으로 이 프로젝트에서는 Orientation Warping을 이용해 달리기 애니메이션 하나만으로 후방을 제외한 7개 방향의 이동 애니메이션을 자연스럽게 만들 수 있었다. 방향마다 별도의 달리기 클립을 준비하지 않아도 전방, 좌우, 대각선 방향에 맞춰 하체와 진행 방향을 보정할 수 있어 필요한 애니메이션 에셋 수를 줄이면서도 다방향 이동을 자연스럽게 표현했다.