본문 바로가기

개발 공부 기록/UnrealEngine5

이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 2

개요 

UE4 학습 목적으로 옛날에 구매하고 잊고있던 '이득우의 언리얼 C++ 게임 개발의 정석' 책을 언리얼 엔진5 학습차원에서
UE5로 학습하면 어떤 부분이 달라지는 지와 공부를 하며 주의할 내용들에 대한 글입니다.

책의 내용이 궁금하다면 다른 분이 정리한 블로그나 자료를 찾아보는 걸 권장합니다.

 

알라딘 책 구매 링크

 

이득우의 언리얼 C++ 게임 개발의 정석

언리얼 엔진 학습에 목말라하는 게임 개발자에게 단비 같은 언리얼 엔진 프로그래밍 책이다. 에픽게임즈 본사의 개발자 프로그램 언리얼 데브 그랜트를 수상한 저자의 책으로, 언리얼 엔진의

www.aladin.co.kr

 

이득우님 인프런 강의 링크

 

이득우님의 강의 - 인프런

인프런 이득우님의 강의 페이지 입니다. - 이득우님 소개 | 인프런

www.inflearn.com

 

IDE: Visual Studio 2022
엔진 버전: Unreal Engine 5.4 

 

본문

문자열을 정의할 때는 모든 플랫폼에서 2바이트 문자열을 지원하는 TEXT 매크로를 사용하는 것이 좋음.
기본적인 문자열 관리는 FString 클래스를 제공해주는데 선언된 변수에서 문자열 정보를 얻어오려면
*연산자를 붙여서 사용해야 함. 아래의 "*GetName()" 부분이 그 예시이다.

UE_LOG(ArenaBattle, Warning, TEXT("Actor Name :  %s, ID : %d, Location X : %.3f"), *GetName(), ID, GetActorLocation().X);

 

직접 로그 카테고리를 지정하여 분류하면 개발할 때 따로 확인도 되고 편하게 사용할 수 있음.

로그 카테고리를 선언하려면 헤더 파일에 DECLARE_LOG_CATEGORY_EXTERN 이라는 매크로를,
cpp 파일에 DEFINE_LOG_CATEGORY 라는 매크로를 사용해야 한다.

// log.h
DECLARE_LOG_CATEGORY_EXTERN(ArenaBattle, Log, All);

// DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity)
// Verbosity는 수준을 의미하며 여기서는 로그 경고 수준을 의미한다.


// log.cpp
DEFINE_LOG_CATEGORY(ArenaBattle);


// actor.cpp
UE_LOG(ArenaBattle, Warning, TEXT("Actor Name :  %s, ID : %d, Location X : %.3f"), *GetName(), ID, GetActorLocation().X);

// 선언한 카테고리 'ArenaBattle'의 Warning 경고 수준으로, TEXT인자에 해당하는 로그가 호출된다.

 

#define을 통해 매크로 함수를 정의해서 사용할 때 위에 선언된 ‘ArenaBattle’ 카테고리를 활용해서 지정해줄 수 있게 된다.

#define ABLOG_CALLINFO (FString(__FUNCTION__) + TEXT("(") + FString::FromInt(__LINE__) + TEXT(")"))
#define ABLOG_S(Verbosity) UE_LOG(ArenaBattle, Verbosity, TEXT("%s"), *ABLOG_CALLINFO)
#define ABLOG(Verbosity, Format, ...) UE_LOG(ArenaBattle, Verbosity, TEXT("%s%s"), *ABLOG_CALLINFO, *FString::Printf(Format, ##__VA_ARGS__))

 

Tick 함수를 통해 액터의 움직임을 구현할 수 있다.

이전 렌더링 프레임에서 현재 렌더링 프레임까지 소요된 시간은 Tick함수의 인자인 DeltaSeconds를 통해 알 수 있다.

private로 변수를 선언할 시 컴파일 과정에서 에러가 발생한다.
→ 언리얼 에디터에서 변수의 값을 변경하려면 접근 권한을 개방해야 하기 때문

따라서 UPROPERTY 매크로에 AllowPrivateAccess라는 META 키워드를 추가하여 캡슐화를 할 수 있다.

private:
	UPROPERTY(EditAnywhere, Category=Stat, Meta= (AllowPrivateAccess = true))
	float RotateSpeed;

 

언리얼 엔진에서 시간을 관리하는 주체는 ‘월드’다.

