프로그래밍/Unreal 부트캠프

TIL 2025.01.21 기록

Rozentea 2025. 1. 21. 20:36

0. 개요


생각보다... 과제까지 하려면 시간이 넉넉하지 않은 것 같아서 최대한 빨리 강의를 듣고 정리하고, 이번주 주말간 과제를 진행해야겠다..

사실 과제를 이전에 만든 블루프린트 플랫포머 게임을 C++ 클래스로 변경해 만드는 작업을 진행해볼까 했었다.

하지만, 시간이 그리 충분하지 않다 느껴져 이전에 블루프린트로 구현했던 일부 기능들을 이번에는 C++로 구현해볼 생각이다.

 

음.. 우선.. 과제 내용은 C++ 클래스를 이용해 움직이는 발판과 회전하는 발판 만들기 등이고..

수업에서 더 나아가 다양한 함정, 기능을 만들어 보았는지, 리플렉션 지정자들을 얼마나 적절히 활용했는지, 랜덤 생성을 구현했는지

정도인 것 같다.

 

음.. 내용이 좀 많긴한데.. 우선 목요일까지 과제를 다 듣고, 간단한 레벨디자인(구상)을 한 뒤 바로 과제를 진행해야할 것 같다.

꼭 구현해보고 싶은건 역시.. 레이저 퍼즐인것 같다.

이번엔 C++로 구현 해보자.. 오히려 더 쉬울것 같다 ㅎㅎ

 

음.. 그러면 이번엔 팀에 마탄의 사수를 컨셉으로한 아케인 펑크 분위기의 로그라이크 게임을 제안해보고 싶었기 때문에 퍼즐 요소로 써먹기 딱 좋은 레이저 퍼즐을 C++로 언제든 붙여서 사용할 수 있도록 깔끔하게 짜보자..!

 

과제 생각은 여기까지하고, 오늘은 강의 2개를 들었다.

오늘도 궁금했던 내용, 몰랐던 내용이 많았어서 의미있는 공부를 할 수 있었다.

물론... 추가적으로 더 찾아보고, 더 사용해봐야하는 부분이 많지만.. ㅠ

오늘도 제 시간에 자긴 글렀따....

 

1. 강의 정리


1. Asset Migrate

에셋 이주는 프로젝트 에셋들의 연관 관계를 해치지 않고 다른 프로젝트로 옮기는 작업이다.

이런 기능을 제공하는 이유는 언리얼 엔진에서는 에셋들이 서로 레퍼런스를 참조해 연관되어 있는 경우가 많기 때문에 그 구조가 깨지면 소스들이 깨지는 경우가 많이 발생하기 때문이다. (특히 애니메이션)

 

⇒ 이전에 플랫포머 게임을 만들 때, 재질과 메쉬를 잘 옮겼어도 레퍼런스가 깨져 재질을 제대로 넣어주지 못하는 상황이 발생했던 경우가 이런 경우인 것 같다.

⇒  진짜 특히 애니메이션이 날라갔었따... 깃발이 었는데..

 

에셋 이주 장점

Migrate는 선택한 에셋 (또는 폴더)이 참조하고 있는 모든 종속 파일을 함께 복사해 주므로, 누락 없이 옮길 수 있습니다.

 

!! 주의할 점 !!

!!! 기존에 같은 이름의 폴더가 있다면 경로 충돌이 발생할 수 있으니 주의 !!!

말그대로 복사가 아니라 옮기는 것이기 때문에 마이그레이트하고 난 뒤의 원래 콘텐츠 폴더는 비어있게 된다.

 

현업에서는

실제 게임 개발 현장에서는 디자이너와 프로그래머가 하나의 프로젝트에서 함께 작업하는 경우가 더 많다.

 

2. 언리얼 엔진 AActor vs. UObject

두 클래스 다 엔진에서 매우 중요한 클래스 계층을 이루고 있으므로, 차이점을 확실히 알아야 한다.

