안동민 개발노트

본문 시작

C++ 클래스와 언리얼 리플렉션 시스템

UCLASS·UPROPERTY·UFUNCTION 매크로가 UHT를 거쳐 에디터와 Blueprint에 노출되는 과정을 익힙니다.

첫 번째 언리얼 C++ 프로젝트를 만들고 에디터 구조를 확인했다면, 이제 본격적으로 코드로 엔진을 제어할 단계입니다.

그 시작점이 바로 리플렉션 시스템(Reflection System)입니다.

이 개념을 이해해야 C++ 클래스가 에디터와 블루프린트 안에서 정상적으로 노출되고, 설정 가능한 형태로 동작합니다.

일반 C++ 클래스만으로는 언리얼 에디터가 코드를 충분히 이해하고 상호작용하기 어렵습니다.

언리얼 엔진은 자체 메커니즘으로 C++ 메타데이터를 생성해, 에디터 노출, 속성 편집, 블루프린트 연동을 가능하게 합니다.

이 연결의 중심이 바로 리플렉션 시스템입니다.

빌드에서는 UBT(Unreal Build Tool)가 먼저 UHT(Unreal Header Tool)를 호출해 리플렉션 선언을 읽고 코드를 생성합니다. 그다음 C++ 컴파일러가 원문과 생성 코드를 빌드합니다.


언리얼의 리플렉션 시스템이란?

리플렉션 시스템은 실행 중 클래스 구조와 동작 정보를 조회/활용할 수 있게 해 주는 메커니즘입니다.

일반 C++은 이런 실행 시점 정보(Runtime Information)를 충분히 제공하지 않지만, 언리얼은 전용 매크로로 클래스/변수/함수 메타데이터를 생성해 런타임과 에디터에서 활용할 수 있게 합니다.

이 메타데이터 덕분에 언리얼 엔진 에디터는 여러분이 C++로 만든 클래스를 인식하고, Actor 계열의 인스턴스를 월드에 배치하거나, 지정자로 허용한 속성을 디테일 패널에서 편집할 수 있게 됩니다.

또한 블루프린트에서 여러분의 C++ 함수를 호출하거나 변수에 접근하는 것도 이 리플렉션 시스템 덕분에 가능한 일이죠.

그럼, 이러한 리플렉션 시스템을 활성화하기 위해 사용되는 주요 매크로들을 하나씩 살펴보겠습니다.


UCLASS(): 언리얼 엔진 클래스로서의 선언

가장 기본이 되는 매크로는 UCLASS()입니다.

여러분이 만드는 C++ 클래스가 언리얼 엔진의 클래스로 인식되려면 반드시 클래스 선언 바로 위에 이 매크로를 붙여야 합니다.

MyAwesomeActor.h
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyAwesomeActor.generated.h" // 필수 포함 파일

UCLASS() // 이 클래스가 언리얼 엔진의 클래스임을 선언합니다.
class MYFIRSTCPPPROJECT_API AMyAwesomeActor : public AActor
{
    GENERATED_BODY() // 이 클래스의 본문을 생성하기 위한 매크로입니다.

public:
    // 생성자입니다.
    AMyAwesomeActor();

protected:
    // 게임 시작 시 호출됩니다.
    virtual void BeginPlay() override;

public:
    // 매 프레임 호출됩니다.
    virtual void Tick(float DeltaTime) override;
};

