개요
UE4 학습 목적으로 옛날에 구매하고 잊고있던 '이득우의 언리얼 C++ 게임 개발의 정석' 책을 언리얼 엔진5 학습차원에서
UE5로 학습하면 어떤 부분이 달라지는 지와 공부를 하며 주의할 내용들에 대한 글입니다.
책의 내용이 궁금하다면 다른 분이 정리한 블로그나 자료를 찾아보는 걸 권장합니다.
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)라고 한다.
플레이어가 플레이 버튼을 눌러 게임을 시작하면 아래와 같은 순서로 플레이를 위한 설정이 갖춰진다.
- 플레이어의 컨트롤러 생성
- 플레이어 폰의 생성
- 플레이어 컨트롤러가 플레이어 폰을 빙의
- 게임의 시작
플레이어 컨트롤러를 게임 모드에 설정하는 것도 폰을 설정하는 것과 동일한 방식이다.
#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;
}
'개발 공부 기록 > UnrealEngine5' 카테고리의 다른 글
이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 5 (1) | 2024.11.07 |
---|---|
이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 4 (3) | 2024.11.06 |
이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 3 (0) | 2024.11.02 |
[UE5] 핫 리로드(Hot Reload)와 라이브 코딩(Live Coding) 테스트 및 정리 (1) | 2024.10.30 |
이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 1 (0) | 2024.10.30 |