TIL_100 : 모듈-플러그인-빌드(UBT/UHT) (2), UPROPERTY

김펭귄·2026년 1월 21일

Today What I Learned (TIL)

목록 보기
100/139

1. 언리얼 빌드 순서

1.1. UBT (Unreal Build Tool)

  • 빌드를 시작하면, 먼저 UBT가 모든 .uplugin.uproject 파일을 스캔하여 모든 모듈을 파악

  • 각 모듈의 Build.cs파일을 읽고 의존성을 파악하여 방향 그래프를 구성함

  • 방향 그래프를 통해 순환 의존성을 체크함 Circular dependency detected between modules 에러 확인

ModuleA → ModuleB → ModuleC → ModuleA // 순환 발생
-> 모듈 A를 빌드하려면 B에 의존하니 B를 먼저해야하고, 그럼 C를 먼저 해야하는데 마찬가지로 A를 더 먼저해야해서 에러발생

  • 해결방법 : 공통 부분을 별도 모듈로 분리, 인터페이스 모듈을 만들어 의존성 역전
    예) A와 B가 직접 서로를 보게 하지 말고, 둘 다 알고 있는 'C(인터페이스)'를 만들어서 서로 C를 통해서 대화하게 함
  • 그리고 방향 그래프를 위상정렬을 통해 빌드 순서를 결정함.
    (아무한테도 의존하지 않는 기초 모듈 먼저 빌드)
빌드 순서:
1. Core (의존성 없음)
2. CoreUObject (Core에 의존)
3. Engine (Core, CoreUObject에 의존)
4. GameplayAbilities (Engine에 의존)
5. MyGame (위의 모든 것에 의존)
  • 그리고 각 모듈의 include path를 최종 확정함

    • UBT는 위에서 만든 의존성 지도를 바탕으로, MyGame이 Engine을 의존한다면 Engine의 Public 폴더 주소를 MyGame의 검색 경로에 자동으로 추가

    • 덕분에 복잡한 전체 경로를 안 써도 파일 이름만으로 불러올 수 있게 됨

  • 마지막으로 각 파일이 UHT(Unreal Header Tool) 처리가 필요한지 확인

1.2. UHT (Unreal Header Tool)

  • UBT가 소스 코드를 훑다가 언리얼 매크로(UCLASS 등)가 포함된 헤더 파일을 발견하면 UHT를 실행시킴

  • UCLASS, UPROPERTY 등 매크로를 읽고, 컴파일러가 이해할 수 있는 순수 C++ 코드를 작성하여 .generated.h.gen.cpp를 생성

  • .generated.h : 해당 클래스의 선언부에 있는 GENERATED_BODY() 매크로를 이 파일의 내용으로 치환

  • .gen.cpp : 리플렉션 정보(클래스 이름, 변수 타입 등)를 엔진 데이터베이스에 등록하는 실제 로직이 들어있다

  • 생성된 파일은 Intermediate 폴더에 생김. 그래서 중간에 빌드 꼬이면 이 폴더를 삭제 했던 것

  • 컴파일러가 읽을 수 있는 이 파일들을 생성함으로써, 순수 C++에선 불가능했던 GC/Reflection 등이 가능해짐

언리얼이 정적 리플렉션을 선택한 이유

  • C#, java같은 경우는 리플렉션을 위해 런타임에 자기를 조회함.
    하지만, 런타임에 조회하기 위해 별도 메모리공간도 필요하고 조회하는데 시간이 걸리는 오버헤드가 발생

  • 언리얼은 compile 이전에 UHT가 metadata를 생성하고 이것과 소스코드 다 함께 컴파일 해서 실행파일에 들어가기 때문에 런타임에 조회해도 성능이 아주 우수한 것

  • UHT에 의해 리플렉션이 가능해졌고, 에디터에서 블루프린트와 연동도 가능해지며, GC도 객체관리를 할 수 있게된다

1.3. 그 이후 빌드 파이프라인

  • 이후 컴파일러에 의해 컴파일이 시작되고, 링킹도 일어나며 빌드가 완료된다
─────────────────────────────────────────────────────────────
  1. UBT (UnrealBuildTool) 실행                               
─────────────────────────────────────────────────────────────
  2. Build.cs / Target.cs 파싱                               
     └─ 모든 모듈의 빌드 설정 수집                            
─────────────────────────────────────────────────────────────
  3. 모듈 의존성 그래프 생성                                  
     └─ 빌드 순서 결정, 순환 의존성 체크                       
─────────────────────────────────────────────────────────────
  4. UHT (UnrealHeaderTool) 실행                             
     ├─ 모든 헤더 파일 스캔                                  
     ├─ UCLASS/UPROPERTY 등 매크로 파싱                       
     └─ .generated.h / .gen.cpp 파일 생성                    
─────────────────────────────────────────────────────────────
  5. C++ 컴파일 시작                                            
─────────────────────────────────────────────────────────────
  6. 링킹                                                    
     ├─ Modular: 각 모듈별 DLL 생성                          
     └─ Monolithic: 하나의 실행 파일 생성                     
─────────────────────────────────────────────────────────────

1.4. 모듈 로딩 순서 자세히

  • 위에서 정리했듯이 의존성이 가장 낮고 기본타입의 모듈부터 로드함

    1. Core계열 모듈 (Core, CoreUObject)

    2. Engine 계열 모듈 (Engine, Renderer, Physics 등)

    3. Plugin 모듈 (Loading Phase에 따라 순서대로 로드)

    4. Project 모듈 (게임 모듈)

    5. Editor 모듈 (UnrealEd, LevelEditor)

