광고


[UE4] TSubclassOf UE4


0. 서문


위 참고 문서에 기술된 TSubclassOf의 정의는 다음과 같다.
"TSubclassOf는 UClass 타입의 안정성을 보장해 주는 템플릿 클래스입니다"

위 문장만 보았을 때 TSubclassOf는 UClass의 하위 클래스로 한정하는 일종의 type traits 클래스라는 것을 알 수 있다.
헌데, UClass는 무엇인가? TSubclassOf에 대해 상세히 살펴보기 전에 우선 이 녀석에 대한 개념부터 살펴보도록 하자.


<UClass>

UClass에 대해 자세히 설명시 별도의 문서 작성 수준이기에, 일단 간단히 설명해 보면 다음과 같다.

헤더 파일에 UObject를 적법하게 선언하면 UHT(Unreal Header Tool)가 헤더 파일을 분석하는 과정이 진행되며, 이 과정이 완료되면 Intermediate 폴더에 UObject의 정보를 담은 메타 파일이 생성된다.

이 메타 정보는 UClass 라는 특별한 클래스를 통해 보관되며, UObject의 다음의 정보들을 모두 기록하고 있다.
  • 클래스 계층 구조 정보
  • 멤버 변수
  • 멤버 함수
이를 통해, 언리얼 엔진은 C++ 언어 자체가 지원하지 못하는 Reflection을 지원할 수 있다.

컴파일 단계에서는 개별 UObject의 UClass가 생성(Not instance, just declaration/definition)되고, 실행 초기의 런타임 과정에서는 UObject마다의 UClass의 인스턴스와 UObject의 인스턴스가 생성된다. 이 과정에서 생성되는 UObject의 인스턴스는 앞으로 월드에 생성될 UObject 인스턴스의 기본 셋팅을 지정하는데 사용되며, 이를 클래스 기본 객체(CDO : Class Default Object)라 한다.

언리얼 엔진에서 CDO를 만드는 이유는 UObject를 생성할 때마다 매번 초기화하지 않고, 클래스 기본 객체를 미리 만들어 놓고 복제하는 방식으로 구현되어 있기 때문이다. 
월드에 동일한 Npc 몬스터를 수백마리 스폰시켜야 되는 상황을 생각해 보면, 그리고 해당 Npc 몬스터 하나하나가 그리 작지 않은 크기라면, 모든 Npc 몬스터를 처음부터 생성하는 방법보다 미리 큰 객체 덩어리를 복사한 뒤 속성 값만 변경하는 방법이 훨씬 효과적임을 알 수 있다.

참고로, UObject의 생성자 함수는 CDO를 생성하는데에만 사용된다.
즉, 엔진이 초기화되는 런타임 과정에서 생성자가 호출되는 것이지, 월드에 스폰될 때 생성되는 것이 아님을 헤깔리지 말자.

다시 정리하면, 엔진 초기 구동 런타임에 하나의 UObject가 초기화 될 때 다음 2개의 인스턴스가 함께 생성된다.
  • UClass 인스턴스
  • UObject의 CDO 인스턴스

마지막으로 언리얼 엔진은 모듈 방식으로 구현되는데, 개별 모듈마다 속한 모든 UObject의 초기화(UClass / CDO 인스턴스 생성)를 진행한다.

UClass에 대해 개략적으로 이해했으니, 이제 본격적으로 TSubclassOf에 대해 알아보도록 하자.


1. 정의