UObject의 개념
  • 오브젝트란, 객체 지향에서는 객체를 의미한다. UObject 클래스는 언리얼 엔진의 모든 클래스의 뿌리 즉, 조상이되는 클래스이다. (모든 클래스의 최상위 부모이다.)
  • 스스로 월드에 배치될 수 없는 클래스다.
  • 주로 데이터나 로직만 담당한다.
    • ex) 플레이어 능력치(HP, Exp), AI 정보, 게임 설정 값, 임시 계산 로직 등
  • 화면에 보이지 않는 추상적인 부분을 처리하는 역할이라고 생각하면 좋다.
AActor의 개념
  • AActor는 UObject를 확장(상속)한 클래스로, 월드에 배치(Spawn) 할 수 있다.
  • 위치 (트랜스폼), 회전, 크기 등 공간적 정보를 가지고 있고, 여러 컴포넌트(메시, 파티클, 사운드 등)를 추가로 붙일 수 있다.
  • 실제 게임 세계에서 보이고 상호작용하는 캐릭터, 적 몬스터, 무기, 조명, 파티클 효과 등은 주로 AActor를 기반으로 제작한다.

 

+ 추가로 조사한 사항 +

 

 

3. 새로운 C++ 클래스 생성하기

툴(Tools) → 새 C++ 클래스 (New C++ Class)를 선택하면 된다.

일반 클래스 : 주로 사용하게 되는 일반적인 클래스들을 모아둔 것이다.

모든 클래스 : 엄청나게 많은 클래스들이 이미 구현되어 있는 것을 확인할 수 있다.

 

특히 모든 클래스에서 가장 상위인 오브젝트를 닫아보면, 모든 클래스들이 오브젝트 클래스를 상속받아 만들어졌다는 것을 확인할 수 있다.

 

 

 

 

 

 

새로운 C++ 클래스를 public으로 추가하게 되면, 우측의 사진 처럼 루트 프로젝트에 하위 폴더가 추가된다.

 

🌟 <클래스 타입 간 차이> 🌟

클래스 타입을 public으로 할 경우 해당 C++ 클래스의 헤더파일이 public 폴더에 생성되고, 어떤 모듈에서든 참조할 수 있게된다.

반면, private로 할 경우 헤더 파일이 private 폴더에 생성되고, 다른 모듈에서는 참조할 수 없게 된다. → 특정 로직이나 구현을 캡슐화해서 외부에 노출하고 싶지 않을 때 사용합니다.

 

규모가 커질수록 이러한 구조를 잘 활용하여 모듈 간 경계를 명확하게 하고, 코드 접근 범위를 정리하는 것이 중요하다..!! (내가 배우고 싶었던 모듈화 잘하는 것..!)

 

4. C++ 클래스를 생성할 때, 꼭 에디터를 사용해야 할까?

결론부터 이야기하자면, Visual Studio에서 직접 수동으로 생성해도 된다.

하지만, UCLASS() 등등 처럼 언리얼에서 사용하기 위한 세팅을 새로운 클래스에서 직접 해주어야 한다.

(내가 아는 것만 하더라도 위에 표시한 것들인데.. 에디터를 통해 만드는게 더 편할거 같다고 느껴진다..)

 

때문에 강사님께서는 에디터의 툴탭을 이용해 생성하는걸 추천한다고 하신다.

 

5. 생성한 C++ 클래스 삭제하기

콘텐츠 브라우저의 C++클래스 폴더에서 직접 삭제할 수 없다.

에초에 삭제할 수 있는 버튼이 없다..

 

🌟방법 🌟

1. 새로운 cpp와 h파일을 visual studio에서 지운다.

    여기까지는 실제 프로젝트 폴더에 저장되어있는 cpp와 h를 지운게 아니라 가상 폴더 구조에서만 삭제가 된 것이다.

2. 디스크의 폴더에 들어가서 직접 cpp와 h를 삭제해준다.

3. 에디터를 종료하고 다시 빌드를 진행해준다.

4. 잘 삭제가 되었는지 에디터를 열어 확인한다.

 

🌟주의할점🌟
만약 이 과정을 무시하고 언리얼 에디터에서만 Delete하거나, 솔루션에서 Remove하지 않고 폴더에서만 파일을 삭제하면 빌드 에러 혹은 클래스 미삭제 같은 문제가 발생할 확률이 늘어가니 때문에 왠만하면 준수해서 진행하는게 좋다.

 

