오늘은 언리얼 C++ 두번째 과제로 Pawn 클래스로 3D 캐릭터 구현과
Tick 기반으로 이동/회전을 처리하였고 Enhanced Input, AnimBlueprint를 활용하여 캐릭터 동작을 구현했다.
Pawn 클래스 생성 및 컴포넌트 구성
CapsuleComponent를 Root로 설정
SpringArm + Camera 구성 (3인칭 시점)
Enhanced Input 기반 이동/회전 구현
Tick + DeltaTime을 활용한 프레임 이동
AddActorLocalOffset을 이용한 이동 처리
AddActorLocalRotation을 이용한 Yaw 회전 구현
SpringArm Pitch 회전 (상하 시점 구현)
AnimBlueprint 연동
GroundSpeed 계산 및 ShouldMove 상태 전환
// MyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "MyCharacter.generated.h"
// 컴포넌트 전방 선언(.h에서 불필요한 인클루드 방지)
class USpringArmComponent;
class UCameraComponent;
class UCapsuleComponent;
// Enhanced Input에 사용하는 입력값 구조체 전방 선언
struct FInputActionValue;
UCLASS()
class UNREALCPPPROJECT07_API AMyCharacter : public APawn
{
GENERATED_BODY()
public:
AMyCharacter();
protected:
// 캡슐 컴포넌트 생성
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Root")
UCapsuleComponent* Capsulecomp;
// 스프링 암 컴포넌트 생성
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
USpringArmComponent* Springcomp;
// 카메라 컴포넌트 생성
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
UCameraComponent* Cameracomp;
virtual void Tick(float DeltaTime) override;
// 입력 바인딩 함수 (Move, Look 연결)
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// 이동 입력 함수
UFUNCTION()
void Move(const FInputActionValue& value);
// 마우스 입력 함수
UFUNCTION()
void Look(const FInputActionValue& value);
// 이동 속도
float MoveSpeed;
// 이동 입력값
FVector2D MoveInput;
// 회전 입력값
FVector2D LookInput;
// 좌, 우 회전 속도
float TurnSpeed;
// 상, 하 회전 속도
float LookSpeed;
};
// MyCharacter.cpp
#include "MyCharacter.h"
#include "MyPlayerController.h"
#include "EnhancedInputComponent.h"
#include "Components/CapsuleComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
AMyCharacter::AMyCharacter()
{
PrimaryActorTick.bCanEverTick = true;
// 캡슐 컴포넌트 생성 후 루트 컴포넌트로 설정
Capsulecomp = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
SetRootComponent(Capsulecomp);
// 물리 비활성화(물리 대신 코드로 직접 제어)
Capsulecomp->SetSimulatePhysics(false);
// 스프링 암 컴포넌트 생성 후 루트 컴포넌트에 부착
Springcomp = CreateDefaultSubobject<USpringArmComponent>(TEXT("Spring Arm"));
Springcomp->SetupAttachment(RootComponent);
// 스프링 암 거리 설정
Springcomp->TargetArmLength = 300.0f;
// Pawn의 회전을 자동으로 따라가지 않도록 설정
Springcomp->bUsePawnControlRotation = false;
// 카메라 컴포넌트 생성 후 스프링 암 컴포넌트에 부착
Cameracomp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Cameracomp->SetupAttachment(Springcomp);
// Pawn의 회전을 자동으로 따라가지 않도록 설정
Cameracomp->bUsePawnControlRotation = false;
// 이동 초기 입력값 0으로 설정
MoveInput = FVector2D::ZeroVector;
// 기본 속도 설정
MoveSpeed = 500.0f;
TurnSpeed = 100.0f;
LookSpeed = 100.0f;
}
void AMyCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 입력값 기반으로 이동하는 벡터 생성(X : 앞, 뒤 / Y : 좌, 우)
FVector Move = FVector(MoveInput.X, MoveInput.Y, 0.0f);
// 이동 입력값이 0이 아닐때만 실행(입력이 있을때만 실행)
if (!Move.IsNearlyZero())
{ // 현재 위치 기준에서 이동
// 이동 입력값 * 기본 속도 * DeltaTime
AddActorLocalOffset(Move * MoveSpeed * DeltaTime, true);
}
// 회전 입력값이 0이 아닐때만 실행(입력이 있을때만 실행)
if (!FMath::IsNearlyZero(LookInput.X))
{ // 좌우 회전 = 마우스 입력값 * 회전 속도 * DeltaTime
float Yawvalue = LookInput.X * TurnSpeed * DeltaTime;
// 현재 위치 기준에서 캐릭터 자체 회전
AddActorLocalRotation(FRotator(0.0f, Yawvalue, 0.0f));
}
// 회전 입력값이 0이 아닐때만 실행(입력이 있을때만 실행)
if (!FMath::IsNearlyZero(LookInput.Y))
{ // 상하 회전 = 마우스 입력값 * 회전 속도 * DeltaTime
float Pitchvalue = LookInput.Y * LookSpeed * DeltaTime;
// 현재 스프링암 회전값 가져오기
FRotator SpringArmRotation = Springcomp->GetRelativeRotation();
// Ptich 값 제한 (캐릭터 누움, 기울어짐 방지 위, 아래 각도 제한)
SpringArmRotation.Pitch = FMath::Clamp(
SpringArmRotation.Pitch - Pitchvalue,
-80.0f,
80.0f
);
// 위에 계산한 회전값 스프링암에 적용
// Pitch를 캐릭터에 적용할 시 방향이 아니라 캐릭터 자체를 회전시켜서
// 시점만 위 아래로 움직이도록 스프링 암으로 적용
Springcomp->SetRelativeRotation(SpringArmRotation);
}
// 프레임 처리 후 입력값 초기화(입력 끊겼는데 계속 움직임 방지)
MoveInput = FVector2D::ZeroVector;
LookInput = FVector2D::ZeroVector;
}
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 바인딩을 하기 위해 Eenhanced Input Component 캐스팅
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{ // 현재 컨트롤러 가져오기
if (AMyPlayerController* PlayerController = Cast<AMyPlayerController>(GetController()))
{ // 이동 입력 바인딩
if (PlayerController->MoveAction)
{
EnhancedInput->BindAction(
PlayerController->MoveAction,
ETriggerEvent::Triggered,
this,
&AMyCharacter::Move
);
}
// 회전 입력 바인딩
if (PlayerController->LookAction)
{
EnhancedInput->BindAction(
PlayerController->LookAction,
ETriggerEvent::Triggered,
this,
&AMyCharacter::Look
);
}
}
}
}
// 이동 입력 함수
void AMyCharacter::Move(const FInputActionValue & value)
{
// FVector2D로 이동 입력값 저장
MoveInput = value.Get<FVector2D>();
}
void AMyCharacter::Look(const FInputActionValue& value)
{
// FVector2D로 회전 입력값 저장
LookInput = value.Get<FVector2D>();
}
// MyPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MyPlayerController.generated.h"
// Enhanced Input에서 사용하는 클래스 전방 선언
class UInputMappingContext;
class UInputAction;
/**
*
*/
UCLASS()
class UNREALCPPPROJECT07_API AMyPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AMyPlayerController();
// IMC 변수 생성
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Input")
UInputMappingContext* InputMC;
// 이동 입력 변수 생성
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Input")
UInputAction* MoveAction;
// 회전 입력 변수 생성
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Input")
UInputAction* LookAction;
virtual void BeginPlay() override;
};
// MyPlayerController.cpp
#include "MyPlayerController.h"
#include "EnhancedInputSubsystems.h"
AMyPlayerController::AMyPlayerController()
// 초기값 nullptr로 설정
// 에디터에서 값이 할당되지 않아서
// 이상한 값 들어가는걸 방지
:InputMC(nullptr),
MoveAction(nullptr),
LookAction(nullptr)
{
}
void AMyPlayerController::BeginPlay()
{
Super::BeginPlay();
// 현재 컨트롤러에 연결된 플레이어 가져오기
if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
{ // Enhanced Input 시스템을 관리하는 Subsystem 가져오기
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{ // IMC 등록이 되어있으면 실행
if (InputMC)
{ // IMC를 시스템에 추가
Subsystem->AddMappingContext(InputMC, 0);
}
}
}
}
// MyGameMode.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "MyGameMode.generated.h"
/**
*
*/
UCLASS()
class UNREALCPPPROJECT07_API AMyGameMode : public AGameMode
{
GENERATED_BODY()
public:
AMyGameMode();
};
// MyGameMode.cpp
#include "MyGameMode.h"
#include "MyCharacter.h"
#include "MyPlayerController.h"
AMyGameMode::AMyGameMode()
{
// 게임 시작시 기본 Pawn 설정
DefaultPawnClass = AMyCharacter::StaticClass();
// 게임 시작시 입력을 처리할 컨트롤러 설정
PlayerControllerClass = AMyPlayerController::StaticClass();
}
Try Get Pawn Owner
- 현재 Pawn 가져오기
Cast To MyCharacter
- Pawn - > MyCharacter로 캐스팅
SET My Pawn
- 캐스팅 된 캐릭터를 변수에 저장
Get Actor Location
- 현재 캐릭터 위치 가져오기
Prev Location(속도 계산용)
- 이전 위치 저장
게임 시작시 > Try Get Pawn Owner로 Pawn을 가져와서 Cast To MyCharacter로 캐스팅 >
캐스팅 된 캐릭터를 SET My Pawn에 저장 및 Get Actor Location으로 현재 캐릭터 위치 가져오기 >
가져온 위치 Prev Location에 저장

