6 minute read

‘이득우의 언리얼 c++ 게임 개발의 정석’ 책 및 언리얼엔진5 문서를 참고하여 작성한 포스트입니다.

언리얼 콘텐츠의 구성 요소

월드

뷰포트 윈도우에 보이는 가상 공간. 다음과 같은 요소를 제공한다.
툴바->세팅->월드 세팅에서 확인이 가능하다.

  • 공간(space)
    • 가상 세계를 구성하는 3차원의 영역
    • 물체는 월드의 영역 어딘가에 반드시 존재해야 하는데, 이를 위해 transform이라는 구조체를 제공한다.
  • 시간
  • 물리
  • 렌더링

액터(Actor)

언리얼 엔진에서 콘텐츠를 구성하는 최소 단위의 물체.
액터들의 목록은 월드 아웃라이너 윈도우에서 확인이 가능하며, 뷰포트에서 선택한 액터의 속성은 디테일 윈도우를 통해 볼 수 있다.

액터는 다음과 같은 요소로 구성된다.

  • 이름
  • 유형: 액터의 역할. 액터의 클래스 이름
  • 트랜스폼
  • 프로퍼티: 액터에 설정된 속성 값. 디테일 윈도우에서 확인 및 편집이 가능하다.
  • 게임 로직: 액터에 특정 상황이 발생할 때 이에 대응할 구체적인 행동을 명령하기 위한 프로그래밍 코드. 블루프린트와 c++이 있음.

레벨(Level)

게임 개발 관점에서 레벨은 ‘플레이어에게 주어지는 스테이지’ 이다.

  • 언리얼 엔진에서의 레벨은 월드에 배치된 엑터들의 집합이다.
  • 액터를 이동하고 붙이고 속성을 설정하는 등의 다양한 행위는 결국 플레이어에게 제공할 스테이지를 조립하는 일이라고 볼 수 있다. 이러한 활동을 ‘레벨을 설계한다’라고 표현한다.
  • 설계된 레벨을 테스트하기 위해 툴바의 플레이 버튼을 이용할 수 있다. 플레이 버튼을 누르면 기존 레벨을 구성하는 액터에 추가해, 접속한 플레이어에 관련된 액터들이 새롭게 생성된다. 여기서 기존의 액터들은 흰색, 새롭게 생성되는 액터들은 노란색으로 표시된다.

컴포넌트(Component)

  • 게임에서 액터의 기능은 크게 1. 시각적 기능, 2. 물리적 기능, 3. 움직임 으로 나눌 수 있다.

언리얼 엔진은 각 기능을 규격화하고 액터가 이들을 조합할 수 있도록 설계했는데, 이러한 규격화된 기능을 컴포넌트라고 한다.

주요 컴포넌트는 다음과 같다.

  • StaticMesh Component
    • 애니메이션이 없는 모델링 에셋인 스태틱메시를 사용해 시각적인 기능과 물리적인 기능을 제공하는 모듈이다. 주로 배경 물체에 사용된다.
  • SkeletalMesh Component
    • 애니메이션 정보가 있는 모델링 에셋인 스켈레탈 메시를 사용해 관련 기능들을 제공한다. 주로 캐릭터에 사용된다.
  • Collision Component
    • 물리적 기능을 설정하기 위한 모듈
  • Camera Component
    • 가상 세계에서 보여지는 현재 상황을 플레이어의 모니터 화면에 출력해주는 기능
  • Audio Component
    • 소리
  • Particle System Component
    • 이펙트 관련
  • Light Component
    • 광원 효과
  • Movement Component
    • 물체에 특정한 움직임을 부여하는 기능

컴포넌트들에 종류마다 독특한 속성들이 있으며, 속성이니 디테일 윈도우에서 확인이 가능하다.

액터는 여러 컴포넌트를 가질 수 있으나, 그중에서 하나의 컴포넌트를 대표로 반드시 지정해야 한다.




액터의 설계

분수대 제작

