C++와 Unreal Engine으로 3D 게임 개발 과제 7

1. WHY
- Pawn 클래스 구조 이해
- 언리얼 엔진에서 Pawn은 PlayerController가 조종할 수 있는 최소 단위입니다.
- CharacterMovementComponent 없이 직접 이동 로직을 구현해봅시다.
- Enhanced Input & 3인칭 카메라
- Enhanced Input 액션을 생성하여 키보드와 마우스 입력을 처리합니다.
- SpringArmComponent 및 CameraComponent를 사용해 3인칭 시점을 구현하며, 마우스 움직임으로 카메라를 회전시킵니다.
- 직접 이동 로직 구현
- AddActorLocalOffset, AddActorLocalRotation 등을 활용하여 WASD와 마우스 입력에 따라 Pawn을 움직이도록 만듭니다.
2. HOW
- 1단계: C++ Pawn 클래스 생성

실수로 만든 캐릭터 컴포넌트를 뜯어보며 Pawn 클래스로 구현을 했다.
// MyPawn.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "MyPawn.generated.h"
class UCapsuleComponent;
class UArrowComponent;
class USpringArmComponent;
class UCameraComponent;
UCLASS()
class NBC_UE_P7_API AMyPawn : public APawn
{
GENERATED_BODY()
public:
// Sets default values for this pawn's properties
AMyPawn();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Comp")
UCapsuleComponent* CapsuleComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Comp")
UArrowComponent* ArrowComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Comp")
USkeletalMeshComponent* SkeletalMeshComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
USpringArmComponent* SpringArmComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
UCameraComponent* CameraComp;
public:
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};
// MyPawn.cpp
#include "MyPawn.h"
#include "Components/CapsuleComponent.h"
#include "Components/ArrowComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
// Sets default values
AMyPawn::AMyPawn()
{
// 캡슐 컴포넌트 루트로 설정
CapsuleComp = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleRoot"));
SetRootComponent(CapsuleComp);
if (CapsuleComp->IsSimulatingPhysics())
{
CapsuleComp->SetSimulatePhysics(false);
}
// arrow 컴포넌트
ArrowComp = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
ArrowComp->SetupAttachment(CapsuleComp);
// 스켈레톤 매쉬 컴포넌트 추가
SkeletalMeshComp = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("SkeletalMesh"));
SkeletalMeshComp->SetupAttachment(CapsuleComp);
if (SkeletalMeshComp->IsSimulatingPhysics())
{
SkeletalMeshComp->SetSimulatePhysics(false);
}
// 스프링 암
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArmComp->SetupAttachment(CapsuleComp);
SpringArmComp->TargetArmLength = 300.0f;
SpringArmComp->bUsePawnControlRotation = true;
// 카메라
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
CameraComp->bUsePawnControlRotation = false;
}
// Called to bind functionality to input
void AMyPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}

블루프린트에서 확인하면 잘 생성되었다.

메쉬 설정, 캡슐 크기, 카메라 위치는 블루프린트에서 조절해줬다.
// MyGameMode.cpp
#include "MyGameMode.h"
#include "MyPawn.h"
#include "MyPlayerController.h"
AMyGameMode::AMyGameMode()
{
DefaultPawnClass = AMyPawn::StaticClass();
PlayerControllerClass = AMyPlayerController::StaticClass();
}

MyPawn을 DefaultPawn으로 설정해준다.

실행시키면 오른쪽 창에서 GameMode에 설정해둔 인스턴스를 확인 가능하다.
- 2단계 : Enhanced Input 액션 설정

Move (WASD용 - Vector2D 타입)
Look (마우스 회전용 - Vector2D 타입)



IMC에 키를 매핑해줬다.
// MyPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MyPlayerController.generated.h"
class UInputMappingContext;
class UInputAction;
UCLASS()
class NBC_UE_P7_API AMyPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AMyPlayerController();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Input")
UInputMappingContext* InputMappingContext;
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()
:InputMappingContext(nullptr),
MoveAction(nullptr),
LookAction(nullptr)
{
}
void AMyPlayerController::BeginPlay()
{
Super::BeginPlay();
if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
if (InputMappingContext)
{
Subsystem->AddMappingContext(InputMappingContext, 0);
}
}
}
}