6.  생성된 C++ 클래스 디폴트 코드 살펴보기

#pragma once

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

UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
	GENERATED_BODY()
	
public:	
	AItem();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

};

 

#pragma once
  • 이 헤더 파일이 여러 번 포함되더라도, 컴파일 시 단 한 번만 처리하도록 해주는 지시어다.
  • 과거에는 #ifndef ~ #define ~ #endif 방식을 사용했지만, 대부분의 최신 C++ 프로젝트에서 #pragma once를 권장한다.
#include "CoreMinimal.h"
  • 언리얼 엔진에서 자주 사용하는 기본 타입(FString, TArray 등)과 매크로(UE_LOG 등), 각종 유틸 함수들이 정의되어 있다.
#include "GameFramework/Actor.h"
  • AActor를 상속받기 위해 필요한 헤더 파일이다.
#include "Item.generated.h"
  • 언리얼 엔진의 리플렉션 시스템에서 필요한 코드를 자동 생성하기 위한 매크로다.
UCLASS()
  • 이 클래스를 언리얼 엔진의 리플렉션 시스템에서 인식하도록 하는 매크로다.
  • 언리얼 에디터에서 이 클래스를 블루프린트로 확장할 수 있게 하고, 에디터의 여러 기능과 연동하도록 한다.
AItem
SPARTAPROJECT_API
  • 이 클래스를 모듈 (지금 프로젝트에서는 SpartaProject) 외부로 Export하기 위한 매크로다.
  • DLL 등으로 빌드할 때 필요한 선언이다.
  • 나중에 빌드할 때 매우 중요한 매크로이다.
GENERATED_BODY() UCLASS()와 짝을 이루어, 엔진 리플렉션에 필요한 코드를 자동 생성해 주는 매크로입니다.
생성자 Actor 객체가 메모리에 생성될 때 한 번 호출됩니다. 월드에 배치되기 전 단계일 수도 있습니다.

리플렉션 시스템이란?

코드로 작성한 것들을 블루프린트로 볼 수 있도록 해주는 기능이다.

 

#include "Item.h" // 자기 자신과 짝이 되는 헤더를 가장 먼저 include해야 함.

// 생성자 구현부
AItem::AItem()
{
		PrimaryActorTick.bCanEverTick = true; // (이후 강의에서 배우는 내용)
}

// BeginPlay() 구현부
void AItem::BeginPlay()
{
		// 부모 클래스(AActor)의 BeginPlay()를 먼저 호출
		Super::BeginPlay();
}

// Tick() 구현부
void AItem::Tick(float DeltaTime)
{
		// 부모 클래스(AActor)의 Tick() 먼저 호출
		Super::Tick(DeltaTime);
}

 

#include "Item.h"
  • 🌟.cpp 파일에서는 가장 먼저, 짝이 되는 헤더 파일을 포함해야 합니다.🌟
Super::Tick(DeltaTime)
  • BeginPlay()나 Tick()에서 부모의 함수를 호출시켜주는 부분이다.
  • 함수 오버라이딩을 생각해보면 이렇게 하는 이유는 충분히 이해가 감.

 

7. 컴포넌트란?

<컴포넌트란?>

액터를 배치하면, 아무것도 보이지 않고, 위치도 변경하지 못한다.
보이지 않는 이유는 아직 3D메시를 설정하지 않았기 때문이고, 위치변경이 안되는 이유는 루트 컴포넌트를 지정해주지 않았기 때문이다.

컴포넌트란 언리얼 엔진에서 Actor가 어떤 역할을 하거나 특정 속성을 갖도록 만들어주는 부품 (파츠) 개념이다.

하나의 Actor가 여러 종류의 컴포넌트를 조합하여 다양한 기능을 구현할 수 있다.

예를 들어, Static Mesh Component + Audio Component + Collision Component를 모두 사용해, 충돌 시 소리가 나는 아이템을 만들 수도 있다.

 

8. Root Component와 Scene Component

루트 컴포넌트(Root Component)란?

