Unreal Engine 5/C++와 Unreal Engine으로 3D 게임 개발

레벨 위에 Character 클래스 생성

iiblueblue 2025. 1. 24. 12:49
⊙ Pawn과 Character 클래스의 차이를 이해한다.
⊙ Character 클래스를 생성하는 방법을 안다.
⊙ Character 클래스의 컴포넌트를 조절하는 방법을 안다.
⊙ Character 클래스를 등록하여 스폰하는 방법을 안다.
더보기

코드스니펫

[SpartaCharacter.h]

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaCharacter.generated.h"

class USpringArmComponent;
class UCameraComponent;

UCLASS()
class CH3_LEARNINGPROJECT_API ASpartaCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ASpartaCharacter();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera")
	USpringArmComponent* SpringArmComp;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	UCameraComponent* CameraComp;

protected:
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};

 

[SpartaCharacter.cpp]

// Fill out your copyright notice in the Description page of Project Settings.


#include "SpartaCharacter.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"

ASpartaCharacter::ASpartaCharacter()
{
	PrimaryActorTick.bCanEverTick = true;

	SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArmComp->SetupAttachment(RootComponent);
	SpringArmComp->TargetArmLength = 300.0f;
	SpringArmComp->bUsePawnControlRotation = true;

	CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
	CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
	CameraComp->bUsePawnControlRotation = false;
}

void ASpartaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

 

[SpartaGameMode.h]

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "SpartaGameMode.generated.h"

/**
 * 
 */
UCLASS()
class CH3_LEARNINGPROJECT_API ASpartaGameMode : public AGameMode
{
	GENERATED_BODY()
public:
	ASpartaGameMode();
	
};

 

[SpartaGameMode.cpp]

// Fill out your copyright notice in the Description page of Project Settings.


#include "SpartaGameMode.h"
#include "SpartaCharacter.h"

ASpartaGameMode::ASpartaGameMode()
{
	DefaultPawnClass = ASpartaCharacter::StaticClass(); // 객체를 생성하지는 않고 클래스를 반환
}

 

 

Pawn과 Character

Pawn과 Character는 부모 자식 관계이다. 그래서 Pawn과 Character에는 서로 제공하고 있는 기능이 비슷하면서도 다른 부분들이 있다. 블루프린트 클래스로 Pawn과 Character을 만들어 비교해보자.

 

Pawn 클래스

Pawn은 플레이어 혹은 AI가 "빙의(posses)"할 수 있는 가장 상위 클래스이다. 즉, 엔진에서 "무언가 조종한다"라고 할 때 기본이 되는 형태가 Pawn이 된다.

기본으로 주는 컴포넌트도 특별한건 없다

 

빙의가 되고 조종을 할 수 있다고 했지만 움직이고 중력이 적용되어 떨어지고 그렇다는 뜻은 아니다. Pawn에서는 이동로직, 충돌처리, 중력, 네트워크 이동을 위한 기능이 기본적으로 포함되어 있지 않기 때문에 제로 베이스로 전부 만들어야 한다. 그렇기 때문에 이런 기능들이 필요한 사람 캐릭터를 만들 것이라면 Pawn으로 시작해 구현하는 것은 부담스럽다.

 

하지만 이런 사람의 움직임이 아닌 특수한 로직을 가진 객체라면 Pawn으로 구현하기 시작하는 것이 낫다. 예를 들어 자동차, 비행기, 드론, 카메라 등이 있다.

 

Character 클래스

캐릭터는 Pawn을 상속받아 만들어진 자식 클래스 중 하나로, 기본적으로 UCharacterMovementComponent를 포함하고 있다. 그 말고도 많은 컴포넌트들이 기본적으로 포함되어 있는 것을 볼 수 있다.

간단하게 포함되어 있는 컴포넌트에 대해서 설명하고 넘어가도록 하겠다. 아래와 같은 컴포넌트가 있다는 점이 Pawn과 확연한 차이를 가진다.

컴포넌트명 설명
CapsuleComponent(Root Component) 캐릭터가 벽이나 지형에 충돌하는 범위를 정의하는 콜리전 컴포넌트
캡슐 형태이며, Radius와 Half Height를 조정해 캐릭터의 물리적 크기를 설정 가능
ArrowComponent 캐릭터가 어느 방향을 바라보고 있는지 표시하기 위해 씬에 화살표를 띄워주는 컴포넌트
게임플레이 로직에서는 직접적인 영향을 주지 않고, 주로 편집기에서 시각적 디버깅용으로 사용
SkeletalMeshComponent 캐릭터의 3D 모델과 애니메이션을 적용하는 컴포넌트
Skeletal Mesh, Anim Bluleprint 등을 여기로 할당해 캐릭터의 외형과 동작을 제어
CharacterMovementComponent 캐릭터의 이동, 점프, 중력, 네트워크 동기화 등 물리적 이동 로직을 담당하는 핵심 컴포넌트
언리얼에서 제공하는 주요 이동 함수(MoveForward, MoveRight, Jump)가 이미 연결되어 있어, 최소한의 코드만으로도 캐릭터 조작 구현 가능

 

