언리얼에서 클래스가 생성되는 과정(Deep2Unreal)

Naezan·2024년 5월 1일
0

언리얼엔진

목록 보기
3/7
post-thumbnail

언리얼에서 C++클래스를 생성하면 기본적인 주석이 생성됩니다.

헤더파일(AMyActor.h)

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

#pragma once

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

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

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

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

};

소스파일(AMyActor.cpp)

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


#include "MyActor.h"

// Sets default values
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);

}

기본적인 주석들은 정말 간단한 정보에 대한 설명을 하고 있기 때문에 개발하는데 있어서 불필요하게 느껴질 수 있습니다.

이 주석들을 제거하려면 아주 간단한 작업을 해주면 되는데, 그전에 언리얼 엔진이 클래스에 주석을 어떻게 생성하는지 알아봅시다.

C++ 클래스는 어떻게 생성될까?

언리얼에서는 클래스를 생성할 때, 일반적으로 아래와 같은 방식을 이용하게 됩니다.

New C++ Class 클릭 1. New C++ Class 클릭

Actor 선택 후 Next 클릭 2. Actor 선택 후 Next 클릭

Create Class 클릭 3. Create Class 클릭

이 과정을 거치게 되면 HelloWorld 클래스가 Source파일에 자동으로 생성됩니다.
그럼 Create Class를 클릭했을 때 어떤 일이 일어나는 것일까요?

(Real)클래스가 생성되는 방법

이 부분에 대해 엄~청 깊게 이해하려면 언리얼의 Editor가 동작하는 방식을 이해할 필요가 있습니다만, 여기서는 다루지 않겠습니다.

다만 일반적으로 Editor 역시 엔진의 한 부분으로 여러분이 Editor에서 시도하는 모든 상호작용은 Editor의 소스코드로 동작한다고만 이해하고 있으면 됩니다.

언리얼의 모든 Editor의 UI정보는 엔진의 Editor폴더에 포함되어 있고 Create Class 역시 여기에 포함됩니다.

Create Class

Create Class 버튼은 SNewClassDialog 클래스(일종의 WidgetClass)에서 생성되어 관리됩니다.

SNewClassDialog의 생성자를 자세히 보면 ChildSlot으로 Create Class 버튼이 SNew메크로를 통해 생성되고 클릭 이벤트(FinishClicked)가 바인딩되어 있는 것을 알 수 있습니다.

Create Class를 클릭하면 FninishClicked()가 호출되는 것이죠.

FninishClicked()에는 다양한 처리과정이 포함되어 있지만 저희는 클래스의 생성에만 관심이 있기에 관련 부분만 보도록 합시다. 간단히 코드를 요약하면 아래와 같습니다.