월드의 시간 관리자(TimeManager)를 통해 시간 값들을 얻어올 수 있다.

Tick함수 인자의 DeltaSeconds 값은 GetWorld()→GetDeltaSeconds() 함수를 사용해 가져올 수 있다.

그 외의 시간 함수들도 GetWorld()를 통해 가져올 수 있다.

게임 월드의 시간은 현실과 동일한 초 단위이다.

 

언리얼 엔진에서 움직임이라는 요소를 액터와 별도로 관리하도록 분리해서 프레임워크를 구성했는데 이를 무브먼트 컴포넌트라고 한다.

무브먼트 컴포넌트는 액터의 움직임에 대한 것을 책임지며 액터는 무브먼트 컴포넌트가 제공하는 이동 메커니즘에 따라 움직인다.

  • FloatingPawnMovement: 중력의 영향을 받지 않는 액터의 움직임을 제공한다. 입력에 따라 자유롭게 움직이게 설계됐다.
  • RotatingMovement: 지정한 속도로 액터를 회전시킨다.
  • InterpMovement: 지정한 위치로 액터를 이동시킨다.
  • ProjectileMovement: 액터에 중력의 영향을 받아 포물선을 그리는 발사체의 움직임을 제공한다.
    주로 총알, 미사일 등에 사용한다.

Tick 함수는 매 프레임마다 콜되어 연산을 수행한다. 그렇기 때문에 만약 성능을 위해 꺼주고 싶다면 생성자 함수 내에 있는 PrimaryActorTick.bCanEverTick의 값을 false로 바꿔주면 된다. true로 바꿀 경우 다시 액터의 Tick 함수가 콜된다.

// Set this actor to call Tick() every frame.  
//You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;

 

액터에게 무브먼트 컴포넌트를 부여한다면 현재 위치와 상관없이 독립적으로 컴포넌트에서 분리되어 액터에게 부착된다.
따라서 아래와 같이 Movement라고 무브먼트 컴포넌트를 부여한다면 분리되어 있는 모습을 확인할 수 있다.

스태틱메시 컴포넌트처럼 트랜스폼 정보가 필수적인 컴포넌트를 ‘씬 컴포넌트’라고 하고,
무브먼트 컴포넌트와 같이 기능만 제공하는 컴포넌트를 ‘액터 컴포넌트’라고 부른다.

 

언리얼 엔진에서 게임을 만드는 작업은 레벨은 구성하는 작업과 게임 플레이를 설계하는 작업 두 가지로 나뉜다.

언리얼 엔진은 게임플레이 프레임워크라는 시스템을 제공하여 게임을 설계하고 만드는데 효과적으로 관리할 수 있도록 도와준다.
대표적인 핵심 요소 세 가지 액터는 아래와 같다.

  • 게임 모드: 게임의 규칙 및 플레이어를 생성하는 등의 게임의 틀을 잡아주는 역할
  • 플레이어 컨트롤러: 게임에 세계에서 플레이어를 대변하여 폰을 움직이는 액터
  • : 플레이어가 조종하는 액터

게임 모드를 특정 레벨에 적용하려면 세팅 → 월드 세팅 를 누르고 열린 월드 세팅 창에서
Game Mode → 게임 모드 오버드라이브에 원하는 게임 모드를 지정해주면 된다.

 

게임 모드에서 기본으로 지정되는 폰을 수정하려면 게임 모드 생성자에서 DefaultPawnClass에
폰의 StaticClass를 넣어 클래스 정보를 넘겨주면 된다.

#include "ABGameMode.h"
#include "ABPawn.h"

// 생성자 생성
AABGameMode::AABGameMode()
{
	// 현재 게임 모드의 기본 폰 설정을 ABPawn 클래스로 바꿔준다.
	// StaticClass는 언리얼 오브젝트의 클래스 정보로 언리얼 헤더 툴에 의해 자동으로 생성된다.
	DefaultPawnClass = AABPawn::StaticClass();
}

 

게임 모드에서는 게임이 시작하면 플레이어에게 폰을 배정해주면서
동시에 그 폰을 조종할 수 있는 플레이어 컨트롤러 액터도 함께 배정한다.