PlayerController에서 IA와 IMC를 연결해준다.
void AMyPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
if (AMyPlayerController* PlayerController = Cast<AMyPlayerController>(GetController()))
{
if (PlayerController->MoveAction)
{
// IA_Move 액션 키를 "키를 누르고 있는 동안" Move() 호출
EnhancedInput->BindAction(
PlayerController->MoveAction,
ETriggerEvent::Triggered,
this,
&AMyPawn::Move
);
}
if (PlayerController->LookAction)
{
// IA_Look 액션 마우스가 "움직일 때" Look() 호출
EnhancedInput->BindAction(
PlayerController->LookAction,
ETriggerEvent::Triggered,
this,
&AMyPawn::Look
);
}
}
}
}
SetupPlayerInputComponent()에서 액션들을 입력 처리 함수에 바인딩한다.
- 3단계 : Pawn 이동 로직 작성

void AMyPawn::Move(const FInputActionValue& value)
{
if (!Controller) return;
const FVector2D MoveInput = value.Get<FVector2D>();
const float Speed = 300.0f;
const float Delta = GetWorld()->GetDeltaSeconds();
if (!FMath::IsNearlyZero(MoveInput.X))
{
// 캐릭터가 바라보는 방향(정면)으로 X축 이동
//AddMovementInput(GetActorForwardVector(), MoveInput.X);
AddActorLocalOffset(FVector(MoveInput.X,0.f,0.f) * Speed * Delta, true);
}
if (!FMath::IsNearlyZero(MoveInput.Y))
{
// 캐릭터의 오른쪽 방향으로 Y축 이동
//AddMovementInput(GetActorRightVector(), MoveInput.Y);
AddActorLocalOffset(FVector(0.f,MoveInput.Y,0.f) * Speed * Delta, true);
}
}
AddActorLocalOffset() 을 통해 이동을 구현했다.
void AMyPawn::Look(const FInputActionValue& value)
{
// 마우스의 X, Y 움직임을 2D 축으로 가져옴
FVector2D LookInput = value.Get<FVector2D>();
const float Speed = 90.0f;
const float Delta = GetWorld()->GetDeltaSeconds();
// 좌우
float CameraYaw = LookInput.X * Speed * Delta;
AddActorLocalRotation(FRotator(0.f, CameraYaw, 0.f));
// 상하
CameraPitch -= LookInput.Y * Speed * Delta;
CameraPitch = FMath::Clamp(CameraPitch, -80.f, 80.f);
SpringArmComp->SetRelativeRotation(FRotator(CameraPitch, 0.f, 0.f));
}
상하 움직임은 SpringArmComp를 회전하고
좌우 움직임은 액터가 회전하도록 했다.
- 도전 과제 1번
- 6축 이동 및 회전 액션 구현
- 이동
- 전/후 (W/S) - 로컬 X축 이동
- 좌/우 (A/D) - 로컬 Y축 이동
- 상/하 (Space/Shift) - 로컬 Z축 이동
- 회전
- Yaw - 좌우 회전, 마우스 X축 이동
- Pitch - 상하 회전, 마우스 Y축 이동
- Roll - 기울기 회전, 마우스 휠 또는 별도 키
- 이동
- Orientation 기반 이동 구현
- 현재 Pawn의 회전 상태에 따라 이동 방향이 결정되는 비행체 움직임을 구현합니다.
- 단순 월드 좌표계 이동이 아닌, Pawn의 로컬 좌표계 기준 이동을 구현합니다.
실제 비행기 조종 원리 (요크 / 스틱 기준)
비행기 조종간은 카메라 시점 조작이 아니라 기체 자세 조작이야.
조종간을 앞으로 민다 → 엘리베이터가 아래로 움직임 → 기수(Nose)가 내려감 → 하강
조종간을 뒤로 당긴다 → 엘리베이터가 위로 움직임 → 기수가 올라감 → 상승
void AMyPlane::Move(const FInputActionValue& value)
{
if (!Controller) return;
const FVector3d MoveInput = value.Get<FVector3d>();
const float Speed = 300.0f;
const float Delta = GetWorld()->GetDeltaSeconds();
if (!FMath::IsNearlyZero(MoveInput.X))
{
// 캐릭터가 바라보는 방향(정면)으로 X축 이동
//AddMovementInput(GetActorForwardVector(), MoveInput.X);
AddActorLocalOffset(FVector(MoveInput.X, 0.f, 0.f) * Speed * Delta, true);
}
if (!FMath::IsNearlyZero(MoveInput.Y))
{
// 캐릭터의 오른쪽 방향으로 Y축 이동
//AddMovementInput(GetActorRightVector(), MoveInput.Y);
AddActorLocalOffset(FVector(0.f, MoveInput.Y, 0.f) * Speed * Delta, true);
}
if (!FMath::IsNearlyZero(MoveInput.Z))
{
// 캐릭터의 위쪽 방향으로 Z축 이동
AddActorLocalOffset(FVector(0.f, 0.f, MoveInput.Z) * Speed * Delta, true);
}
}
void AMyPlane::Look(const FInputActionValue& value)
{
// 마우스의 X, Y 움직임을 2D 축으로 가져옴
FVector3d LookInput = value.Get<FVector3d>();
const float Speed = 90.0f;
const float Delta = GetWorld()->GetDeltaSeconds();
// 좌우
float CameraYaw = LookInput.X * Speed * Delta;
AddActorLocalRotation(FRotator(0.f, CameraYaw, 0.f));
// 상하
float CameraPitch = LookInput.Y * Speed * Delta;
AddActorLocalRotation(FRotator(CameraPitch, 0.f, 0.f));
// 기울기
float CameraRoll = LookInput.Z * Speed * Delta;
AddActorLocalRotation(FRotator(0.f, 0.f, CameraRoll));
}
기존 코드에서 간단하게 수정 및 추가 해주면 된다.
Roll은 E,Q 키로 매핑하고 상하 이동은 Space bar, Left Shift 로 했다.