모든 Actor는 최상위 컴포넌트루트 컴포넌트 (Root Component)를 가져야 하는데, Root Component는 액터의 트랜스폼 (위치,회전,크기)를 정의하며, 모든 하위 컴포넌트가 이를 기준으로 동작한다.

 

그렇다면, 어떤 컴포넌트를 루트 컴포넌트로 해야 할까?

단순히 사운드 컴포넌트만 붙는 액터처럼 하나만 붙는 경우 해당 컴포넌트를 루트 컴포넌트로 사용해도 된다.

하지만, 여러 컴포넌트들을 추가해 만드는 액터의 경우

일반적으로, Scene Component를 루트로 설정하여 액터의 트랜스폼을 관리하고, 그 아래에 다양한 컴포넌트를 계층적으로 붙인다.

 

씬 컴포넌트(Scene Component)란?

Scene Component모든 트랜스폼 속성을 가지는 기본 컴포넌트다.

Scene Component는 직접적인 시각적 출력 (3D 모델, 빛 등)을 가지지 않지만, 다른 컴포넌트들의 계층적 트랜스폼을 정의하는 기준점 역할을 한다.

즉, Transform에 대한 정보를 가지고 있고, 시각적인 출력은 없기 때문에 루트 컴포넌트로써는 최적화 되어있는 컴포넌트이다.

이는 언리얼 엔진에서도 권장하는 것이다.

 

9. Static Mesh Component

Static Mesh Component란?

애니메이션 없이 움직임이 없거나 단순 이동·회전만 하는 고정된 (Static) 3D 모델을 표시하는 컴포넌트다.
3D 모델을 표현하고, 물리 충돌과 관련된 기능도 제공한다.

해당 컴포넌트의 중요한 구성요소는 StaticMesh 에셋을 할당 받을 수 있는 것과 머티리얼을 추가해 모델의 표면의 질감을 표현할 수 있다.

 

Static Mesh

3D 모델 (정적 메쉬 데이터)를 정의합니다. 이는 모델 파일이며, 언리얼 엔진에서는 .uasset 파일로 관리된다.

 

Material

메쉬의 표면 시각적 속성을 정의합니다. 색상, 질감, 반사율, 투명도 등의 속성을 설정할 수 있습니다.
Static Mesh에는 여러 개의 머티리얼 슬롯이 있을 수 있습니다. 슬롯은 메쉬의 특정 부분에 다른 머티리얼을 적용할 수 있도록 합니다.

 

10.  코드로 컴포넌트 부착하기

#pragma once

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

UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
	GENERATED_BODY()

protected:
	USceneComponent* SceneRoot;
	UStaticMeshComponent* StaticMeshComp;
	
public:	
	AItem();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

};
#include "Item.h"

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;

	// CreateDefaultSubobject<>() => 컴포넌트 생성 함수
	SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
	// SetRootComponent => 루트 컴포넌트 지정 함수
	SetRootComponent(SceneRoot);

	// StaticMeshComponent 생성
	StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	// SetupAttachment => 컴포넌트를 자식으로 붙여주는 함수
	StaticMeshComp->SetupAttachment(SceneRoot);

	// StaticMesh와 Material 설정
	// 컨텐츠 드로어에서 레퍼런스를 복사해 그 주소를 넘겨주었다.
	// ConstructorHelpers::FObjectFinder<>는 디렉터리를 받아 오브젝트를 찾아주는 구조체이다..?
	static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Statue.SM_Statue"));
	// 에셋을 가져왔으면, 잘 가져와졌는지, Succeeded()를 이용해 확인해준다.
	if (MeshAsset.Succeeded())
	{
		// 성공했다면, StaticMeshComponent에 우리가 가져온 매시를 전달해준다.
		StaticMeshComp->SetStaticMesh(MeshAsset.Object);
	}

	static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Props/Materials/M_Statue.M_Statue"));
	static ConstructorHelpers::FObjectFinder<UMaterial> GlassAsset(TEXT("/Game/Resources/Props/Materials/M_StatueGlass.M_StatueGlass"));
	if (GlassAsset.Succeeded())
	{
		StaticMeshComp->SetMaterial(0, GlassAsset.Object);
	}
	if (MaterialAsset.Succeeded())
	{
		// 머티리얼은 사실 여러가지를 가져올 수도 있다.
		// 때문에 머티리얼을 지정해줄 때 몇 번째 인덱스에 지정해준다는 것도 함께 전달해야 한다.
		StaticMeshComp->SetMaterial(1, MaterialAsset.Object);
	}
}

 

