언리얼 엔진에서 프로퍼티 시스템은 하나의 리플렉션(Reflection) 시스템이다.

리플렉션은 프로그램이 실행시간에 자기 자신을 조사하는 기능이다.

이는 엄청나게 유용하고 언리얼 엔진 기술의 근간을 이루는 것으로, 에디터의 Detail Panel, Serialization, Garabage Collection, Networld Replication, Blueprint / C++ Communication 등등 다수의 핵심 시스템에 탑재된 기능이다.

C++은 기본적으로 어떠한 형태의 리플렉션도 지원하지 않는데, 언리얼 엔진 4에서는 자체적으로 C++ 클래스, 구조체, 함수, 멤버 변수, 열거형 관련 정보를 수집, 질의, 조작하는 별도의 시스템이 구축되어있다.

전형적으로 이러한 리플렉션은 프로퍼티 시스템 이라고 부르는데, 리플렉션은 그래픽 용어적으로도 많이 쓰이기 때문이다.

리플리케이션 시스템은 옵션이다. 리플렉션 시스템에 보이도록 했으면 하는 유형이나 프로퍼티에 주석을 달아주면, UHT(Unreal Header Tool)가 프로젝트를 컴파일 할 때 해당 정보를 수집한다.

헤더에 리플렉션이 있는 유형으로 마킹을 하려면, 파일 상단에 특수한 inlcude 파일을 추가해줘야 한다.

#include "FileName.generated.h"

그러면 리플렉션이 있는 유형은 이 파일을 고려해야 함을, 시스템 구현에도 필요함을 UHT에 알려준다.

참고로 저 include 구문은 헤더 파일 선언 맨 마지막에 존재해야 한다. 그래야만 원하는 모든 값에 리플렉션을 박아넣을 수 있기 때문이다.

아주 간단한 예를 들어 이런 코드를 보자.

#pragma once

#include "Engine.h"
#include "GameFramework/Actor.h"
#include "BOOM.generated.h"

UCLASS()
class TEST_API ABOOM : public AActor
{
	GENERATED_BODY()
	
public:	
	ABOOM();

	UPROPERTY(EditAnywhere)
	float time=0;

	UPROPERTY()
	int32 count=0;

	UFUNCTION(BlueprintCallable)
	void Explosion();

	virtual void Tick(float DeltaTime) override;

};

여기서 나오는 리플렉션 관련 매크로는 크게 3가지의 범주로 나뉜다.

UCLASS(), UPROPERTY(), UFUNCTION() 이다.

각 리플렉션 매크로는 여러 지정자들 (Classifiers) 을 조합해 특성이나 기능을 추가하거나 변경할 수 있다.

먼저 UCLASS()는 이 클래스가 리플렉션 되었음을 나타내고, 헤더에 .generated.h 가 추가되며, 리플렉션 관련 정보들을 수집해 저장해두었다고 알려준다.

GENERATED_BODY()를 통해 이 매크로가 리플렉션된 것에 대한 추가적인 typedef를 주입해준다.

UCLASS의 지정자들

https://api.unrealengine.com/KOR/Programming/UnrealArchitecture/Reference/Classes/Specifiers/index.html

UPROPERTY의 지정자들

https://api.unrealengine.com/KOR/Programming/UnrealArchitecture/Reference/Properties/Specifiers/index.html

UFUNCTION의 지정자들

https://api.unrealengine.com/KOR/Programming/UnrealArchitecture/Reference/Functions/Specifiers/index.html

UHT (Unreal Header Tool)의 한계

UHT는 실제 C++ Parsor(파서)가 아니다.

해당 언어의 상당 부분을 이해하고 실제로 가능한 만큼 텍스트를 건너뛰기는 하지만, 리플렉션된 유형, 함수, 프로퍼티에만 주의를 기울인다.

여전히 몇몇은 햇갈릴 수 있기 때문에, 기존 헤더에 리플렉션된 유형을 추가할 때, 단어를 바꾸거나, #if CPP / #endif 짝으로 둘러싸야한다.

주석을 단 프로퍼티나 함수에는 (WITH_EDITOR 와 WITH_EDITORONLY_DATA 를 제외하고) #if/#ifdef 사용을 피해야 하는데, 왜냐하면 generated 코드가 그에 대해 레퍼런싱하여 그 정의가 참이지 않은 경우 환경설정에서 컴파일 에러가 나기 떄문이다.

대부분의 흔한 유형은 예상대로 작동하지만, 프로퍼티 시스템은 가능한 C++ 유형 전부를 나타내지 못한다.

특히 TArray 및 TSubclassOf같은 몇몇 템플릿유형만 지원되며, 그 템플릿 파라미터는 중첩 유형이 될 수 없다.

UHT는 런타임에 나타낼 수 없는 유형을 붙이는 경우 오류 메시지가 뜬다.

