Unreal Engine5

[UE5] 용어 정리

econo-my 2024. 9. 2. 14:46

액터의 특성 컴포넌트를 조작하는 섹터

 

· 액터

게임은 수많은 오브젝트로 구성된다. 플레이어가 직접 조작하는 캐릭터, 상대하는 몬스터, 배치된 구조물, 벽, 바닥, 획득할 수 있는 아이템 등 모든 요소들이 고유의 특징을 가지고 있다. 이러한 게임에 배치할 수 있는 모든 오브젝트를 언리얼 엔진에선 '액터'라고 하며, 단어 그대로 게임 월드의 특정 공간에서 자신에게 주어진 역할을 수행하는 요소이다.

이러한 액터의 주요 기능은 크게 세 가지로 나눌 수 있다.

  • 시각적 기능 : 플레이어에게 어떻게 보여질 것인가?
  • 물리적 기능 : 액터의 이동과 액터들 간의 상호 동작을 어떻게 할 것인가?
  • 움직임 : 액터가 어떤 움직임을 가질 것인가?

모든 액터가 위 세 가지 기능 전체를 적극적으로 활용하는 것은 아니다. 구조물이나 맵 자체를 구성하는 액터는 움직임이 필요없을 수도 있고, 캐릭터나 다른 움직이는 액터들이 특정 구역에 들어왔을 때 이벤트를 발생시키는 범위를 표현하는 보이지 않는 액터는 시각적 기능이 필요가 없다.

 

각 액터는 필요한 기능을 붙이고, 필요없는 기능을 제거하여 최적의 형태를 가지게 되는데, 이러한 기능을 '컴포넌트'라고 한다. 기본 빈 액터는 루트 컴포넌트가 DefaultSceneRoot이므로 트랜스폼, 렌더링, 리플리케이션, 콜리전, HLOD, 입력, 액터, 쿠킹이라는 섹션으로 구분된다.

 

 

· 컴포넌트

언리얼 엔진 이전에 유니티 엔진을 다뤄본 경험자라면 위 그림의 트랜스폼, 렌더링 ... 등이 컴포넌트라고 생각할 것이다. 하지만 언리얼 엔진에서 이것은 섹션이라고 칭한다.

언리얼 엔진에서 컴포넌트란

액터에 특수한 기능을 추가할 수 있는 오브젝트

라고 할 수 있다. 즉, 액터를 언리얼 게임 오브젝트의 단위이면서 컴포넌트라는 이름의 특수 유형 오브젝트를 담는 그릇으로 볼 수 있다는 것이다. 컴포넌트를 '특정한 기능 하나'로 정의하지 않고 '오브젝트'라고 하는 것은 서로 다른 컴포넌트에서 같은 섹션을 찾을 수 있기 때문이다.

 

예를 들어 StaticMeshComponent에도 트랜스폼 섹션이 있고, CharacterMesh에도 트랜스폼 섹션이 있다. 심지어 하는 역할도 똑같다.

 

 

· 섹션

섹션은 컴포넌트의 세부 기능이라고 보면 된다. 언리얼의 메시 컴포넌트는 렌더링이 되어 그 자체로 형체를 가지고 그것이 게임 월드에서 특정한 역할을 하게 된다. 이때 메시 컴포넌트를 가진 오브젝트는 충돌이나 중력과 같은 물리 법칙이 성립해야 할 것이고, 월드 내의 어떤 위치에, 어느 정도의 회전 각도를 가지고 있는지와 같은 세부적인 내용들이 필요할 것이다.또한 각 섹션에서 에디터 UI로 수정 가능한 필드를 '프로퍼티' 라고 한다.

 

 

· 루트 컴포넌트

새로 생성한 Default 액터의 컴포넌트는 DefaultSceneRoot라는 이름을 가지고 있다. 이때 액터 인스턴스 바로 아래에 있는 컴포넌트를 루트 컴포넌트라고 하며, 트랜스폼이 있고 다른 컴포넌트를 어태치할 수 있는 **'씬 컴포넌트'**만이 루트 컴포넌트가 될 수 있다. 이 말은 즉, 모든 액터는 반드시 트랜스폼을 가진다는 것을 의미한다.

루트 컴포넌트에 다른 컴포넌트를 어태치하면 새로운 컴포넌트는 루트의 자식으로 종속된다. 액터 배치 패널에서 캐릭터 액터를 추가하면 다음과 같은 컴포넌트 구조를 가지는 것을 확인할 수 있다.

 

CapsuleComponent라는 콜리전을 위한 기본 컴포넌트를 루트 컴포넌트로 두고, 스켈레탈(인간형) 메시와 애니메이션을 효과적으로 적용할 수 있는 CharacterMesh 컴포넌트를 어태치하였다. 두 컴포넌트를 동시에 가지고 있기 때문에 Character 액터는 애니메이션이 적용된 메시와 함께 충돌 처리 등의 물리 효과를 모두 가질 수 있다.

 

 

· 논-씬 컴포넌트

앞에서 루트 컴포넌트는 씬 컴포넌트라고 하였다. 반대로 논-씬 컴포넌트는 트랜스폼을 가지지 않는 컴포넌트라고 생각하면 된다. 앞서 살펴본 Character 액터의 CharacterMesh와 CapsuleComponent는 각각의 트랜스폼이 필요하다.

 

캐릭터의 메시가 어느 위치에, 어느 각도로, 얼마나 큰 크기를 가지는지 정보가 필요하지만, 캐릭터의 충돌 처리를 담당할 콜라이더가 반드시 캐릭터 메시와 정확히 같은 크기에 같은 위치를 가지고 있을 필요는 없다. 때로는 콜라이더가 조금 더 크거나 작을 수도 있고, 캐릭터 메시의 특정 부위에 닿았을 때만 충돌 처리를 해줄 필요도 있기 때문이다. 따라서 두 컴포넌트는 모두 개별 트랜스폼이 필요하다.

 

하지만 그렇지 않은 경우가 있다. 위에 삽입한 CharacterMovement 컴포넌트를 보면 걷기, 떨어지기, 헤엄지기 등 다양한 무브먼트 모드를 지원할 뿐, 그 컴포넌트 자체가 형태를 가지지 않는다. 이러한 컴포넌트를 '논-씬 컴포넌트'라 하며, 이는 루트 컴포넌트에 종속되지 않고 따로 취급된다.

 

 

 

액터 C++ 클래스

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

언리얼 에디터를 통해 Actor 클래스를 상속받은 클래스를 만들었을 때 기본적으로 추가되는 헤더파일들이다.

 

· CoreMinimal.h

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

 

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

 

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

 

 

· GameFramework/Actor.h

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

 

 

· MyActor.generated.h

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

 

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

 

 

· 클래스 선언부 규칙

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

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

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

 

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

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

 

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

 

 

· 클래스.cpp

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

AMyActor::AMyActor()
{
 	// 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 AMyActor::BeginPlay()
{
	Super::BeginPlay();

}

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

}

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

 

AMyActor()

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

생성자 내에 작성돼있는

PrimaryActorTick.bCanEverTick = true;

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

 

BeginPlay()

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

Super::BeginPlay();

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

// MyActor.h

virtual void BeginPlay() override;

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

 

Tick()

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

PrimaryActorTick.bCanEverTick = true;

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

 

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

위 함수들은 기본 액터 클래스가 제공하는 대표적인 함수들이다.