CreateDefaultSubobject<T>(TEXT("")) Unreal Engine에서 컴포넌트를 생성하고 초기화할 때 사용하는 함수다.
"StaticMesh", "SceneRoot"는 각 컴포넌트의 식별 이름이 된다.
TEXT() TEXT 매크로는 문자열을 유니코드로 처리하기 위한 것으로, 언리얼 엔진 코드 표준에서 권장한다.
SetRootComponent(SceneRoot) 루트 컴포넌트를 SceneRoot로 설정합니다.
SetupAttachment(SceneRoot) StaticMeshComp를 SceneRoot에 부착 (Attach)한다.
StaticMeshComp는 SceneRoot의 하위 컴포넌트로 동작하며, SceneRoot의 트랜스폼 변화에 따라 움직인다.
ConstructorHelpers::FObjectFinder<T> Unreal Engine에서 특정 리소스를 경로 기반으로 로드하는 클래스
TEXT("/Game/Resources/Props/SM_Statue.SM_Statue") 리소스의 경로를 나타낸다.

리소스의 경로를 가져오려면 아래와 같이 해당하는 에셋을 우클릭하고 Copy Reference를 해서 붙여넣기를 한다.
단, /Game 여기서부터만 입력하면 되고, 앞에 경로는 삭제를 해준다.
(/Game은 Unreal Engine에서 프로젝트의 Content 폴더를 나타낸다.)

.Succeeded() 지정된 경로에서 리소스를 성공적으로 찾았는지 확인한다.
경로가 잘못되었거나 리소스 파일이 누락된 경우 실패를 반환한다.
SetStaticMesh(), SetMaterial() 성공적으로 로드된 Static Mesh를 StaticMeshComp에 설정한다.
로드된 Material을 StaticMeshComp의 특정 머티리얼 슬롯에 적용한다.
사용한 매시가 머티리얼 슬롯을 2개 갖기 때문에 각각 0, 1 인덱스의 머티리얼에 알맞은 머티리얼을 넣어주었다.

 

리소스 경로 쉽게 가져오는 법

 

11. 에디터에서 컴포넌트 확인하기

언리얼 에디터 Content Browser에서 C++ Classes → SpartaProject → Public 폴더 안에 Item 클래스를 레벨 뷰포트에 다시 배치해보니 잘 나오는 것을 확인할 수 있었다.

하지만 디테일 창을 보면, RootComponent만 보이고, StaticMeshComponent는 보이지 않는다.

 

이유는 에디터 상에서 Component들이 노출되도록 리플렉션 시스템에 등록하지 않았기 때문에 보이지 않는 것이다.

반면, RootComponent는 설정하지 않아도 기본적으로 리플렉션 시스템에 등록되어 Details 창에서 볼 수가 있다.

 

현재 Details 창에서는 Component가 보이지 않고 에디터 상에서 할당은 불가능하지만, 뷰포트 상에서 메시랑 머티리얼이 잘 할당되었다는 것을 확인할 수가 있다.

 

2. 마무리


원래는 수업을 듣고나면, 미니 과제들을 다 하려했었는데, 생각보다 시간이 부족했다.. ㅠ

TIL 작성시간이 더 늘어나게 된 원인인데.. 언리얼은 많이 안다뤄봤기 때문에 어쩔 수 없는 것 같다..

 

때문에 조금 다시 생각한게, 아에 강의를 최대한 빨리 듣고 정리해둔 뒤,

주차 과제를 진행하면서, 그간 못했던 일일 미니 과제의 내용을 포함해서 좀 더 다양한 기능을 가질 수 있도록 만들어볼까 한다.

 

음.. 사실 그러면 이번 과제만하더라도 하고 싶은게 많아질 것 같다.