분수대 액터는 분수대 구조물의 비주얼과 충돌을 담당할 스태틱메시 컴포넌트와 물의 비주얼을 담당할 스태틱메시 컴포넌트로 구성될 것이다.

  • c++에서 액터가 두 개의 스태틱메시 컴포넌트를 가지려면 분수대 액터의 멤버 변수로 두 개의 UStaticMeshComponent 클래스의 포인터를 선언해 주어야 한다.

  • 빈 액터를 만들고 헤더파일에 선언된 CoreMinimal.h 파일을 EngineMinimal.h 파일로 변경해 UStaticMeshComponent 클래스 정보를 참조할 수 있도록 한다.

CoreMinimal.h 에는 오브젝트가 동작할 수 있는 최소 기능만 선언되어 있다. 콘텐츠 제작에는 다양한 엔진 기능이 필요하므로 EngineMinimal.h로 바꾸어 코드를 작성한다.

fountain.h

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

#pragma once

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

UCLASS()
class ARENABATTLE_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;

	UPROPERTY()
	UStaticMeshComponent *Body;

	UPROPERTY()
	UStaticMeshComponent *Water;

};

UStaticMeshComponent 클래스를 포인터로 선언했으므로 앞으로 구현부에서는 메모리를 동적으로 할당해 대입한다. 언리얼 엔진은 실행 환경(Runtime)을 통해 객체가 더 이상 사용되지 않으면 할당된 메모리를 자동으로 소멸시키는 기능을 제공한다.

언리얼 실행 환경이 객체를 자동으로 관리하게 만들려면 코드에서 UPROPERTY 매크로 를 꼭!! 선언해야 한다.

모든 객체가 이 매크로로 메모리 관리가 가능한 것은 아니다. 언리얼 오브젝트라는 객체만 사용 가능하다.

어떤 c++ 클래스가 언리얼 오브젝트 클래스가 되려면 클래스 선언에 언리얼 엔진이 정의한 특별한 매크로와 규칙을 부여해야 한다.

  • 클래스 선언 매크로
    • 클래스 선언의 윗줄에 UCLASS라는 매크로를 선언하고 클래스 내부에는 GENERATED_BODY 매크로를 선언한다.
  • 클래스 이름 접두사:
    • 규칙에 맞는 접두사가 붙어야 한다. 크게 U와 A가 있는데, A는 액터 클래스에 사용하고 U는 그 외의 클래스에 사용된다.
    • 위 코드의 경우 액터이므로 AFountain이라는 클래스 이름을 가지고, 구성요소인 스태틱메시 컴포넌트는 액터가 아니므로 UStaticMeshComponent라는 클래스 이름을 가진다.
  • generated.h 헤더 파일
    • 소스 코드 컴파일하기 전에 언리얼 엔진은 Unreal Header Tool이라는 도구를 사용해 클래스 선언을 분석하고 언리얼 실행 환경에 필요한 부가 정보를 별도의 파일에 생성한다. 이 툴에 의해 자동으로 생성되는 부가 파일이 generated.h 파일이다. 반드시 선언해주기!
  • 외부 모듈에의 공개 여부
    • _declspec(dllexport)은 DLL 내 클래스 정보를 외부에 공개할 지 결정하는 키워드이다. 언리얼 엔진에서 이 키워드를 사용하려면 ‘모듈명_API’라는 키워드를 클래스 선언 앞에 추가해야 한다. 그래야 다른 모듈에서 해당 객체에 접근이 가능하다.
    • 위에선 ARENABATTLE_API


Fountain.cpp

액터의 구축은 클래스의 생성자 코드에서 진행된다. 언리얼 엔진은 new가 아닌 CreateDefaultSubobject API라는 특별한 함수를 제공한다.

// 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;

	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
	Water = CreateDefaultSubobject<UStaticMeshComponent>(TeXT("WATER"));
	
	RootComponent = Body;
	Water->SetupAttachment(Body);
}

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

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

}