Pawn과 다르게 이동, 회전, 점프, 중력, 지형 따라가기, 네트워크 동기화 등 보행형 캐릭터에 필요한 기능이 이미 구현되어 있어 사람 형태의 캐릭터를 만들기에 편리하다. 거기다가 미리 정의된 대표적인 함수들(예: MoveForward, MoveRight, Jum)이 존재하므로 금방 구현이 가능하다.

 

하지만 반대로 자동차나 비행기처럼 완전히 다른 이동 방식을 구현해야할 때는 캐릭터가 제공하는 기능들이 방해가 될 수 있다. 이 때는 Pawn을 기반으로 구현하는 것이 더 낫다.

 


 

Character 클래스 생성

Character 생성도 부모 클래스만 다르게 설정해주면 된다. Character 클래스를 생성할 때 부모 클래스를 Character을 선택하고 생성해준다.

Character 클래스 구성

만들어진 헤더 파일과 cpp 파일은 아래와 같다.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaCharacter.generated.h"

UCLASS()
class CH3_LEARNINGPROJECT_API ASpartaCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ASpartaCharacter();

protected:
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};
#include "SpartaCharacter.h"

ASpartaCharacter::ASpartaCharacter()
{
	PrimaryActorTick.bCanEverTick = true;
}

void ASpartaCharacter::BeginPlay()
{
	Super::BeginPlay();
}

void ASpartaCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

void ASpartaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
}

 


 

Character 컴포넌트 조정

아무래도 C++ 클래스 만으로는 보면서 캐릭터를 조정할 수 없으니 이 또한 Blueprint 클래스로 감싸서 만들어 주는 것이 편집하기 편하다. 

스켈레탈 메시(Skeletal Mesh) 설정

Skeletal Mesh는 이전까지 보던 Static Mesh랑 이름이 다른데 아주 큰 차이가 있다. 내부에 뼈대가 있다는 점이다. 이 뼈대(본, Bone)은 아래 그림처럼 부모-자식 관계로 되어 있으며, 본이 움직이면 외형(Mesh)도 함께 움직이게 된다. 그럼 Static Mesh는 뼈가 없으니까 못 움직이냐 하면 맞다. Static Mesh는 움직이지 못한다.

본과 메시는 연동되기 때문에 애니메이션(Bone 움직임)에 맞춰 캐릭터가 뛰거나 걷는 동작을 할 수 있게 되는 것이다. 언리얼 엔진은 물리 엔진과도 연결할 수 있어, Ragdoll(피격 후 쓰러지는) 효과 등 물리 기반 애니메이션 구현도 쉽게 가능하다.

 

Skeletal Mesh라고 다른 방법으로 적용하지 않는다. BluePrint 편집창으로 들어와 Static Mesh와 마찬가지로 Meh에서 Skeletal Mesh Asset을 선택해준다.

메시를 적용하면 뷰포트에 자동으로 캐릭터 외형이 표시되고, Material도 기본으로 연결된다.

 

위 사진을 보자마자 뭔가 이상하다는 생각이 들어야 한다. 캐릭터는 캡슐이랑 맞지 않게 붕 떠있고 캐릭터가 화살표 방향으로 쳐다보고 있지도 않다. 이를 조정해주는 것은 어렵지 않다. 캐릭터를 아래로 이동 시키고 화살표 방향을 보도록 돌려준다.

 

캡슐 크기 조정

캡슐이 캐릭터보다 조금 작아서 캐릭터가 캡슐 안에 들어가도록 radius와 half height를 조절해준다.

 

스프링암(SpringArm) 및 카메라(Camera Component) 추가

지금 만드려고 하는 플레이어는 1인칭이 아니라 3인칭으로 동작할 것이다. 따라서 캐릭터를 바라보고 있는 카메라가 하나 필요하다. SpringArm은 이미 블루프린트를 공부하면서도 봤지만 셀카봉 같은 것이라고 생각하면 된다. 그리고 그 끝에는 Camera를 달아줄 것이다.

 

스프링암(SpringArm)과 카메라(Camera Component)를 추가하기 위해서는 새로운 컴포넌트를 추가하는 것이므로 Visual Studio를 다시 켜주도록 한다.

 