프로퍼티 시스템에 대한 유형 계층구조

  • UField
    • UStruct
      • UClass (C++ class)
      • UScriptStruct (C++ struct)
      • UFunction (C++ function)
    • UEnum (C++ enumeration)
    • UProperty (C++ 매개 변수 , 함수 매개 변수)
    • 그 외의 여러 서브 클래스, 다른 타입들

UStruct는 기본적인 종합 구조체 (C++ 클래스, 구조체, 함수와 같이 다른 멤버를 포함하는 모든 것)이며, UScriptStruct인 C++ 구조체와는 다른 것이다.

UClass는 그 자손으로 함수나 프로퍼티를 포함할 수 있는 반면, UFunction, UScriptStruct는 프로퍼티만 포함 가능하다.

UStruct에서 파생된 것들은, 모든 멤버에 대한 반복처리를 위해 TFieldIterator를 사용한다.

for (TFieldIterator<UProperty> PropIt(GetClass()); PropIt; ++PropIt)
{
	UProperty* Property = *PropIt;

	// Do something with the property

}

TFieldIterator 에 대한 템플릿 인수는 (UField 를 사용해서 프로퍼티와 함수 둘 다를, 또는 하나 아니면 다른 것을 볼 수 있도록) 필터로 사용된다.

반복처리기 생성자에 대한 두 번째 인수는 필드를 지정된 클래스/구조체에만 도입시킬 것인지, 아니면 부모 클래스/구조체에도 도입시킬것인지(기본)이다. 함수에는 아무런 효과가 없다.

각 유형에는 고유의 플래그 세트가 (EClassFlags + HasAnyClassFlags 등이) 있을 뿐만 아니라 UField 에서 상속된 범용 메타데이터 저장 시스템도 있다.

키워드 지정자는 보통 실행시간 게임내 필요한가 아니면 에디터 함수성으로만 필요한가에 따라 플래그 또는 메타데이터 중 하나로 저장된다.

이런 식으로 에디터 전용 메타데이터는 벗겨내어 메모리를 절약하면서 실행시간 플래그는 항상 사용할 수 있도록 하는 것이 가능하다.

리플렉션 데이터를 사용하면 (프로퍼티 열거, 데이터 주도형 방식으로 값 구하거나 설정, 리플렉션된 함수 실행, 새 오브젝트 생성까지도) 여러가지 많은 작업이 가능하다.

여기서 각 사례별로 자세히 들어가 보기 보다는, UnrealType.h 와 Class.h 를 통해 살펴보다가, 이뤄내고자 하는 것과 비슷한 작업을 하는 코드 예제를 추적해 내려가는 것이 쉬울 것이다.

프로퍼티 시스템의 작동 원리

UBT ( Unreal Build Tool ) 와 UHT ( Unreal Header Tool ) 가 함께하여 실행시간 리플렉션을 강화시키는 데 필요한 데이터를 생성한다.

UBT 는 그 역할을 위해 헤더를 스캔한 다음 리플렉션된 유형이 최소 하나 있는 헤더가 들어있는 모듈을 기억한다.

그 헤더 중 어떤 것이든 지난 번 컴파일 이후 변경되었다면, UHT 를 실행하여 리플렉션 데이터를 수집하고 업데이트한다.

UHT 는 헤더를 파싱하고, 리플렉션 데이터 세트를 빌드한 다음, (모듈별.generated.inl 에 기여하는) 리플렉션 데이터가 들어있는 C++ 코드를 생성할 뿐만 아니라, (헤더별 .generated.h 인) 다양한 헬퍼 및 thunk 함수도 생성한다.

리플렉션 데이터를 C++ generated 코드로 저장하는 것의 한 가지 주요 장점은, 바이너리와의 동기화가 보장된다는 점이다.

오래되거나 버전이 맞지 않는 리플렉션 데이터를 로드할 일은 없는데, 나머지 엔진 코드와 함께 컴파일되기 때문이다.

그리고 특정 플랫폼/컴파일러/최적화 콤보의 패킹 작동방식을 역엔지니어링하려 하기 보다는, C++ 표현식을 사용해서 시작시 멤버 오프셋/등등을 계산하기 때문이다.

UHT 역시도 generated 헤더를 소모하지 않는 독립형 프로그램으로 만들어졌기에, UE3 의 스크립트 컴파일러에서 흔히 발생했던 닭이냐 계란이냐 문제가 생기지 않는다.

generated 함수에는 StaticClass() / StaticStruct() 같은 것이 포함되어 있어, 유형에 대한 리플렉션 데이터를 구하는 것이 쉬워질 뿐만 아니라, 블루프린트나 네트워크 리플레키에션에서 C++ 함수를 호출하는 데 사용되는 thunk 를 구하는 것도 쉬워진다.

이는 클래스나 구조체의 일부로 선언되어야 하며, GENERATED_UCLASS_BODY() 또는 GENERATED_USTRUCT_BODY() 매크로가 리플렉션된 유형에 포함되어야 하는지에 대한 이유가 된다.

이 매크로를 정의하는 #include ‘TypeName.generated.h’ 는 물론이다.