과제 소개
- Pawn 클래스 구조 이해
- 언리얼 엔진에서 Pawn은 PlayerController가 조종할 수 있는 최소 단위입니다.
- CharacterMovementComponent 없이 직접 이동 로직을 구현해봅시다.
- Enhanced Input & 3인칭 카메라
- Enhanced Input 액션을 생성하여 키보드와 마우스 입력을 처리합니다.
- SpringArmComponent 및 CameraComponent를 사용해 3인칭 시점을 구현하며, 마우스 움직임으로 카메라를 회전시킵니다.
- 직접 이동 로직 구현
- AddActorLocalOffset, AddActorLocalRotation 등을 활용하여 WASD와 마우스 입력에 따라 Pawn을 움직이도록 만듭니다.
필수 과제 (기본 요구 사항)
필수 과제 1번 - C++ Pawn 클래스와 충돌 컴포넌트 구성
- Pawn 클래스 생성
- 충돌 컴포넌트를 루트 컴포넌트로 설정합니다 (CapsuleComponent/BoxComponent/SphereComponent 중 택 1).
- SkeletalMeshComponent, SpringArmComponent, CameraComponent를 부착하여 3인칭 시점을 구성합니다.
- GameMode에서 DefaultPawnClass를 이 Pawn 클래스로 지정합니다.
- Physics 설정
- 루트 충돌 컴포넌트 및 SkeletalMeshComponent 모두 Simulate Physics = false로 설정합니다.
- 물리 시뮬레이션이 아닌 코드로 직접 제어합니다.
필수 과제 2번 - Enhanced Input 매핑 & 바인딩
- Input 액션 생성
- Move (WASD용 - Vector2D 타입)
- Look (마우스 이동용 - Vector2D 타입)
- Input Mapping Context (IMC)에 액션들을 매핑합니다.
- 입력 바인딩 및 이동/회전 로직 구현
- SetupPlayerInputComponent()에서 각 액션에 함수를 바인딩합니다.
- AddActorLocalOffset(), AddActorLocalRotation() 등을 사용하여 이동과 회전을 구현합니다.
- 이동 방향은 Pawn의 Forward/Right 벡터에 따라 결정됩니다.
- 마우스 입력으로 Yaw와 Pitch를 직접 계산하여 회전을 구현합니다.
- AddControllerYawInput() 또는 AddControllerPitchInput() 같은 기본 제공 함수를 사용하지 않습니다.
- 평면 상에서의 이동 및 회전만 처리합니다 (중력/낙하 효과 없음).
이전 과제까진 과제를 완료 한 후 글을 작성했지만 이번 과제부터는 필수과제 1번부터 천천히 하나씩 해결해나가며 발생한 문제점과 해결과정을 작성할 예정이다.
Pawn 클래스 생성
Pawn 클래스 생성 하고 충돌 컴포넌트를 루트 컴포넌트로 설정
// .h
UPROPERTY(VisibleAnywhere, Category = "Character")
USceneComponent* CapsuleCollision;
캡슐 크기를 에디터에서 수정할 수 있게 UPROPERTY 지정자를 붙였다.
// .cpp
CapsuleCollision = CreateDefaultSubobject<UCapsuleComponent>(TEXT("RootCollision"));
SetRootComponent(CapsuleCollision);
SkeletalMeshComponent, SpringArmComponent, CameraComponent를 부착하여 3인칭 시점을 구성
// .h
UPROPERTY(VisibleAnywhere, Category = "Character")
USkeletalMeshComponent* SkeletalMeshComp;
UPROPERTY(VisibleAnywhere, Category = "Character")
USpringArmComponent* SpringArmComp;
UPROPERTY(VisibleAnywhere, Category = "Character")
UCameraComponent* CameraComp;
스켈레탈 메시 에셋을 에디터에서 설정하도록 UPROPERY 지정자를 붙였다.
스프링암과 카메라도 설정을 바꿀일이 있으면 에디터에서 수정하기 위해 UPROPERTY 지정자를 붙였다.
SkeletalMeshComp = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("SkeletalMesh"));
SkeletalMeshComp->SetupAttachment(CapsuleCollision);
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArmComp->SetupAttachment(CapsuleCollision);
SpringArmComp->TargetArmLength = 300.0f;
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
스켈레탈 메시 컴포넌트와 스프링암 컴포넌트를 캡슐컴포넌트에 붙이고 카메라 컴포넌트를 스프링암의 소켓 SocketName(스프링암의 끝부분)에 붙였다.