SpartaCharater.h 파일에서 일단 필요한 컴포넌트가 생기면 언제나 그랬듯 컴포넌트들을 선언해주도록 하자.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaCharacter.generated.h"

UCLASS()
class CH3_LEARNINGPROJECT_API ASpartaCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ASpartaCharacter();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera")
	USpringArmComponent* SpringArmComp;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	UCameraComponent* CameraComp;

protected:
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};

이렇게 UPROPERTY까지 조건을 붙여서 예쁘게 선언했건만, 이 코드는 오류가 난다. 특별한건 아니고 USpringArmComponent와 UCameraComponent를 사용하려면 앞에서 관련 헤더 파일을 선언해야만 한다. 앞에 헤더 파일을 선언하면 되겠지만 다른 방법을 사용해서 문제를 해결해보려 한다.

 

전방 선언(Forward Declaration)은 헤더 파일의 의존성을 줄이는 좋은 습관이다. #include를 일단 적고 보는 것보다 헤더 파일에서는 아래 처럼 클래스를 전방선언 해주고 cpp 파일에서 실제 헤더파일은 include 해주면 파일의 의존성을 줄일 수 있다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaCharacter.generated.h"

class USpringArmComponent;
class UCameraComponent;

UCLASS()
class CH3_LEARNINGPROJECT_API ASpartaCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ASpartaCharacter();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera")
	USpringArmComponent* SpringArmComp;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	UCameraComponent* CameraComp;

protected:
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};

 

헤더 파일은 이렇게 선언해주고 구현 파일은 아래와 같이 작성한다.

// Fill out your copyright notice in the Description page of Project Settings.


#include "SpartaCharacter.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"

ASpartaCharacter::ASpartaCharacter()
{
	PrimaryActorTick.bCanEverTick = false; // Tick 끄기

	SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArmComp->SetupAttachment(RootComponent);
	SpringArmComp->TargetArmLength = 300.0f;
	SpringArmComp->bUsePawnControlRotation = true;

	CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
	CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
	CameraComp->bUsePawnControlRotation = false;
}

void ASpartaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

앞쪽 부터 보면 CameraComponent 헤더 파일과 SpringArmComponent 헤더가 include 되어 있는 것을 확인할 수 있다.

 

그럼 본격적으로 컴포넌트를 만들고 붙여보자. SpringArmComponent를 CreateDefaultSubobject를 이용해 생성하고 SetupAttachment를 이용해 RootComponent 하위에 붙여준다. CameraComp도 같은 방법으로 생성해주고 대신 SprintArmComp 하위에 붙여준다. 처음 보는 함수들, 매개변수들은 아래서 좀 더 자세히 다뤄보자.

 

SpringArmComp->TargetArmLength = 300.0f;

카메라암의 팔 길이의 기본값을 300으로 설정한 것이다. 즉, 카메라와 캐릭터 사이의 거리를 설정한 것이라고 할 수 있다.

SpringArmComp->bUsePawnControlRotation = true;

컨트롤러 회전에 따라 스프링 암도 회전하도록 설정한 것이다. 플레이어가 마우스를 움직이면 Controller의 회전값이 변경되고, 이 때 스프링 암도 같이 회전하도록 설정한다. 결과적으로 카메라 각도에 따라 회전하여 3인칭 시점을 자연스럽게 구현할 수 있다.

CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);

카메라 컴포넌트를 SprintArmComp의 하위로 붙이는 상황에서 마지막 매개변수 하나가 더 있는 것을 발견했을 것이다. 이 매개변수는 스프링암의 끝 부분을 가리키는 것으로 소켓이라고 한다. 결국 이 코드의 뜻은 스프링 암 소켓 위치(가장 끝 부분)에 카메라를 부착하겠다는게 된다.

CameraComp->bUsePawnControlRotation = false;

위에 SprintArmComp는 컨트롤러 회전에 따라 함께 회전하도록 하였지만 카메라는 그 회전을 막은 것이다. 이미 스프링 암이 회전을 처리하므로 카메라 자체는 PawnControlRotation을 사용하지 않도록 한다. 카메라는 그냥 스프링암에 달려만 있으면 스프링암이 회전할 것이다.

 

이렇게 컴포넌트를 추가 후 빌드를 돌리고 언리얼 에디터로 돌아가보면 스프링암과 카메라가 추가되어 있는 것을 볼 수 있다.

 

카메라는 어깨 높이까지 올라가 있는 것이 좋기 때문에 아래와 같이 높이를 수정해준다.

 