Event Blueprint Update Animation
- 매 프레임마다 실행
My Pawn Is Valid
- Pawn이 있는지 확인
Sequence
- 여러 로직을 순서대로 실행 (Then 0, 1, 2)
Get Actor Location
- 현재 캐릭터 위치 가져오기
Prev Location
- 이전 캐릭터 위치 값
Vector Length XY
- X, Y 기준 이동 거리를 속도로 변환 (Z 제외)
SET Ground Speed
- 계산된 이동 속도를 저장
SET Should Move
- 이동 중인지 여부 (true / false)
SET Prev Location
- 현재 위치를 다시 저장 (다음 프레임 계산용)
Prev Location로 이전 위치와 Get Actor Location로 현재 위치를 가져와서 >
Subtract (Vector - Vector)로 (현재 위치 - 이전 위치) 계산 >
계산한 값과 Delta Time X가 연결되어 있지만 속도 계산이 아닌 값 보정 용도로 사용 >
보정한 값을 Vector Length XY로 X, Y 기준 이동 거리 계산 > SET Ground Speed로 이동 속도 저장
Ground Speed 값이 3보다 크면 > SET Should Move에 저장
Get Actor Location로 현재 위치를 가져와서 > SET Prev Location에 저장 > 다음 프레임에서 이전 위치로 사용

