[UE5] Unreal Engine 5 길라잡이 - 3. 액터 C++ 클래스

세동네·2022년 6월 7일
2
post-thumbnail

이 시리즈는 이득우의 언리얼 C++ 게임 개발의 정석을 바탕으로 작성되었습니다.

언리얼 에디터의 상단 툴바에서 [ - 새로운 C++ 클래스 생성]을 눌러 '액터'를 선택하고 Fountain이라는 이름으로 클래스를 생성해주었다.

클래스가 정상적으로 생성되면 이처럼 콘텐츠 브라우저 패널의 C++ 클래스 폴더 또는 액터 배치 패널에서 이름을 검색하는 것으로 해당 액터를 확인할 수 있다.

이와 함께 IDE에 해당 클래스 파일이 열린 것을 확인할 수 있을 것이다.

////////////////////////////////////////
////////////// Fountain.h //////////////
////////////////////////////////////////

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Fountain.generated.h"

UCLASS()
class UE5PRACTICE_API AFountain : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AFountain();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};
////////////////////////////////////////
///////////// Fountain.cpp /////////////
////////////////////////////////////////

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


#include "Fountain.h"

// Sets default values
AFountain::AFountain()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void AFountain::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void AFountain::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

이것이 언리얼 엔진이 제공하는 기본 C++ 클래스의 형태이다. 각 코드가 무엇을 의미하는지 이해하고 갈 필요가 있다.

· 클래스.h

먼저 Fountain.h라는 헤더파일을 살펴보자.

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Fountain.generated.h"

액터 클래스의 헤더 파일은 위 헤더들을 기본적으로 포함하고 있다.

  • "CoreMinimal.h"
    언리얼 엔진은 각 C++ 클래스가 사용할 가능성이 있는 많은 헤더들을 기본적으로 포함하도록 설계되어 있고, 이것이 CoreMinimal.h이다. 하지만 이는 언리얼 오브젝트가 동작할 수 있는 최소 기능만 제공하는 헤더이다.

    편집기에서 CoreMinimal.h 파일을 열어 확인해보면 이와 같이 언리얼 엔진 프로그래밍에 필요한 여러 기본적인 타입, 연산들의 헤더를 포함하며, 필요에 따라 더 많은 헤더를 포함하는 상위 헤더 모음을 사용할 수 있다. CoreMinimal.h 또는 이와 함께 다양한 엔진 클래스를 포함한 EngineMinimal.h을 예제에서 주로 사용할 것이다.

    이러한 헤더 모음은 엔진의 부하를 줄이는 IWYU(Include-What-You-Use)모델의 기본이 되는 것이다. 과거 언리얼 엔진은 모든 언리얼 오브젝트 클래스가 Engine.h와 같은 모놀리식(하나의 큰 덩어리) 헤더 파일을 포함했는데, 엔진 크기가 커짐에 따라 컴파일 시간이 너무 오래 걸리고 병목현상이 발견되는 문제가 생겼다. 이것을 위해 언리얼 엔진 4.16 버전부터 IWYU 모델 방식이 도입되었고, CoreMinimal.h를 최소 형태로 필요한 종속성만 포함하고 나머지는 직접 추가하는 식으로 이루어진다.

  • "GameFramework/Actor.h"
    액터 C++ 클래스로써 작동하기 위해 오브젝트 C++ 클래스가 AActor 클래스를 상속받아야 하며, 이 AActor 클래스의 정의가 포함된 헤더이다.

  • "Fountain.generated.h"
    언리얼 엔진에서 매우 중요한 개념인 리플렉션(언리얼 프로퍼티 시스템)을 위한 헤더 파일이다. 해당 헤더가 있어야 리플렉션이 있는 유형에서 해당 클래스를 고려해야 하고 시스템 구현에 필요함을 언리얼 헤더 툴 UHT(Unreal Header Tool)에 알린다.

    소스 코드를 컴파일하기 이전에 UHT를 사용해 클래스 선언을 분석하고 언리얼 실행 환경에 필요한 부가 정보를 해당 generated.h 파일에 생성한다. 컴파일 과정에서 필연적으로 발생하는 파일이므로 코드 헤더 선언부 마지막에 generated.h 헤더를 반드시 포함해줘야 한다.

    generated.h 헤더를 포함하는 것으로 UENUM(), UCLASS(), USTRUCT(), UFUNCTION(), UPROPERTY() 와 같은 키워드를 사용할 수 있게 되고, 스크립트로 관리하던 다양한 정보를 엔진 에디터에서 다룰 수 있다. 리플렉션은 생각보다 훨씬 중요하여 간단하게 설명하는 것으로 끝낼 개념이 아니기 때문에 따로 포스팅을 하여 정리할 것이다.

- 클래스 선언부 규칙

C++ 클래스 헤더가 포함하는 하위 헤더 파일에 대해 알아보았으니, 클래스 선언부를 살펴보자.

UCLASS()
class UE5PRACTICE_API AFountain : public AActor
{
	GENERATED_BODY()
	...
}

앞서 generated.h 파일을 설명할 때 말한 것처럼 해당 클래스가 언리얼 오브젝트임을 선언하기 위해 클래스 선언 위에 UCLASS() 키워드를, 클래스 내부에 GENERATED_BODY() 키워드를 선언해 해당 클래스가 리플렉션 되었음을 나타낸다.