위 코드에서 몇 가지 중요한 점을 짚어보겠습니다.

  • #include "MyAwesomeActor.generated.h": UCLASS()를 사용하는 언리얼 클래스에서 반드시 마지막 include여야 합니다. UBT가 호출한 UHT가 자동 생성하는 파일이며, 리플렉션 시스템에 필요한 메타데이터 코드가 들어 있습니다.

  • UCLASS(): 이 매크로가 붙어 있는 클래스는 언리얼 엔진에 의해 특별하게 처리됩니다. 이 예제는 AActor에서 액터 기능과 Blueprintable 성질을 물려받습니다. 일반 UObject 계열의 블루프린트 파생 허용 여부는 Blueprintable 지정자와 상속 관계로 결정됩니다.

  • GENERATED_BODY(): 이 매크로도 UCLASS()와 함께 이 예제처럼 리플렉션에 참여하는 클래스의 본문(Body) 시작 부분에 포함되어야 합니다. 언리얼 엔진의 리플렉션 시스템이 제대로 작동하기 위해 필요한 내부 코드를 자동으로 생성해줍니다.

  • A 접두사: AMyAwesomeActor처럼 액터 계열 클래스에는 보통 A를 붙입니다. 컴포넌트는 U(예: UStaticMeshComponent), 열거형은 E(예: ECollisionChannel)를 쓰는 식입니다.

    UHT가 처리하는 타입에는 필요한 접두사를 지켜야 합니다. 이는 일반 C++ 타입의 자유로운 이름 선택과 구분되는 엔진 규칙입니다.

  • MYFIRSTCPPPROJECT_API: 이는 DLL 익스포트/임포트와 관련된 매크로입니다. 여러분의 프로젝트 모듈에서 이 클래스를 외부에 노출할 때 사용됩니다. 크게 신경 쓰지 않아도 되지만, 언리얼 프로젝트에서 자동 생성되는 코드에 붙어 있음을 인지하고 계시면 됩니다.

  • public AActor: 우리가 만든 AMyAwesomeActor는 AActor를 상속받고 있습니다. AActor는 언리얼 엔진 월드에 배치할 수 있는 모든 객체의 기본 클래스입니다. 나중에 다른 기본 클래스들도 배우게 될 것입니다.


UPROPERTY(): 에디터 노출 및 직렬화

UPROPERTY()는 지원되는 멤버 변수를 리플렉션 시스템에 등록합니다. 에디터 편집, 블루프린트 접근, 직렬화와 GC 참조 추적은 각각의 지정자·타입·사용 경로에 따라 결정되며, 매크로 하나가 모든 권한이나 저장 기능을 켜지는 않습니다.

MyAwesomeActor.h
#pragma once

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

UCLASS()
class MYFIRSTCPPPROJECT_API AMyAwesomeActor : public AActor
{
    GENERATED_BODY()

public:
    AMyAwesomeActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "My Properties")
    float MyFloatVariable; // 에디터에서 편집 가능한 float 변수

    UPROPERTY(VisibleAnywhere, Category = "My Properties")
    int MyIntegerVariable; // 에디터에서 볼 수만 있는 int 변수

    UPROPERTY(EditDefaultsOnly, Category = "My Properties")
    FString MyStringVariable; // 블루프린트 기본값에서만 편집 가능한 문자열
};

UPROPERTY() 매크로 괄호 안에 들어가는 키워드들은 해당 변수가 에디터에서 어떻게 동작할지, 블루프린트에서 어떻게 접근할지 등을 정의합니다.

몇 가지 자주 사용되는 키워드를 소개합니다.

  • EditAnywhere: 이 변수는 언리얼 에디터의 디테일 패널에서 어디에서든(에디터 레벨, 블루프린트 기본값 등) 편집할 수 있습니다. 가장 일반적으로 사용됩니다.
  • VisibleAnywhere: 이 변수는 디테일 패널에서 볼 수는 있지만(Read-Only) 편집할 수는 없습니다.
  • EditDefaultsOnly: 클래스 기본값 등 아키타입에서 편집할 수 있고, 레벨에 배치한 개별 인스턴스에서는 편집할 수 없습니다.
  • BlueprintReadWrite: 이 변수는 블루프린트에서 읽고 쓰는(Set/Get) 것이 모두 가능하게 합니다.
  • BlueprintReadOnly: 이 변수는 블루프린트에서 읽기만(Get)가능하게 합니다.
  • Category = "My Properties": 디테일 패널에서 이 변수들이 특정 카테고리(My Properties) 아래에 묶여 표시되도록 합니다. 여러 변수가 있을 때 패널을 깔끔하게 정리하는 데 유용합니다.
  • meta=(DisplayName="My Custom Name"): 디테일 패널에 표시되는 변수 이름을 원하는 대로 지정할 수 있습니다.

UFUNCTION(): 함수 등록과 호출 권한

UFUNCTION()은 함수를 리플렉션 시스템에 등록합니다. 블루프린트 그래프에서 직접 호출하려면 BlueprintCallable이나 BlueprintPure 같은 지정자가 필요합니다.