에디터에서 스프링암의 높이와 각도를 조절하고 캡슐 컴포넌트의 크기를 스켈레탈 메시에 맞게 조정했다.
GameMode에서 DefaultPawnClass를 이 Pawn 클래스로 지정

게임모드 클래스를 C++로 만들어서 블루프린트로 파생시킨 후 프로젝트 세팅에서 Default GameMode를 수정했다.
// 게임모드.cpp
DefaultPawnClass = AMovablePawn::StaticClass();
게임모드 생성자에서 DefaultPawnClass를 MovablePawn으로 할당했다.

게임모드 블루프린트에서 DefalutPawnClass를 위에서 만든 BP_MovablePawn으로 할당했다.
Physics 설정
루트 충돌 컴포넌트 및 SkeletalMeshComponent 모두 Simulate Physics = false로 설정합니다.
// .cpp
CapsuleCollision->SetSimulatePhysics(false);
SkeletalMeshComp->SetSimulatePhysics(false);
이 함수는 강의에서 안나와서 처음써보는데 SetPhysics를 치니 이 함수가 떠서 사용했다.
Input 액션 생성
이 부분은 강의에서도 언리얼 에디터에서 생성했으므로 언리얼 에디터에서 진행한다.

둘다 ValueType을 Axis2D로 했다.
Input Mapping Context (IMC)에 액션들을 매핑

IA_Look에는 마우스의 XY값을 주고 Y축만 반대 값을 줬다.

W는 수정 없음(1,0), S는 반대 값(-1,0), D는 축 바꿈(0,1), A는 축 바꿈과 반대 값(0,-1)을 줬다.
여기까지 하고보니 과제 내용에 없어서 넘겼는데 플레이어 컨트롤러가 입력 장치의 입력을 받아서 폰에게 명령을 내린다는걸 배웠는데 컨트롤러를 만들지 않았다는걸 깨달았다. 플레이어 컨트롤러를 만들어서 IMC를 활성화 해줘야한다.
플레이어 컨트롤러
// 게임모드.cpp
PlayerControllerClass = AHW7PlayerController::StaticClass();
플레이어 컨트롤러 클래스를 만들고 게임모드 생성자에서 PlayerControllerClass로 지정했다.

게임모드를 블루프린트로 파생시킨 뒤 게임모드의 컨트롤러로 할당하고 테스트로 실행해보니 생성이 잘 됐다.
이제 플레이어에서 IMC를 활성화 해야한다.
// 플레이어 컨트롤러.h
public:
AHW7PlayerController();
UPROPERTY(EditDefaultsOnly, Category = "Input")
UInputMappingContext* IMC_Main;
UPROPERTY(EditDefaultsOnly, Category = "Input")
UInputAction* Move;
UPROPERTY(EditDefaultsOnly, Category = "Input")
UInputAction* Look;

강의에서는 생성자에서 IMC와 IA에 nullptr로 초기화한 후 에디터에서 만들어둔 IMC와 IA를 넣어줬었는데 만약 초기화 하지 않으면 어떻게 될지 궁금해서 해봤는데 에디터에서 똑같이 비어있었다.
AHW7PlayerController::AHW7PlayerController()
{
IMC_Main = nullptr;
Move = nullptr;
Look = nullptr;
}

