본문 바로가기

개인 프로젝트(과제)

7번 과제(Pawn 클래스로 3D 캐릭터 만들기) 필수 과제

과제 소개

  • 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 했다.

 

자연스러운 카메라 회전과 캐릭터 이동이 완료가 됐다.