우선, TSubclassOf의 선언/정의는 다음의 헤더 파일에서 확인할 수 있다.
  • Engine/Source/Runtime/CoreUObject/Public/Templates/SubclassOf.h
  1. #pragma once
  2.  
  3. #include "CoreMinimal.h"
  4. #include "UObject/Class.h"
  5.  
  6. // UClass를 타입에 안전하게 넘기는 특성 클래스
  7. template <class TClass>
  8. class TSubclassOf
  9. {
  10.     template <class TClassA>
  11.     friend class TSubclassOf;
  12.  
  13. private:
  14.     UClass* Class = nullptr;
  15.  
  16. public:
  17.     // 기본 생성자
  18.     FORCEINLINE TSubclassOf() = default;
  19.  
  20. public:
  21.     // 복사생성자 : UClass 포인터를 받고, 런타임에 호환 여부 체크
  22.     FORCEINLINE TSubclassOf(UClass* From)
  23.         :
  24.         Class(From)
  25.     {
  26.     }
  27.     // 복사생성자 : 컴파일 타임에 호환 여부 체크
  28.     // enable_if + is_convertible 사용에 주목
  29.     template <class TClassA, class = typename TEnableIf<TPointerIsConvertibleFromTo<TClassA, TClass>::Value>::Type>
  30.     FORCEINLINE TSubclassOf(const TSubclassOf<TClassA>& From)
  31.         :
  32.         Class(*From)
  33.     {
  34.     }
  35.  
  36. public:
  37.     // 대입연산자 : UClass 포인터를 받고, 런타임에 호환 여부 체크
  38.     FORCEINLINE TSubclassOf& operator=(UClass* From)
  39.     {
  40.         Class = From;
  41.         return *this;
  42.     }
  43.     // 대입연산자 : 컴파일 타임에 호환 여부 체크
  44.     // enable_if + is_convertible 사용에 주목
  45.     template <class TClassA, class = typename TEnableIf<TPointerIsConvertibleFromTo<TClassA, TClass>::Value>::Type>
  46.     FORCEINLINE TSubclassOf& operator=(const TSubclassOf<TClassA>& From)
  47.     {
  48.         Class = *From;
  49.         return *this;
  50.     }
  51.    
  52. public:
  53.     // UClass로 Dereferencing, 런타임에 타입 체크
  54.     FORCEINLINE UClass* operator* () const
  55.     {
  56.         if (!Class || !Class->IsChildOf(TClass::StaticClass()))
  57.         {
  58.             return nullptr;
  59.         }
  60.         return Class;
  61.     }
  62.  
  63.     // 단순히 UClass로 Dereferencing
  64.     FORCEINLINE UClass* operator-> () const
  65.     {
  66.         return **this;
  67.     }  
  68.     // 단순히 UClass로 Dereferencing
  69.     FORCEINLINE UClass* Get() const
  70.     {
  71.         return **this;
  72.     }
  73.     // UClass로 타입 암시적 변환 (단순히 UClass로 Dereferencing)
  74.     FORCEINLINE operator UClass* () const
  75.     {
  76.         return **this;
  77.     }
  78.  
  79. public:
  80.     // CDO 객체 반환
  81.     FORCEINLINE TClass* GetDefaultObject() const
  82.     {
  83.         return Class ? Class->GetDefaultObject<TClass>() : nullptr;
  84.     }
  85.  
  86. public:
  87.     // 직렬화
  88.     friend FArchive& operator << (FArchive& Ar, TSubclassOf& SubclassOf)
  89.     {
  90.         Ar << SubclassOf.Class;
  91.         return Ar;
  92.     }
  93.     // 해쉬값
  94.     friend uint32 GetTypeHash(const TSubclassOf& SubclassOf)
  95.     {
  96.         return GetTypeHash(SubclassOf.Class);
  97.     }
  98. };

위 클래스 정의를 보면 알겠지만, TSubclassOf가 복사생성/대입연산될 때 인자가 UClass* 이냐, TSubclassOf 상수 참조이냐에 따라, 타입 체크의 시점이 다름을 확인할 수 있다.
  • UClass* : 런타임 체크
  • TSubclassOf 상수 참조 : 컴파일 타임 체크 (참고 : [TR1/C++11] enable_if)

TSubclassOf 끼리의 컴파일 타임 체크는 enable_if + is_convertible 타입 특성 클래스의 조합으로 구성되어 있다.
TClassA가 TClass로 컨버전이 불가능하면, 즉 TClassA가 TClass의 하위 클래스가 아니면 void type이 나오게 되고 컴파일에 실패하게 되는 것이다.

서문의 <UClass> 챕터에 얘기했듯이 모든 UObject에 상응하는 UClass가 생성되고 인스턴싱 된다.
이를 이용해서, 특정 타입으로 한정하는 것이기에 "UClass를 이용해 UObject 타입을 한정짓는 것"이라고 표현해도 무방하다.


2. 예제

지금까지 살펴본 것처럼 TSubclassOf 템플릿 클래스를 이용해 특정 멤버 변수나 변수를 특정 UObject/UClass의 하위 클래스로 한정지을 수 있다.

이를 통해, 관련없는 녀석이 들어와 런타임 에러를 낸다거나 하는 실수를 사전에 방지할 수 있다.
특히 프로그래머가 아닌 기획/아트에게 제공되는 블루프린트에 노출되는 변수일수록 중요한 타입 제약이라 할 수 있다.
(물론 나 같은 경우 프로그래머도 안 믿는다. 믿을 건 사람이 실수할 여지를 최소화한 견고한 구성 뿐...)

아래 예제는 HUD 화면에 사용할 위젯 클래스를 UUserWidget 이하로 한정짓는 코드 샘플이다.

  1. // HUD 화면에 사용할 위젯 클래스
  2. UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Power, meta = (BlueprintProtected = "true"))
  3. TSubclassOf<UUserWidget> HUDWidgetClass;


1 2 3 4 5 6 7 8 9 10 다음