MyAwesomeActor.h
#pragma once

// ... (생략) ...

UCLASS()
class MYFIRSTCPPPROJECT_API AMyAwesomeActor : public AActor
{
    GENERATED_BODY()

public:
    AMyAwesomeActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "My Properties")
    float CurrentHealth;

    UFUNCTION(BlueprintCallable, Category = "My Functions")
    void TakeDamage(float DamageAmount); // 블루프린트에서 호출 가능한 함수

    UFUNCTION(BlueprintPure, Category = "My Functions")
    float GetHealthRatio() const; // 블루프린트에서 호출 가능한 순수(Pure) 함수
};

UFUNCTION() 매크로 괄호 안의 키워드도 UPROPERTY()와 유사하게 함수의 동작 방식을 정의합니다.

  • BlueprintCallable: 이 함수를 블루프린트에서 호출(Call) 할 수 있게 합니다. 예를 들어, 버튼을 클릭했을 때 특정 C++ 함수를 실행하고 싶을 때 사용합니다.
  • BlueprintPure: 실행 핀 없이 값으로 사용하는 함수로 노출합니다. 개발자는 관찰 가능한 상태를 변경하지 않도록 작성해야 하지만, 지정자가 수학적 순수성을 검증하지는 않습니다. Getter가 읽는 객체 상태가 달라지면 인자가 같아도 결과가 달라질 수 있으며, 결과를 자동으로 캐시하지 않습니다.
  • Category = "My Functions": UPROPERTY와 마찬가지로 블루프린트 에디터에서 함수가 특정 카테고리 아래에 표시되도록 합니다.
  • Server / Client / NetMulticast: 네트워크 게임 개발 시 RPC의 실행 대상을 지정하는 키워드입니다. 실제 원격 호출은 소유권과 복제 등 네트워크 조건도 충족해야 합니다. 이 책의 범위를 벗어나지만, 나중에 고급 과정을 학습할 때 접하게 될 것입니다.

선언과 노출 확인

예제 속성의 편집 권한과 블루프린트 접근

같은 UPROPERTY라도 에디터와 블루프린트 그래프 권한은 별도로 지정합니다.

예제 속성의 편집 권한과 블루프린트 접근
예제 멤버에디터의 값 편집블루프린트 그래프
실수 값MyFloatVariable: 기본값과 배치한 인스턴스에서 편집BlueprintReadWrite: 읽기·쓰기
정수 값MyIntegerVariable: VisibleAnywhere로 표시만 가능읽기·쓰기 지정자 없음
문자열 값MyStringVariable: EditDefaultsOnly로 기본값에서 편집읽기·쓰기 지정자 없음
실수 값
에디터의 값 편집: MyFloatVariable: 기본값과 배치한 인스턴스에서 편집
블루프린트 그래프: BlueprintReadWrite: 읽기·쓰기
정수 값
에디터의 값 편집: MyIntegerVariable: VisibleAnywhere로 표시만 가능
블루프린트 그래프: 읽기·쓰기 지정자 없음
문자열 값
에디터의 값 편집: MyStringVariable: EditDefaultsOnly로 기본값에서 편집
블루프린트 그래프: 읽기·쓰기 지정자 없음

이 표는 위의 선언에 지정된 권한을 비교합니다. Category는 표시 분류이며 접근 권한을 추가하지 않습니다.

다음은 권한과 객체 참조를 별도로 선언한 멤버 예시입니다. 리플렉션에 참여하는 클래스 내부에 둡니다.

권한과 참조를 분리한 멤버 선언
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Combat")
float MaxHealth = 100.0f;

UPROPERTY()
TObjectPtr<UObject> ActiveEffect;

ActiveEffect의 강한 GC 참조는 이 멤버를 가진 객체 자체가 도달 가능한 상태일 때 의미가 있습니다. 자세한 생성·수명 규칙은 다음 절에서 다룹니다.

이 절의 헤더들은 선언을 설명하는 예시이며, 생성자와 함수 정의까지 포함한 완성 프로그램은 아닙니다. 오류가 나면 UHT가 읽는 선언과 마지막 generated.h include, C++ 컴파일·링크 오류, 에디터가 로드한 클래스 순서로 구분해 확인합니다.