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++ 클래스가 언리얼 엔진의 클래스로 인식되려면 반드시 클래스 선언 바로 위에 이 매크로를 붙여야 합니다.
#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 참조 추적은 각각의 지정자·타입·사용 경로에 따라 결정되며, 매크로 하나가 모든 권한이나 저장 기능을 켜지는 않습니다.
#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 같은 지정자가 필요합니다.
#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++ 컴파일·링크 오류, 에디터가 로드한 클래스 순서로 구분해 확인합니다.