New State Machine 생성 후 Output Pose에 연결

New State Machine에서 Idle, Walk State 생성 >
Entry -> Idle <-> Walk 연결

Idle State에서 Idle 애니메이션 연결

Walk에서 Walk 애니메이션 연결

Idle -> Walk 조건 Should Move 연결
Should Move == true 일 때 실행

Walk -> Idle NOT Should Move 연결
Should Move == false 일 때 실행

이동키를 입력했을때 캐릭터가 바라보는 방향 기준으로 이동하게 만들기 위해 아래처럼 코드를 구현하였다.
// MyCharacter.cpp
FVector Move =
(GetActorForwardVector() * MoveInput.X) +
(GetActorRightVector() * MoveInput.Y);
if (!Move.IsNearlyZero())
{
AddActorLocalOffset(Move * MoveSpeed * DeltaTime, true);
}
이렇게 구현하고 게임 시작을 해서 테스트를 해보니,
캐릭터를 회전한 뒤 이동 키를 누르면 해당 방향이 아닌 다른 방향으로 이동 되는 문제가 발생하였다.
원인을 확인해보니 방향 벡터를 직접 계산하는 방식과 로컬 기준 이동 방식이 현재 구현중인 캐릭터 이동의 구조에 맞지 않아서 발생한 것 같아서 아래와 같이 코드를 수정하였다.
// MyCharacter.cpp
FVector Move = FVector(MoveInput.X, MoveInput.Y, 0.0f);
if (!Move.IsNearlyZero())
{
AddActorLocalOffset(Move * MoveSpeed * DeltaTime, true);
}
이렇게 수정하니 방향을 바꾸고 이동을 해도 정상적으로 이동이 되어서 해결하였다.
이동 키를 한 번 눌렀다가 금방 뗐는데 캐릭터가 계속 이동하는 문제가 발생하였다.
처음에는 입력이 계속 들어오는 줄 알았지만, 확인해보니 입력값이 초기화되지 않는 것이 원인이었다.
Tick 함수에서 입력값을 계속 사용하고 있었기 때문에 이전 프레임의 입력값이 그대로 유지되면서
이동이 멈추지 않는 현상이 발생하였다.
그래서 Tick 마지막에 입력값을 초기화하도록 수정하였다.
// MyCharacter.cpp
void AMyCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
FVector Move = FVector(MoveInput.X, MoveInput.Y, 0.0f);
if (!Move.IsNearlyZero())
{
AddActorLocalOffset(Move * MoveSpeed * DeltaTime, true);
}
// =====오류 발견 후에 추가=====
MoveInput = FVector2D::ZeroVector;
}
이렇게 수정하니 입력이 없을 때 이동이 멈추며 정상적으로 동작 되어서 해결 하였다
처음에 회전값을 아래 코드처럼 X축과 Y축 둘 다 똑같이 구현을 했다.
// MyCharacter.cpp
if (!FMath::IsNearlyZero(LookInput.X))
{
float Yawvalue = LookInput.X * TurnSpeed * DeltaTime;
AddActorLocalRotation(FRotator(0.0f, Yawvalue, 0.0f));
}
if (!FMath::IsNearlyZero(LookInput.Y))
{
float Pitchvalue = LookInput.Y * TurnSpeed * DeltaTime;
AddActorLocalRotation(FRotator(Pitchvalue, 0.0f, 0.0f));
}
게임 시작 후 테스트를 해보니 이렇게 하면 캐릭터가 방향에 따라 기울어지면서 움직이는 문제가 발생하였다.
원인을 확인해보니 AddActorLocalRotation(FRotator(Pitchvalue, 0.0f, 0.0f))는 Pawn 자체를 Pitch 기준으로 회전 시키는 방식이라서, 캐릭터 자체가 기울어지면서 움직이는 문제가 발생한것이었다.
그래서 카메라의 부모인 Spring Arm에 적용 하는 방식으로 아래 코드처럼 수정을 하였다.
void AMyCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!FMath::IsNearlyZero(LookInput.X))
{
float Yawvalue = LookInput.X * TurnSpeed * DeltaTime;
AddActorLocalRotation(FRotator(0.0f, Yawvalue, 0.0f));
}
if (!FMath::IsNearlyZero(LookInput.Y))
{
float Pitchvalue = LookInput.Y * LookSpeed * DeltaTime;
FRotator SpringArmRotation = Springcomp->GetRelativeRotation();
SpringArmRotation.Pitch = FMath::Clamp(
SpringArmRotation.Pitch - Pitchvalue,
-80.0f,
80.0f
);
Springcomp->SetRelativeRotation(SpringArmRotation);
}
LookInput = FVector2D::ZeroVector;
}
코드를 수정하니 내가 의도한대로 좌우 회전은 캐릭터 방향이 움직이고, 상하 회전은 카메라 시점만 움직이게 되었고,
더이상 캐릭터가 기울어지지 않고 자연스럽게 위아래 시점도 조절할 수 있게 작동되었다.
FMath::Clamp를 사용하여 캐릭터가 기울어지거나 눕는걸 방지하기 위해 Pitch 값을 -80도 ~ 80도 범위로 제한 하였고,
계속 이동 되는 문제 해결한것과 마찬가지로 마지막에 입력값을 초기화 하도록 해주었다.
AnimBlueprint에서 GroundSpeed를 확인해보니 계속 0으로 나오는 문제가 발생하였다.
처음에는 Character처럼 Get Velocity를 사용했지만, Pawn에서는 정상적으로 값이 나오지 않았다.
그래서 위치를 이용하여 직접 속도를 계산하도록 수정하였다.