클래스 이름은 (모듈명)_API A(클래스명)과 같은 구조로 선언된 것을 확인할 수 있다.

(모듈명)_API는 외부 모듈에서 해당 객체에 접근할 수 있게 해주는 키워드로, 윈도우의 DLL 시스템이 _declspec(dllexport)라는 키워드로 DLL 내 클래스 정보를 외부에 공개할지 결정하는 기능을 언리얼에서 가능하게 해주는 용도이다.

또한 클래스명 앞의 A는 해당 클래스가 Actor임을 의미하는 클래스 이름 접두사이다. 이러한 접두사는 크게 AU가 제공되는데, A는 액터 클래스에 사용하고 U는 액터가 아닌 클래스에 사용한다. 우리가 생성한 Fountain 클래스는 액터이므로 접두사 A가 붙었다.

· 클래스.cpp

위 구조를 가지고 있다면 액터 클래스는 기본적인 준비가 된 것이다. 실제 클래스가 해야 하는 일을 함수로 cpp 파일에 작성해주면 된다. 이때 유의해야 할 것은 클래스에서 사용할 함수나 컴포넌트 등은 반드시 클래스.h 파일에 선언해주어야 한다는 것이다. 정의는 cpp 파일에서 한다.

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

}

// Called when the game starts or when spawned
void AFountain::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void AFountain::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

앞서 말한 것처럼 cpp 파일이 포함하는 클래스.h 헤더에 위 세 함수 AFountain(), BeginPlay(), Tick()이 선언되어 있다. 각 함수가 실제로 해야 하는 일은 cpp 파일에서 작성한다.

  • AFountain()
    AFountain()는 생성자로, 인스턴스가 생성되었을 때 호출된다. 즉, 월드에 해당 액터가 배치되었을 때 호출되는 것이다. 이러한 생성자 호출 시점은 레벨이 초기화되지 않은 시점이므로 월드나 다른 액터를 참조하거나 새로운 액터를 생성하는 등의 작업은 불가능하다. 액터의 프로퍼티를 초기화하거나 컴포넌트를 초기화해주는 등의 제한된 작업만 이루어진다.

    생성자 내에 작성돼있는

    PrimaryActorTick.bCanEverTick = true;

    Tick() 함수를 활성화 시키는 트리거 역할로, 아래 Tick() 함수에 대해 설명할 때 같이 알아볼 것이다.

  • BeginPlay()
    레벨이 플레이되거나 스폰됐을 때 즉, 게임이 시작됐을 때 최초 1회 호출되는 함수이다. 이 함수가 호출되는 시점은 레벨이 초기화되고 액터가 완전하게 제 기능을 하기 시작하는 순간이다. 유니티 엔진의 Start() 함수 역할을 한다고 보면 된다. 여기서부터 월드나 다른 액터를 참조할 수 있다. 내부에 기본적으로

    Super::BeginPlay();

    라는 함수를 호출하고 있는데, Fountain 클래스가 상속 받은 AActor 클래스의 BeginPlay() 함수를 호출하는 것이다. 이 때문에 클래스의 헤더에

    // Fountain.h
    
    virtual void BeginPlay() override;

    와 같이 BeginPlay() 함수가 가상함수를 오버로딩하는 형태로 선언되어 있는 것이다. 다만, 기본 C++ 가상함수 사용법과 조금 다르게 부모의 BeginPlay()를 호출해줘야 자식 클래스에서 오버로딩한 BeginPlay()가 실제 게임에서 작동할 수 있다. 해당 코드가 반드시 있어야 함을 유의하자.

  • Tick()
    게임은 시간에 따라 환경이 자연스럽게 변하는 것이 중요하다. 캐릭터가 이동을 할 때 출발점에서 도착점까지 순간이동하는 것이 아니라 필요에 따라 걸어가는 과정을 표현할 필요가 있다. 이것을 위해 프레임 단위로 그 움직임에 필요한 연산을 처리해줄 필요가 있는데, 이러한 처리를 해주는 함수이다. 앞서 살펴본

    PrimaryActorTick.bCanEverTick = true;

    의 코드로 액터의 틱을 활성화시킬 수 있고, 기본적으로 한 프레임에 한 번 Tick() 함수 코드블럭을 실행한다. 유니티 엔진의 Update() 함수와 같은 역할을 한다.

    액터에 Tick() 함수가 필요 없을 수도 있다. 이 경우 클래스 파일에서 Tick() 함수를 지울 수 있지만, 위의 틱을 활성화하는 코드도 반드시 삭제해줘야 한다. 마찬가지로 BeginPlay() 함수도 필요하지 않다면 삭제해도 무방하다.

위 함수들은 기본 액터 클래스가 제공하는 대표적인 함수들이고, 언리얼 엔진에서 제공하는 더 다양한 함수들과 사용자가 필요에 따라 만드는 커스텀 함수들에 대해선 이후 포스팅에서 차차 다뤄볼 것이다.


· 참고

언리얼 엔진 IWYU(Include-What-You-Use) 모델
액터 클래스 생성자와 BeginPlay() 함수의 차이

0개의 댓글