모듈 인터페이스

// MyGameModule.h
#pragma once

#include "Modules/ModuleManager.h"

class FMyGameModule : public IModuleInterface
{
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
    static inline FMyGameModule& Get()
    {
        return FModuleManager::LoadModuleChecked<FMyGameModule>("MyGame");
    }

    static inline bool IsAvailable()
    {
        return FModuleManager::Get().IsModuleLoaded("MyGame");
    }
};
  • StartupModule : 모듈 로드 시 호출

  • ShutdownModule : 모듈 언로드 시 호출

  • Get : 모듈 접근 헬퍼

// MyGameModule.cpp
#include "MyGameModule.h"

#define LOCTEXT_NAMESPACE "FMyGameModule"

void FMyGameModule::StartupModule()
{
    UE_LOG(LogTemp, Log, TEXT("모듈 시작"));
}

void FMyGameModule::ShutdownModule()
{
    UE_LOG(LogTemp, Log, TEXT("MyGame 모듈 종료"));
    // 리소스 정리
}

#undef LOCTEXT_NAMESPACE

// Primary Game Module 등록
IMPLEMENT_PRIMARY_GAME_MODULE(FMyGameModule, MyGame, "MyGame");

// 일반 모듈인 경우
// IMPLEMENT_MODULE(FMyGameModule, MyGame)
  • StartupModule에서 가능한 작업
    • 커스텀 콘솔 명령어 등록
    • 에셋 타입 등록
    • 슬레이트 스타일 등록
    • 서브시스템 초기화
  • 매크로로 모듈을 엔진에 등록

1.5. Plugin 로딩 순서 자세히

  • .uplugin의 LoadingPhase와 플러그인 간 의존성에 따라 로드

EarliestPossible → PostConfigInit → PreDefault → Default → PostDefault → PostEngineInit

  • 이제 모듈, 플러그인까지 로딩 되었으면, UHT에 의해 리플렉션 정보가 등록된다(gen.cpp)

2. UPROPERTY 지정자

UPROPERTY( /* Specifiers... */)
  • UPROPERTY의 지정자는 크게 두 종류로 구분할 수 있다. 플래그와 메타데이터

2.1. Flags

UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated)
float Health;
  • 런타임에 필요한 정보

  • Flag들은 64비트 정수 하나에 비트로 저장되어, 런타임에 아주 빠르게 조회 가능

  • 아래 코드처럼 플래그 있는지 확인 가능

FProperty* Prop = ...;
if (Prop->HasAnyPropertyFlags(CPF_BlueprintReadOnly))
{
    // BlueprintReadOnly 플래그 존재
}

2.2. Metadata

UPROPERTY(/**/ meta=(DisplayName="체력", ClampMin="0", ClampMax="100"))
float Health;
  • 언리얼 에디터에서만 필요한 에디터 전용 정보

  • 따라서 Shipping 빌드에선 제거되며, 게임 로직에서도 Metadata에 접근 불가능

  • Metadata는 TMap<FName, FString> 형태로 저장

2.3. 주요 Specifiers

  • 에디터 노출

    • EditAnywhere : 블루프린트 CDO와 인스턴스 모두에서 편집 가능

    • EditDefaultsOnly : 블루프린트 CDO에서만 편집 가능

    • EditInstanceOnly : 레벨에 배치된 인스턴스에서만 편집 가능

    • VisibleAnywhere : 보이지만 편집 불가

    • VisibleDefaultsOnly : CDO에서만 보임, 편집 불가

    • VisibleInstanceOnly : 인스턴스에서만 보임, 편집 불가

  • 블루프린트 노출

    • BlueprintReadOnly : 블루프린트에서 읽기만 가능

    • BlueprintReadWrite : 블루프린트에서 읽기/쓰기 가능

    • BlueprintGetter=FuncName : 커스텀 getter 함수 지정

    • BlueprintSetter=FuncName : 커스텀 setter 함수 지정

  • 네트워크 동기화

    • Replicated : 서버→클라이언트 복제

    • ReplicatedUsing=FuncName : 복제 시 콜백 함수 호출

    • NotReplicated : 명시적으로 복제 안 함

  • 직렬화 : 메모리에 있는 분산된 데이터(객체)를 파일로 저장하거나 네트워크로 전송 가능하도록 '데이터 라인(Stream)'으로 변환. 저장/로드, 네트워크 등에 사용됨

    • Transient : 저장/로드 안 함

    • DuplicateTransient : 저장은 되나, 복제 시 값 복사 안 하고 0으로 초기화

    • SaveGame : 세이브 게임에 포함

    • SkipSerialization : 직렬화 건너뜀

  • 객체(Object) 안에 또 다른 객체를 담을 때

    • Instanced : 객체를 '참조'하는 게 아니라, 내부에서 새로 생성해서 소유

    • Export : 객체를 다른 곳으로 복사하거나 파일로 내보낼 때, 단순히 주소정보만 보내는게 아니라 데이터 전체를 통째로 서브오브젝트로 묶어 내보냄

    • NoClear : 에디터에서 None으로 설정 불가

3. 느낀 점

  • 이렇듯 런타임 메타데이터를 만들어서 리플렉션이 가능해졌고, 리플렉션을 통해 블루프린트 사용/네트워크/런타임 조회/Garbage Collector 등 여러 기능을 사용할 수 있게 되었다
profile
반갑습니다

0개의 댓글