CreateDefaultSubobject API에 사용하는 문자열 값은 액터에 속한 컴포넌트를 구별하기 위한 Hash 값 생성에 사용된다. 어떤 값이어도 상관 없지만 여기서는 2바이트 문자열 체계를 동일하게 유지시키는 TEXT 매크로를 사용한다.

루트 컴포넌트를 Body 로 지정하였고, Water 는 자식이 되게 했다.

Fountain 액터를 에디터의 뷰포트로 드래그 해 액터를 생성할 수 있다. 아직은 분수대가 보이지 않지만 스태틱메시 컴포넌트가 생성된 것은 디테일 윈도우에서 확인이 가능하다.


액터와 에디터 연동

디테일 윈도우에서 컴포넌트 속성을 편집하기 위해서는 UPROPERTY 매크로 안에 VisibleAnywhere 키워드를 추가해 주어야 한다.

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Body;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Water;

액터 클릭한 뒤, w 키를 눌러 이동 모드로 변경 후 end를 누르면 바닥까지 내려가서 붙음!

액터 기능의 확장

기본 위치 값 변경

SetRelativeLocation을 이용하면 컴포넌트의 기본 위치 값을 변경할 수 있다. 변경할 위치 값은 언리얼 엔진이 제공하는 구조체 FVector를 사용해 전달한다.

// Sets default values
AFountain::AFountain()
{
	...

	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 135.0f));
}

지정된 좌표가 기본값이 된다? 기본값으로 돌아가는 버튼 클릭하면 이 좌표로 이동한다?

조명과 이펙트 기능 추가

//Fountain.h
//public:
	UPROPERTY(VisibleAnywhere)
	UPointLightComponent *Light;

	UPROPERTY(VisibleAnywhere)
	UParticleSystemComponent *Splash;
//
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;

	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WATER"));
	Light = CreateDefaultSubobject<UPointLightComponent>(TEXT("LIGHT"));
	Splash = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("SPLASH"));

	RootComponent = Body;
	Water->SetupAttachment(Body);
	Light->SetupAttachment(Body);
	Splash->SetupAttachment(Body);

	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 135.0f));
	Light->SetRelativeLocation(FVector(0.0f, 0.0f, 195.0f));
	Splash->SetRelativeLocation(FVector(0.0f, 0.0f, 195.0f));
}
  • 에디터로 돌아가 fountain에 추가된 컴포넌트에 light와 splash(이펙트)를 설정해주면 된다.



객체 유형과 값 유형

  • 언리얼 오브젝트의 속성 값은 객체를 관리하는 객체 유형값을 관리하는 값 유형으로 나뉜다.
  • 언리얼 오브젝트 클래스의 포인터는 대표적인 객체 유형이다.

값 유형

  • 바이트: uint8
  • 정수: int32
  • 실수: float
  • 문자열: FString, FName
  • 구조체: FVector, FRotator, FTransform

  • 값 유형으로 클래스 멤버 변수를 선언하고 UPROPERTY 매크로를 설정해주면 매크로 선언만으로도 미리 예약된 기본값이 지정된다.
  • 정수면 0!
  • 아래 내용을 추가해준다.
//fountain.h
...
public:
...
	UPROPERTY(VisibleAnywhere)
	int32 ID;
  • 그러면 에디터의 액터 속성에 ID 속성이 보인다고 한다…(왜 안보이지? - 전체 탭에서 검색해보니 있길래 찾아보니 맨 아래에 나온다..)
  • 이 ID 값은 수정이 불가능하고 볼 수 만 있다.
  • 값 유형과 객체 유형 모두 ```VisibleAnywhere``` 키워드를 사용하면 해당 속성의 데이터를 변경할 수 없다.
  • 하지만 객체 유형의 경우 객체에 속한 속성들이 있는데, 에디터가 편리하게 이들을 보여주고 편집할 수 있게 해주기 때문에,
  • 위의 경우에는 스태틱 메시 설정 등 편집이 가능했던 것이다.
  • 속성의 데이터를 변경해주고 싶다면, EditAnyWhere 키워드를 사용해주면 된다.
  • 그리고 UPROPERTY 매크로 내에 Category=분류명 규칙으로 키워드를 추가하면, 지정한 분류에서 속성 값을 관리할 수 있다.
  • 다음과 같이 수정해보자.