현재 위치 - 이전 위치 > 계산한 값과 Delta Time X가 연결되어 있지만 속도 계산이 아닌 값 보정 용도로 사용 >
보정한 값을 Vector Length XY로 X, Y 기준 이동 거리 계산 > SET Ground Speed로 이동 속도 저장을 해주었고,

Ground Speed의 값이 3 이상이면(이동 키를 눌렀을때) SET Should Move에 저장 후,
AnimBlueprint에서 Idle -> Walk에는 ShouldMove를 연결,
Walk -> Idle에는 NOT ShouldMove를 연결해주어서 문제를 해결하였다.
Character 클래서에서는 자동으로 처리되던 기능들을 Pawn 클래스에서 직접 이동, 회전 계산을 하면서 구현해보았다.
Tick과 DeltaTime을 활용하여 프레임에 관계없이 이동과 회전을 처리하고, 로컬 기준 이동 방식과 캐릭터 회전(Yaw), 카메라 회전(Pitch)을 분리해서 구현하면서 각 기능을 직접 구성해보는 경험을 할 수 있었다.
특히 위치 기반으로 속도를 직접 계산하여 AnimBlueprint까지 연결하는 과정에서 차라리 비행 같은 단순 이동이 더 쉬울 것 같다는 생각이 들 정도로 구현 과정이 쉽지 않았다.
특히 캐릭터 이동 방향이 이상해지는거랑 Pitch 회전시 캐릭터가 기울어지는 문제가 많이 힘들었는데 어찌저찌 해결을 구조에 대한 이해가 많이 늘었다.
1. 이동 방향이 어긋나는 문제
- 로컬 기준 이동과 방향 벡터 계산 방식의 차이
2. 입력값이 초기화되지 않아서 계속 이동되는 문제
- Tick에서 입력 처리 순서의 중요성
3. Ptich를 캐릭터에 직접 적용하고 캐릭터 자체가 기울어지는 문제
- 카메라 회전 시점과 캐릭터 회전을 분리해서 구현
4. Pawn 클래스 캐릭터는 애니메이션 전환에 필요한 값도 직접 생성
이 과정을 겪으며 단순하게 기능을 구현이 아닌 구조와 원리를 잘 이해를 해야한다고 느껴졌고,
어느정도 언리얼 C++을 이해하는데 많은 도움이 되었다.
| Unreal C++ 기초 7. InterFace 기반 아이템 클래스 기능 구현 (0) | 2026.04.16 |
|---|---|
| Unreal C++ 기초 6. InterFace 기반 아이템 클래스 생성 (0) | 2026.04.15 |
| Unreal C++ 기초 5. GameMode와 Character 클래스를 활용해서 캐릭터 구현하기 (2) | 2026.04.09 |
| 리플렉션을 활용한 회전발판과 움직이는 장애물 만들기 (1) | 2026.04.08 |
| Unreal C++ 기초 4. 로그, 리플렉션, 함수, 리플렉션 (0) | 2026.04.07 |