테스트를 해보고 혹시 모르니 생성자에서 초기화를 하고 만들어둔 IMC와 IA를 할당했다.
void AHW7PlayerController::BeginPlay()
{
Super::BeginPlay();
if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
if (IMC_Main)
{
Subsystem->AddMappingContext(IMC_Main, 0);
}
}
}
}
이 코드는 너무 길고 이해도 잘 안돼서 정리한 글을 참고했다.
입력 바인딩 및 이동/회전 로직 구현
SetupPlayerInputComponent()에서 각 액션에 함수를 바인딩
// MovablePawn.cpp
void AMovablePawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
if (AHW7PlayerController* PlayerController = Cast<AHW7PlayerController>(GetController()))
{
if (PlayerController->Move)
{
EnhancedInput->BindAction(PlayerController->Move, ETriggerEvent::Triggered, this, &AMovablePawn::Move);
}
if (PlayerController->Look)
{
EnhancedInput->BindAction(PlayerController->Look, ETriggerEvent::Triggered, this, &AMovablePawn::Look);
}
}
}
}
EnhancedInput 변수에 InputComponent로 캐스팅해서 성공하면 InputComponent의 객체 포인터를 담고
PlayerController 변수에 HW7PlayerController로 캐스팅 해서 성공하면 HW7PlayerController의 객체 포인터를 담고
PlayerController의 Move변수(현재는 IA_Move)가 유효하면 HW7PlayerController의 Move변수에 담겨있는 인풋 액션(IA_Move)의 키를 누를때(ETriggerEvent::Triggered) MovablePawn(this)을 Move함수(이동하는 함수)를 호출한다.
AddActorLocalOffset(), AddActorLocalRotation() 등을 사용하여 이동과 회전을 구현
void AMovablePawn::Move(const FInputActionValue& value)
{
if (!Controller) return;
AddActorLocalOffset({ value.Get<FVector2D>().X * 8,value.Get<FVector2D>().Y * 8, 0 });
}
Move는 value(IA_Move의 입력값)의 X, Y값을 각 AddActorLocalOffset에 FVector 인자로 넣어서 움직임을 구현했다.
이동 속도가 너무 느려서 8을 곱했다.
void AMovablePawn::Look(const FInputActionValue& value)
{
AddActorLocalRotation({ value.Get<FVector2D>().Y, value.Get<FVector2D>().X, 0 });
}
AddActorLocalRotation은 Frotator를 인자로 받는데 Frotator는 Pitch(X축 회전), Yaw(Z축 회전), Roll(Y축 회전) 값을 가지고 있다.
마우스가 위아래로 움직일 때 시점이 위아래로 바뀌도록 Pitch에 value의 Y값을, 좌우로 움직일 때 시점이 좌우로 움직이도록 Yaw에 value의 X값을 넣었다.
그랬더니 문제가 발생했는데 캐릭터가 모든 방향으로 회전하며 캐릭터의 앞 방향으로 가게 되니 게임에서 죽었을 때 자유시점이 되듯이 막 날아다녔다.
그래서 캐릭터의 상하 회전을 하지 않도록 Pitch회전을 막았다.
void AMovablePawn::Look(const FInputActionValue& value)
{
AddActorLocalRotation({ 0, value.Get<FVector2D>().X, 0 });
}
캐릭터의 상하 회전을 막았더니 카메라까지 상하로 회전이 되지 않았다.
카메라는 스프링암의 끝에 붙어있으니 value의 y값을 이용해서 스프링의 Pitch를 회전시키면 될 것 같다는 생각이 들었다.
void AMovablePawn::Look(const FInputActionValue& value)
{
AddActorLocalRotation({ 0, value.Get<FVector2D>().X, 0 });
SpringArmComp->SetRelativeRotation({ value.Get<FVector2D>().Y, 0, 0 });
}
이제 카메라가 회전은 잘 되지만 180도가 넘어가서 회전을 해버려서 화면이 상하반전이 되는 경우가 발생했다.
void AMovablePawn::Look(const FInputActionValue& value)
{
AddActorLocalRotation({ 0, value.Get<FVector2D>().X, 0 });
float CurrentPitch = SpringArmComp->GetRelativeRotation().Pitch + value.Get<FVector2D>().Y;
CurrentPitch = FMath::Clamp(CurrentPitch, -70, 50);
SpringArmComp->SetRelativeRotation({CurrentPitch , 0, 0 });
}
FMath::Clamp를 이용하면 범위를 정할 수 있다하여 클램프 함수로 범위를 지정해줬다.
SpringArmComp의 현재 Pitch 값에 마우스로 입력 받은 Y값을 더해서 범위안에서 회전하도록 Set 했다.
자연스러운 카메라 회전과 캐릭터 이동이 완료가 됐다.
'개인 프로젝트(과제)' 카테고리의 다른 글
| 8번 과제(게임 루프 및 UI 재설계하기) 필수 과제 (0) | 2025.10.01 |
|---|---|
| 7번 과제(Pawn 클래스로 3D 캐릭터 만들기) 도전 과제 (0) | 2025.09.22 |
| 6번 과제(회전 발판과 움직이는 장애물 퍼즐 스테이지) (1) | 2025.09.17 |
| 4번 과제 복습(연금술 공방 관리 시스템 구현) (0) | 2025.09.02 |
| 3번 과제 복습(인벤토리 시스템 구현) (1) | 2025.09.01 |