UPROPERTY(EditAnywhere, Category=ID)
int32 ID;



에셋의 지정

  • fountain 관렬 에셋이 자동으로 로딩되도록 c++ 코드에 기능을 추가해보자
  • 언리얼 엔진에서 사용하는 모든 에셋은 레벨 에셋을 제외하고 모두 ```uasset```이라는 동일한 확장자를 가진다.
  • 에디터에서 각 에셋에 마우스를 올려보면 각 에셋이 어떤 데이터를 품고 있는 지 알 수 있다.
  • c++ 코드에서 해당 에셋을 불러들이려면 에셋의 고유한 키 값을 파악하고,
  • 에셋을 관리하는 시스템에 키 값으로 질의를 던져 에셋의 포인터를 가져와야 한다!
  • 언리얼 엔진에서는 키 값으로 경로 값을 사용한다. 이 또한 마우스를 올려두면 확인 가능하다.
  • 에셋 우클릭 해서 경로 복사 눌러도 되지만,
  • 에셋 클릭하고 컨트롤 씨 하는게 훨 편하다!
  • 복사한 경로는 다음과 같은 규칙을 가지고 있다.
    • {오브젝트 타입}'{폴더명}/{파일명}.{에셋명}'
      • 오브젝트 타입: 에셋의 타입을 명시적으로 지정
      • 폴더명/파일명: 물리적 디스크에 위치한 에셋의 경로 정보
      • 에셋명: 에디터에서 보여지는 에셋의 이름. 중복될 수 있음.
    • 오브젝트 타입 정보와 작은 따옴표는 제거해도 된다.
  • 경로 정보를 사용해 코드에서 에셋을 불러오려면,
  • 먼저, ConstructorHelpers 클래스의 FObjectFinder를 사용해 변수를 선언하고 이 변수에 경로 값을 전달한다.
  • 그러고나서 변수의 Object 멤버 값을 사용해 에셋에 대한 포인터를 가져온다.
  • 생성자 부분에 다음을 추가해보자.
ConstructorHelpers::FObjectFinder<UStaticMesh>
	SM_BODY(TEXT("/Game/InfinityBladeGrassLands/Environments/Plains/Env_Plains_Ruins/
				StaticMesh/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01"));
if(SM_BODY.Succeeded())
{
	Body->SetStaticMesh(SM_BODY.Object);
}
  • 언리얼 에디터의 콘텐츠 드로어 내의 fountain 액터의 썸네일 아이콘이 바뀌었다.
  • 이 액터를 뷰포트로 드래그해 생성하면 스태틱메시가 반영된 fountain이 생성될 것이다.
  • 에셋의 경로 정보는 게임 실행 중에 바뀔일이 없으므로 static으로 선언해 처음 한 번만 초기화하는 것이 바람직하다.
  • 물과 이펙트 에셋도 동일하게 해보자.
static ConstructorHelpers::FObjectFinder<UParticleSystem>
	SM_SPLASH(TEXT("/Script/Engine.ParticleSystem'/Game/InfinityBladeGrassLands/
		Effects/FX_Ambient/Water/P_Water_Fountain_Splash_Base_01.P_Water_Fountain_Splash_Base_01'"));
if(SM_SPLASH.Succeeded())
{
	Splash->SetTemplate(SM_SPLASH.Object);
}

static ConstructorHelpers::FObjectFinder<UStaticMesh>
	SM_WATER(TEXT("/Script/Engine.StaticMesh'/Game/InfinityBladeGrassLands/
			Effects/FX_Meshes/Env/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
if(SM_WATER.Succeeded())
{
	Water->SetStaticMesh(SM_WATER.Object);
}

Leave a comment