void SNewClassDialog::FinishClicked()
{
	if (클래스가 블루프린트 일 때)
	{
		...
	}
	else
	{

		FString HeaderFilePath;
		FString CppFilePath;

		// Track the selected module name so we can default to this next time
		LastSelectedModuleName = SelectedModuleInfo->ModuleName;

		GameProjectUtils::EReloadStatus ReloadStatus;
		FText FailReason;
        
        // 말그대로 허용할 수 없는 헤더 이름
		const TSet<FString>& DisallowedHeaderNames = FSourceCodeNavigation::GetSourceFileDatabase().GetDisallowedHeaderNames();
        
        // GameProjectUtils::AddCodeToProject 정적 함수를 호출하여 소스코드를 생성
		const GameProjectUtils::EAddCodeToProjectResult AddCodeResult = GameProjectUtils::AddCodeToProject(NewClassName, NewClassPath, *SelectedModuleInfo, ParentClassInfo, DisallowedHeaderNames, HeaderFilePath, CppFilePath, FailReason, ReloadStatus);
		if (AddCodeResult == GameProjectUtils::EAddCodeToProjectResult::Succeeded)
		{
			...
	}
}

GameProjectUtils::AddCodeToProject()의 정적 함수를 호출하여 소스코드 생성 과정을 수행하고
AddCodeToProject() 함수는 GameProjectUtils캡슐화된 AddCodeToProject_Internal()을 호출합니다.

이때 NewClassName, NewClassPath, SelectedModuleInfo, ParentClassInfo와 같은 정보들은 에디터에서 입력한 정보로 이미 셋팅되어 있는 정보들입니다. 이 정보를 기반으로 소스코드를 생성하게 됩니다.

AddCodeToProject_Internal()에서는 경로의 유효성 체크, 현재 소스파일이 있는지 여부에 따라 소스코드 생성을 위한 기본 파일 생성, cpp와 h파일 생성, 언리얼 빌드툴 실행, 소스컨트롤 마킹작업(만약 켜져있다면), 컴파일 중입니다.. 잠시만 기다리세요... 문구 띄우기, 라이브 코드 컴파일 실행 혹은 UBT로 리컴파일 실행(HotReload) 등과 같은 다양한 작업을 진행합니다.

헤더파일의 생성

정말 많은 로직을 수행하지만 여기선 헤더파일이 생성되는 부분을 집중적 분석하고자 합니다.

GameProjectUtils::EAddCodeToProjectResult GameProjectUtils::AddCodeToProject_Internal(const FString& NewClassName, const FString& NewClassPath, const FModuleContextInfo& ModuleInfo, const FNewClassInfo ParentClassInfo, const TSet<FString>& DisallowedHeaderNames, FString& OutHeaderFilePath, FString& OutCppFilePath, FText& OutFailReason, EReloadStatus& OutReloadStatus)
{
	...

	// Class Header File
	const FString NewHeaderFilename = NewHeaderPath / ParentClassInfo.GetHeaderFilename(NewClassName);
	{
		FString UnusedSyncLocation;
		TArray<FString> ClassSpecifiers;

		// 인터페이스면 UCLASS 매크로에 MinimalAPI 지정자 삽입
		// Set UCLASS() specifiers based on parent class type. Currently, only UInterface uses this.
		if (ParentClassInfo.ClassType == FNewClassInfo::EClassType::UInterface)
		{
			ClassSpecifiers.Add(TEXT("MinimalAPI"));
		}

		// 헤더파일 생성 로직
		if ( GenerateClassHeaderFile(NewHeaderFilename, CleanClassName, ParentClassInfo, ClassSpecifiers, TEXT(""), TEXT(""), UnusedSyncLocation, ModuleInfo, false, OutFailReason) )
		{
			CreatedFiles.Add(NewHeaderFilename);
		}
        //실패하면 파일 삭제, 그 이유는 GenerateClassHeaderFile가 파일을 생성하는 로직이 내부에 있기 때문
		else
		{
			DeleteCreatedFiles(NewHeaderPath, CreatedFiles);
			return EAddCodeToProjectResult::FailedToAddCode;
		}
	}
    
    ...


	return EAddCodeToProjectResult::Succeeded;
}

다른 로직은 제쳐두고 GenerateClassHeaderFile() 함수가 헤더 파일을 생성한다는 것을 알 수 있습니다.

템플릿을 이용한 소스코드 생성

GenerateClassHeaderFile()는 가장 먼저 템플릿의 루트경로(템플릿은 .template형태의 파일입니다) 를 가져옵니다.
(템플릿이 어디에 있는 파일인지는 맨 아래쪽에서 다루고 있으니 천천히 스크롤을 내리시면서 보시면 됩니다.)

그리고 GetHeaderTemplateFilename()함수를 통해 템플릿의 파일 이름을 부모 클래스 타입정보를 기반(Actor면 Actor Template)으로 가져오고 ReadTemplateFile()을 호출하여 템플릿 파일의 정보를 Template(매개변수 Out)에 읽어들입니다.

(살짝 코드 아랫부분을 보면 클래스의 경로 바로 옆에 class HelloWorld_API처럼 매크로를 넣어주는 것도 확인할 수 있습니다.)

  • GetHeaderTemplateFilename()는 액터의 경우 ActorClass.h.template의 텍스트를 반환합니다.

  • ReadTemplateFile()에선 TemplateFileName을 매개변수로 템플릿 파일에 있는 모든 문자를 Template(FString)으로 가져옵니다.

디버깅을 통해 보면 아래와 같이 변수가 셋팅되어집니다.

FullFileName FullFileName에 입력된 템플릿 파일 경로

FullFileName FullFileName의 파일을 OutFileContents로 읽음

마지막으로 읽어들인 템플릿의 전처리정보?를 알맞는 텍스트로 변환한 뒤 WriteOutputFile() 함수를 호출하여 헤더파일의 이름으로 지정된 경로에 변환된 텍스트를 저장(파일 입출력)합니다.

ActorClass.h.template

자 이제 헤더파일이 템플릿에 맞게 생성되는 것을 알았으니 진짜 템플릿이 어디에 있는지 알아봐야하겠죠.

템플릿 파일들의 경로는 엔진이 설치된 경로인 UE_??\Engine\Content\Editor\Templates 에 위치해 있습니다.

그리고 파일을 열어보면 아래와 같이 구성되어 있습니다.

이제 더러운 주석들을 제거할 차례입니다.
아래와 같이 주석들을 지워줍니다.

%COPYRIGHT_LINE%

#pragma once

#include "CoreMinimal.h"
%BASE_CLASS_INCLUDE_DIRECTIVE%
#include "%UNPREFIXED_CLASS_NAME%.generated.h"

UCLASS(%UCLASS_SPECIFIER_LIST%)
class %CLASS_MODULE_API_MACRO%%PREFIXED_CLASS_NAME% : public %PREFIXED_BASE_CLASS_NAME%
{
	GENERATED_BODY()
	
public:	
	%PREFIXED_CLASS_NAME%();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

	%CLASS_FUNCTION_DECLARATIONS%
	%CLASS_PROPERTIES%
};

이제 클래스를 생성하면 정말 깔끔해진 클래스를 볼 수 있습니다.

디버깅을 통한 최종 결과 시각화(Text Visualizer)

디버깅을 통해서 시각화를 해보면 Template에 있던 정보들이

FinalOutput에서는 아래와 같이 정리되는 것을 알 수 있습니다.

가볍게 마무리하며

이 글을 가벼운 생각으로 보고자 들어오신 분들은 적잖은 당황을 하셨을거라고 생각합니다.
단순히 주석을 제거하는데 왜 내부 소스코드까지다 쑤셔보면서 마지막에 간단한 팁으로 마무리 하는 것인지..

사실 이유는 딱히 없습니다. Actorclass.h.template가 어떻게 동작하는 것이지?라는 궁금증에서 시작한 삽질이죠.. 그리고 이렇게 공부하는게 더 재밌게 느껴져서입니다.

삽질하며 하루를 마무리한 게임개발자 내잔이였습니다. 긴글 읽어주셔서 감사합니다.

profile
게임 개발자

0개의 댓글

관련 채용 정보