언리얼 엔진에서 플레이어가 플레이어 컨트롤러를 통해 폰을 조종하는 행위를 빙의(Possess)라고 한다.
플레이어가 플레이 버튼을 눌러 게임을 시작하면 아래와 같은 순서로 플레이를 위한 설정이 갖춰진다.

  1. 플레이어의 컨트롤러 생성
  2. 플레이어 폰의 생성
  3. 플레이어 컨트롤러가 플레이어 폰을 빙의
  4. 게임의 시작

플레이어 컨트롤러를 게임 모드에 설정하는 것도 폰을 설정하는 것과 동일한 방식이다.

#include "ABGameMode.h"
#include "ABPawn.h"
#include "ABPlayerController.h"

// 생성자 생성
AABGameMode::AABGameMode()
{
	// 현재 게임 모드의 기본 폰 설정을 ABPawn 클래스로 바꿔준다.
	// StaticClass는 언리얼 오브젝트의 클래스 정보로 언리얼 헤더 툴에 의해 자동으로 생성된다.
	DefaultPawnClass = AABPawn::StaticClass();
	// 플레이어 컨트롤러 설정도 동일하게 ABPlayerController로 바꿔준다.
	PlayerControllerClass = AABPlayerController::StaticClass();
}

 

플레이어가 게임에 입장하는 것을 로그인이라 하며 로그인 과정에서 플레이어에게 할당한 플레이어 컨트롤러가 생성된다.

플레이어가 로그인을 완료하면 게임 모드의 PostLogin 이벤트 함수가 호출되는데
이 함수 내부에서는 플레이어가 조종할 폰을 생성하고 플레이어 컨트롤러가 해당 폰에 빙의하는 작업이 이뤄진다.

폰과 플레이어 컨트롤러가 생성되는 시점은 각 액터의 PostInitializeComponents 함수로 파악 가능하고
빙의를 진행하는 시점은 플레이어 컨트롤러의 Possess, 폰의 PossessedBy 함수로 파악할 수 있다.

각 함수들을 override하여 로그를 찍어보아 타이밍을 알아보려는데 플레이어 컨트롤러의 Possess 함수는
UE5에서 final 함수로 변경되어 재정의를 할 수가 없었다.

 

공식문서 에서 APlayerController를 찾아본 결과 UE4.22 버전 이후로는 Possess에서 OnPossess로 메서드명이 바뀐 것을 확인할 수 있다.

 

작성 후 실행하면 아래처럼 우선 플레이어 컨트롤러가 생성되어

1. PostInitializeComponents를 실행한 후

2. 게임 모드 PostLogin Begin 

3. 폰의 PostInitializeComponents

4. 플레이어의 OnPossess

5.다시 폰의 PossessedBy

6. PostLogin End

순서대로 나오는 걸 확인할 수 있다.

 

이러한 방식은 몇 명이 들어올지 모르는 멀티 플레이 게임에 적합할 수 있다.
폰의 Auto Possess Player 속성을 사용하면 새롭게 폰을 생성하지 않고 레벨에 이미 배치된 폰에 플레이어 컨트롤러가 빙의할 수 있다.
이를 이용하면 1개의 폰을 조종하는 싱글 게임에서 더 유용하게 사용할 수 있다.

폰을 배치한 후 폰 → 플레이어 자동 빙의 메뉴에서 Player 0으로 변경해주면 게임 모드에서 ABPawn 액터를 생성하지 않고
플레이어 컨트롤러에게 레벨에 있는 배치된 마네킹 액터에게 빙의하라 명령한다.
Player 0은 로컬 플레이어를 의미한다.

 

C++로 제작된 폰이 아닌 블루프린트로 제작된 폰을 기본 폰으로 사용하고자 한다면
블루프린트 애셋의 클래스 정보를 넘겨주면 동일하게 사용할 수 있다.

ContructorHelpers의 FClassFinder를 통해 블루프린트 애셋의 경로로 정보를 가져올 때 경로에
접미사 ‘_C’를 붙이면 블루프린트 애셋의 클래스 정보를 가져올 수 있다.

// BP_ThirdPersonCharacter'_C' 
static ConstructorHelpers::FClassFinder<APawn> BP_PAWN_C(TEXT("/Script/Engine.Blueprint'/Game/ThirdPerson
 /Blueprints/BP_ThirdPersonCharacter.BP_ThirdPersonCharacter_C'"));
if (BP_PAWN_C.Succeeded())
{
	DefaultPawnClass = BP_PAWN_C.Class;
}