갑자기 드는 의문점. 분명 스프링암과 카메라 모두 UPROPERTY를 설정할 때 어디서든 볼 수 있고 블루프린트에서 읽기만 가능하도록 하였는데 위치가 수정되는 이유는 뭘까? 사실 그렇게 설정하여도 블루프린트 에디터에서는 수정할 수 있다.

 


 

Character 등록

만들어 놓은 캐릭터는 등록해놓지 않으면 빙의해주지도 않고 레벨에 등장하지도 않는다. 어떤 캐릭터를 플레이어로 사용할지 지정해주는 역할을 하는 클래스는 무엇일까? 바로 GameMode 클래스이다.

https://iiblueblue.tistory.com/184

 

Unreal Engine의 GameMode

⊙ Unreal Engine의 GameMode가 무엇인지 이해한다. ⊙ GameMode 클래스를 생성하는 방법에 대해 안다. ⊙ 생성한 GameMode 클래스를 적용하는 방법을 안다. GameMode란?게임을 제작하다보면 가장 큰 컨트롤

iiblueblue.tistory.com

GameMode의 DefaultPawnClass에서는 게임 시작 시 어떤 캐릭터(Pawn/Character)를 플레이어에게 제공할 것인지를 결정한다. 

Visual Studio에서 설정

이전에 직접 만들었던 SpartaGameMode 헤더와 구현 파일을 열어서 코드를 수정한다.

 

먼저 헤더 파일에는 생성자를 만들어준다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "SpartaGameMode.generated.h"

UCLASS()
class CH3_LEARNINGPROJECT_API ASpartaGameMode : public AGameMode
{
	GENERATED_BODY()
public:
	ASpartaGameMode();
	
};

 

그리고 중요한건 구현 파일이다.

// Fill out your copyright notice in the Description page of Project Settings.


#include "SpartaGameMode.h"
#include "SpartaCharacter.h"

ASpartaGameMode::ASpartaGameMode()
{
	DefaultPawnClass = ASpartaCharacter::StaticClass(); // 객체를 생성하지는 않고 클래스를 반환
}

생성자에서 DefaultPawnClass를 설정해준다. 이 코드가 있으면 게임 시작 시 SpartaCharacter가 자동으로 스폰되고, PlayerController와 연결되어 플레이어가 조작할 수 있게 된다.

 

한 가지 더 유용하게 앞으로 사용할 수 있는 것은 StaticClass()함수이다. StaticCalss() 함수는 객체를 생성하지는 않고 클래스로 반환하는 함수이다. 앞으로 객체를 생성하지는 않으면서 클래스만 전달하고 싶을 때 사용하도록 하자.

 

Unreal Editor에서 설정

언리얼 에디터에서도 SpartaGameMode의 DefaultPawnClass를 설정할 수 있다.

사진은 WorldSettings이다

전역 GameMode를 설정했던 Edit → Project Settings → Maps&Modes 라던지, 레벨 GameMode를 설정했던 Window   WorldSettings에서도 레벨의 캐릭터를 지정할 수 있다.

 

등록을 마친 후 플레이를 누르면

아래처럼 캐릭터가 잘 생성된 것을 볼 수 있다.


 

배운 내용 정리

  • Pawn은 플레이어 혹은 AI가 "빙의(posses)"할 수 있는 가장 상위 클래스이다. 즉, 엔진에서 "무언가 조종한다"라고 할 때 기본이 되는 형태가 Pawn이 된다.
  • 캐릭터는 Pawn을 상속받아 만들어진 자식 클래스 중 하나로, 기본적으로 UCharacterMovementComponent를 포함하고 있다. 그 외엔 Capsule, SkeletalMesh, Arrow 컴포넌트가 포함된다.
  • Character 생성도 부모 클래스만 다르게 설정해주면 된다. Character 클래스를 생성할 때 부모 클래스를 Character을 선택하고 생성해준다.
  • C++ 클래스 만으로는 보면서 캐릭터를 조정할 수 없으니 이 또한 Blueprint 클래스로 감싸서 만들어 주는 것이 편집하기 편하다. 블루프린트 에디터에서 캐릭터의 위치와 방향(x축을 바라봄)을 알맞게 조절해주자.
  • 3인칭을 만들기 위해서는 SpringArm 컴포넌트와 Camera 컴포넌트를 코드로 추가한다.
  • 전방 선언(Forward Declaration)은 헤더 파일의 의존성을 줄이는 좋은 습관이다.
  • GameMode의 DefaultPawnClass에서는 게임 시작 시 어떤 캐릭터(Pawn/Character)를 플레이어에게 제공할 것인지를 결정한다. 코드로 추가하는 것과 에디터에서 추가하는 것 모두 가능하다.