2014년 3월 27일

언리얼 프로퍼티 시스템 (리플렉션)

저자: * Michael Noland

리플렉션(Reflection)은 프로그램이 실행시간에 자기 자신을 조사하는 기능입니다. 이는 엄청나게 유용한 데다 언리얼 엔진 테크놀로지의 근간을 이루는 것으로, 에디터의 디테일 패널, 시리얼라이제이션, 가비지 콜렉션, 네트워크 리플리케이션, 블루프린트/C++ 커뮤니케이션 등 다수의 시스템에 탑재된 것입니다. 그러나 C++ 는 어떠한 형태의 리플렉션도 지원하지 않아, 언리얼에는 자체적으로 C++ 클래스, 구조체, 함수, 멤버 변수, 열거형 관련 정보를 수집, 질의, 조작하는 별도의 시스템이 구축되어 있습니다. 전형적으로 이러한 리플렉션은 '프로퍼티 시스템'이라고 부르는데, '리플렉션'은 그래픽 용어이기도 하기 때문입니다.

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

마크업

헤더에 리플렉션이 있는 유형으로 마킹을 하려면, 파일 상단에 특수한 include 를 추가해 줘야 합니다. 그러면 리플렉션이 있는 유형은 이 파일을 고려해야 함을, 시스템 구현에도 필요함을 UHT 에 알려줍니다 (자세한 정보는 '커튼 안쪽 살펴보기' 부분을 참고하세요).

#include "FileName.generated.h"

이제 UENUM(), UCLASS(), USTRUCT(), UFUNCTION(), UPROPERTY() 를 사용하여 헤더의 다양한 유형과 멤버 변수 주석을 달 수 있습니다. 이 매크로 각각은 유형 및 멤버 선언 전에 오며, 추가적인 지정자 키워드를 담을 수 있습니다. (StrategyGame 에서) 현실 세계의 예제를 살펴봅시다:

//////////////////////////////////////////////////////////////////////////

// Base class for mobile units (soldiers)

#include "StrategyTypes.h"

#include "StrategyChar.generated.h"

 

UCLASS(Abstract)

class AStrategyChar : public ACharacter, public IStrategyTeamInterface

{

GENERATED_UCLASS_BODY()

 

/** How many resources this pawn is worth when it dies. */

UPROPERTY(EditAnywhere, Category=Pawn)

int32 ResourcesToGather;

 

/** set attachment for weapon slot */

UFUNCTION(BlueprintCallable, Category=Attachment)

void SetWeaponAttachment(class UStrategyAttachment* Weapon);

 

UFUNCTION(BlueprintCallable, Category=Attachment)

bool IsWeaponAttached();

 

protected:

/** melee anim */

UPROPERTY(EditDefaultsOnly, Category=Pawn)

UAnimMontage* MeleeAnim;

 

/** Armor attachment slot */

UPROPERTY()

UStrategyAttachment* ArmorSlot;

 

/** team number */

uint8 MyTeamNum;

[이하 코드 생략]

};

이 헤더는 ACharacter 에서 파생하여 AStrategyChar 라는 새 클래스를 선언합니다. UCLASS() 를 사용하여 리플렉션 되었음을 나타내고, C++ 정의 안에서는 GENERATED_UCLASS_BODY() 매크로와 짝을 이루기도 합니다. GENERATED_UCLASS_BODY() / GENERATED_USTRUCT_BODY() 매크로는 리플렉션된 클래스나 구조체에 필수적인데, 클래스 본문에 추가적인 함수나 typedef 를 주입하기 때문입니다.

첫 번째 표시되는 프로퍼티는 ResourcesToGather 로, EditAnywhere 와 Category=Pawn 주석이 달린 것입니다. 즉 이 프로퍼티는 에디터 내 어느 디테일 패널에서도 편집할 수 있으며, Pawn 카테고리에 표시된다는 뜻입니다. BlueprintCallable 과 카테고리로 마킹된 한 쌍의 주석 함수가 있는데, 블루프린트에서 호출 가능하다는 뜻입니다.

MyTeamNum 선언에서 보이듯, 리플렉션된 프로퍼티와 아닌 것을 같은 클래스에 섞어도 됩니다만, 리플렉션된 프로퍼티가 아닌 것은 해당 리플렉션에 의존하는 시스템 전부에 보이지 않는 다는 점만 주의하시면 됩니다 (즉 리플렉션 되지 않은 UObject 포인터 그대로를 저장하면 보통 가비지 콜렉터가 레퍼런스를 확인할 수 없기 때문에 위험한 일입니다).

(EditAnywhere 또는 BlueprintCallable 같은) 지정자 키워드 각각은 의미나 사용법에 대한 짧은 코멘트와 함께 ObjectBase.h 에 미러링되어 있습니다. 키워드가 하는 일이 확실치 않은 경우, 보통 Alt+G 를 누르면 ObjectBase.h 의 정의부로 이동시켜 줍니다 (실제 C++ 키워드는 아니지만, Intellisense 또는 VAX 는 그 차이를 잘 이해하지 못하는 것 같습니다).

자세한 정보는 게임플레이 프로그래밍 참고서 를 참고하시기 바랍니다.

한계

UHT 는 실제 C++ 파서가 아닙니다. 해당 언어의 상당 부분을 이해하고 실제로 할 수 있는 만큼 텍스트를 건너뛰기는 하지만, 리플렉션된 유형, 함수, 프로퍼티에만 주의를 기울입니다. 그러나 몇몇은 여전히 헛갈릴 수 있기에, 기존 헤더에 리플렉션된 유형을 추가할 때는 뭔가 단어를 바꾸거나 #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++ member variable or function parameter)

(Many subclasses for different types)

UStruct 는 기본적인 종합 구조체 (C++ 클래스, 구조체, 함수와 같이 다른 멤버를 포함하는 모든 것)이며, (UScriptStruct 인) C++ 구조체와 헛갈리지 말아야 합니다. UClass 는 그 자손으로 함수나 프로퍼티를 포함할 수 있는 반면, UFunction 과 UScriptStruct 는 프로퍼티로만 제한됩니다.

UTypeName::StaticClass() 나 FTypeName::StaticStruct() 를 작성하여 리플렉션된 C++ 유형에 대한 UClass 또는 UScriptStruct 를 구할 수 있으며, Instance->GetClass() 를 사용해서 UObejct 인스턴스에 대한 유형을 구할 수 있습니다 (구조체 인스턴스의 유형을 구하는 것은 불가능한데, 구조체에 대한 공통의 베이스 클래스나 필수 저장공간이 없기 때문입니다).

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 를 통해 살펴보다가, 이뤄내고자 하는 것과 비슷한 작업을 하는 코드 예제를 추적해 내려가는 것이 쉬울 것입니다.

커튼 안쪽 살펴보기

프로퍼티 시스템을 그냥 사용만 하려는 경우 이 부분은 건너뛰어도 좋습니다만, 그 작동 원리를 알아두면 리플렉션된 유형이 포함되는 헤더의 몇 가지 한계와 몇 가지 결정사항에 도움이 됩니다.

Unreal Build Tool (UBT) 와 Unreal Header Tool (UHT) 가 함께하여 실행시간 리플렉션을 강화시키는 데 필요한 데이터를 생성합니다. 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' 는 물론입니다.