시연 영상에서 필수 과제와 도전 과제가 같이 보였으면 좋겠어서 트리거 액터를 통해 폰 클래스를 교체할 수 있도록 했다.
// PawnSwitchTrigger.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GameFramework/Pawn.h"
#include "PawnSwitchTrigger.generated.h"
class UBoxComponent;
class APawn;
UCLASS()
class NBC_UE_P7_API APawnSwitchTrigger : public AActor
{
GENERATED_BODY()
public:
APawnSwitchTrigger();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pawn Switch")
TSubclassOf<APawn> SwitchPawnClass;
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere)
UBoxComponent* Box;
UFUNCTION()
void OnOverlapBegin(
UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult
);
private:
bool bIsSwitching = false;
};
// PawnSwitchTrigger.cpp
#include "PawnSwitchTrigger.h"
#include "Components/BoxComponent.h"
#include "MyGameMode.h"
#include "Kismet/GameplayStatics.h"
APawnSwitchTrigger::APawnSwitchTrigger()
{
PrimaryActorTick.bCanEverTick = false;
Box = CreateDefaultSubobject<UBoxComponent>(TEXT("Box"));
RootComponent = Box;
Box->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Box->SetCollisionResponseToAllChannels(ECR_Ignore);
Box->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
}
void APawnSwitchTrigger::BeginPlay()
{
Super::BeginPlay();
Box->OnComponentBeginOverlap.AddDynamic(this, &APawnSwitchTrigger::OnOverlapBegin);
}
void APawnSwitchTrigger::OnOverlapBegin(
UPrimitiveComponent*,
AActor* OtherActor,
UPrimitiveComponent*,
int32,
bool,
const FHitResult&
)
{
if (bIsSwitching)
return;
bIsSwitching = true;
APawn* Pawn = Cast<APawn>(OtherActor);
if (!Pawn) return;
AMyGameMode* GM = Cast<AMyGameMode>(UGameplayStatics::GetGameMode(this));
if (!GM) return;
GM->SwitchPlayerPawn(SwitchPawnClass);
}
3. RESULT
https://github.com/yoonseo4343/NBC_UE_P7
GitHub - yoonseo4343/NBC_UE_P7: 내일배움캠프 언리얼7기 과제 7
내일배움캠프 언리얼7기 과제 7. Contribute to yoonseo4343/NBC_UE_P7 development by creating an account on GitHub.
github.com
'내배캠Unreal_TIL > UE' 카테고리의 다른 글
| [TIL] 2026-01-16 | UE 애니메이션 리타겟팅 (0) | 2026.01.16 |
|---|---|
| [TIL] 2026-01-15 | UE 회전 발판과 움직이는 장애물 (0) | 2026.01.15 |
| [TIL] 2026-01-13 | State Machine 설계를 통한 캐릭터 애니메이션 적용 (0) | 2026.01.13 |
| [TIL] 2026-01-12 | UE C++ 캐릭터 컨트롤 (0) | 2026.01.12 |
| [TIL] 2026-01-09 | C++ 문자 종류 판단, Actor 라이프 사이클, Transform, 리플렉션 시스템 (0) | 2026.01.09 |