UE5 UClass Reflection

에크까망·2024년 9월 26일

들어가기 전에

  • 단순화 하기 위해서 문체를 단정적으로 사용하지만 지극히 개인 의견일 뿐 입니다.
  • 본문과 관련해서 오류 지적이나 의견 있으시면 꼭 댓글 부탁드립니다.
  • 글 작성 시점은 2024/09 입니다.
  • 글 작성 기준은 UE5.4.4 입니다.

들어가며

공식 문서에도 있고, 각종 블로그 에도 있고 많은 곳에 UClass, UObject 설명이 있지만, 굳이 덧붙일려고 한다. 아마 오류도 있을 수 있고, UE5.5.0 에서는 또 바뀌어 있을지도 모르지만, 그래도 정리해 보고자 한다.

이번 포스팅에서는 언리얼 엔진에서

  • UClass 의 주목적
  • UObject-UClass 상속 관계 및 특정 상황에서의 Instance 구성
  • UClass Reflection 이 어떤 거고, 어떻게 활용하고, 어떻게 가능한건지 맛보기
  • ClassDefaultObject
  • UClass Reflection 내용을 어떻게 언리얼엔진이 채워 놓는지 맛보기
  • 등등...

에 대해서 다뤄 보기로 한다. 다른 포스팅에서도 그렇지만 여기서도 여러분이 기본적인 UCLASS(...), UFUNCTION(...), UPROPERTY(...) 의 코딩 방법은 알 고 있다고 가정한다. Code 가 좀 나오지만 그리 어렵지는 않으니 눈으로 한번 살펴보자.

UClass 의 주 목적

UClass 는 특정 UObject 의 내부 정보를 읽기/쓰기/순회/호출 에 필요한 메타데이터를 제공하는데 주 목적이 있다. 공식 문서도 하도 여러군데 흩어져 있어서 따로 링크를 달지 않겠지만 UClass 의 주목적은 누가 뭐라고 해도 이런 Reflection 기능의 제공이다. C++에서는 기본적으로 지원하지 않으니 말이다. Reflection 기능의 사전적 정의를 먼저 살펴보자.

  • 런타임 타입 정보 확인: 객체의 클래스나 인터페이스 정보를 실행 중에 확인
  • 메서드 호출: 프로그램에서 메서드 이름을 문자열로 전달해 런타임에 해당 메서드를 호출.
  • 속성 접근 및 수정: 객체의 필드나 속성에 접근하거나 수정.
  • 클래스 생성: 런타임에 클래스나 객체를 동적으로 생성할 수 있다.

위에서 '클래서 생성' 은 BlueprintClass 생성이 담당하고 있다고 봐도 되겠다. Runtime 에서 언리얼엔진에서 Class 를 생성해서 사용하는 경우가 있을까... 싶긴하다.

Reflection을 한국 말로 굳이 번역하자면 '반영' 정도가 될 듯한데 딱 부러지게 맞는 용어가 아닌듯 하다. 그냥 리플렉션으로 받아 들이자.

Sample Code 'UMyParent'

Class UMyParent

UCLASS()  
class EKUE54_API UMyParent : public UObject  
{  
    GENERATED_BODY()  
    protected:  
    UPROPERTY(EditAnywhere, BlueprintReadWrite)  
    int32 MyNumA = 1;  
  
    UPROPERTY(EditAnywhere, BlueprintReadWrite)  
    int32 MyNumB = 2;  
  
    UFUNCTION(BlueprintCallable)  
    int32 GetAddedNumA(int32 v);  
};

Class UMyChild

UCLASS()  
class EKUE54_API UMyChild : public UMyParent  
{  
    GENERATED_BODY()  
};

Unreal Reflection Usage

위의 Sample Code 가 있다고 했을때, MyNumA 를 String 기반으로 가져 올때는 다음과 같이 하면 된다.

Get Value

void PrintMyNumA()  
{  
    // UMyParent 클래스의 인스턴스가 있다고 가정  
    UMyParent* ParentInstance = NewObject<UMyParent>();  
  
    // 현재 클래스를 가져옴  
    UClass* Class = UMyParent::StaticClass();  
  
    // 'MyNumA'라는 이름의 프로퍼티를 찾음  
    FProperty* FoundProperty = Class->FindPropertyByName(FName("MyNumA"));  
  
    if (FoundProperty)  
    {       // 프로퍼티가 int32 형인지 확인  
       if (FIntProperty* IntProperty = CastField<FIntProperty>(FoundProperty))  
       {          // 이 클래스의 인스턴스에서 MyNumA 값 가져오기  
          int32 MyNumAValue = IntProperty->GetPropertyValue_InContainer(ParentInstance);  
  
          // 값을 로그로 출력  
          UE_LOG(LogTemp, Warning, TEXT("MyNumA Value: %d"), MyNumAValue);  
       }    
    }
    else  
    {  
       UE_LOG(LogTemp, Warning, TEXT("Property 'MyNumA' not found."));  
    }
}

Property Iterating A

// UPROPERTY 순회 예제  
void PrintUProperties(UObject* Object)  
{  
    if (Object == nullptr) 
    return;
  
    UClass* Class = Object->GetClass();  
    for (TFieldIterator<FProperty> PropIt(Class); PropIt; ++PropIt)  
    {       
	   FProperty* Property = *PropIt;  
       FString PropertyName = Property->GetName();  
  
       // 예제: int32 또는 float 값 가져오기  
       if (FIntProperty* IntProp = CastField<FIntProperty>(Property))  
       {          
	       int32 Value = IntProp->GetPropertyValue(IntProp->ContainerPtrToValuePtr<void>(Object));  
          UE_LOG(LogTemp, Log, TEXT("%s: %d"), *PropertyName, Value);  
       }       
       else if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Property))  
       {          
	       float Value = FloatProp->GetPropertyValue(FloatProp->ContainerPtrToValuePtr<void>(Object));
          UE_LOG(LogTemp, Log, TEXT("%s: %f"), *PropertyName, Value);  
       }       
       else  
       {  
          UE_LOG(LogTemp, Log, TEXT("Property %s is not int or float"), *PropertyName);  
       }    
    }
}

Property Iterating B

void PrintClassProperties(UClass* Class)  
{  
    if (Class == nullptr)  
    {       
	    UE_LOG(LogTemp, Warning, TEXT("Class is null!"));  
       return;  
    }  
    // ChildProperties로부터 필드 순회  
    for (FField* Field = Class->ChildProperties; Field != nullptr; Field = Field->Next)  
    {       // 필드 이름 출력  
       UE_LOG(LogTemp, Log, TEXT("Field Name: %s"), *Field->GetName());  
  
       // 필드가 FProperty 타입이면 추가 정보를 출력  
       if (FProperty* Property = CastField<FProperty>(Field))  
       {          
	       UE_LOG(LogTemp, Log, TEXT("Property Type: %s"), *Property->GetCPPType());  
       }    
    }
}

Call Function

stirng 기반으로 GetAddedNumA 호출은 이와 같이 하면 된다.

void CallGetAddedNumA()  
{  
    // UMyParent 클래스의 인스턴스 생성  
    UMyParent* ParentInstance = NewObject<UMyParent>();  
  
    // 현재 클래스의 UClass 정보 가져오기  
    UClass* Class = UMyParent::StaticClass();  
  
    // 'GetAddedNumA' 함수 찾기  
    UFunction* Function = Class->FindFunctionByName(FName("GetAddedNumA"));  
   if (Function)  
    {       // 파라미터 구조체 준비 (인자와 반환값을 위한 구조체)  
       struct FMyParams
       {  
          int32 InputValue;  
          int32 ReturnValue;  // 함수가 반환할 값을 저장  
       };  
  
       FMyParams Params;  
       Params.InputValue = 10;  // 전달할 인자 값 설정  
  
       // 함수 호출: 파라미터 구조체를 전달하고, 함수는 반환값을 해당 구조체에 채워 넣음  
       ParentInstance->ProcessEvent(Function, &Params);  
  
       // 반환값을 로그로 출력  
       UE_LOG(LogTemp, Warning, TEXT("Returned Value: %d"), Params.ReturnValue);  
    }    
    else  
    {  
       UE_LOG(LogTemp, Warning, TEXT("Function 'GetAddedNumA' not found."));  
    }
}

C++ Reflection 'HowTo'

위 와 같은 동작이 C++ 에서 어떻게 가능한걸까? 그건 class 멤버 변수 와 함수를 해당 class 에서 상대 주소값(offset) 을 계산해두고 this+offset 기반으로 값을 읽고(메모리에서), 함수를 호출 할 수 있기 때문이다. 아래 예제를 살펴보자.

Source Code

#include <iostream>
#include <cstdint>

class MyClass {
public:
    int a;
    int b;

    // 멤버 함수: c + a + b 출력
    void PrintSum(int c) {
        std::cout << "Sum: " << (c + a + b) << std::endl;
    }

    // static 멤버: 변수 a, b의 오프셋과 PrintSum 함수 포인터
    static size_t offset_a;
    static size_t offset_b;
    static void (MyClass::* PrintSumOffset)(int);
};

// static 변수 정의
size_t MyClass::offset_a = offsetof(MyClass, a);
size_t MyClass::offset_b = offsetof(MyClass, b);
void (MyClass::* MyClass::PrintSumOffset)(int) = &MyClass::PrintSum;

int main() {
    // MyClass 인스턴스 생성
    MyClass MyObj;
    MyObj.a = 10;
    MyObj.b = 20;

    // 포인터를 이용해 MyObj의 a, b 값을 가져오기
    int* pA = reinterpret_cast<int*>(reinterpret_cast<uint8_t*>(&MyObj) + MyClass::offset_a);
    int* pB = reinterpret_cast<int*>(reinterpret_cast<uint8_t*>(&MyObj) + MyClass::offset_b);

    std::cout << "Value of a: " << *pA << std::endl; // a의 값 출력
    std::cout << "Value of b: " << *pB << std::endl; // b의 값 출력

    // PrintSum 함수를 호출 (포인터를 통해서)
    (MyObj.*(MyClass::PrintSumOffset))(5); // c=5 인자로 전달

    return 0;
}

Output

Value of a: 10
Value of b: 20
Sum: 35

일견 이상해 보일 수도 있지만 사실 이상할 건 없다. C++ Class 를 컴파일 하면 어셈블리 코드가 this 포인터에서 Offset 을 더해서 작동하게 원래 만들어지는데 그걸 C++ Source 단에서 직접 코딩한 것 뿐이다.

똑같진 않지만, 언리얼에서 FField 에서 값을 가져오는 부분을 살펴보자. 위 'Unreal Reflection Usage' 에서 int32 MyNumAValue = IntProperty->GetPropertyValue_InContainer(ParentInstance); 부분을 따라가면 아래 Code가 나온다.

UE_5.4/Engine/Source/Runtime/CoreUObject/Public/UObject/UnrealType.h


FORCEINLINE void* ContainerVoidPtrToValuePtrInternal(void* ContainerPtr, int32 ArrayIndex) const  
{  
    checkf((ArrayIndex >= 0) && (ArrayIndex < ArrayDim), TEXT("Array index out of bounds: %i from an array of size %i"), ArrayIndex, ArrayDim);  
    check(ContainerPtr);  
  
    if (0)  
    {  
       // in the future, these checks will be tested if the property is NOT relative to a UClass  
       check(!GetOwner<UClass>()); // Check we are _not_ calling this on a direct child property of a UClass, you should pass in a UObject* in that case  
    }  
  
    return (uint8*)ContainerPtr + Offset_Internal + static_cast<size_t>(ElementSize) * ArrayIndex;  
}

위에서 ContainerPtr 가 UObject 의 Pointer 이고 Offset_Internal 이 각 FField(여기서는 FIntProperty) 에 저장되어 있는 UClass 에서의 해당 변수 Offset Pointer 이다. ArrayIndex 는 배열 타입일 경우 몇 번쨰 인자인가 하는 것으로 여기서는 '0' 이라고 보면 된다.

결국 각 FFieldUPROPERTY 에 있는 변수에 대해 'Name', 'Type', 'OfssetPointer' 를 가지고 있고 각 UObject 에 해당하는 UClass 의 Instance 가 FField 목록을 가지고 있다고 보면 된다.

언리얼이 실제로 저 값을 어떻게 채워서 UClass Instance 를 제공하는지는 아래에서 살펴기로 하고, 우선은 '아 저렇게 돌겠구나...' 하는 마음의 위안을 삼자. 이제 실제로 언리얼 에서 어떻게 구성되어 있는지 대략적으로 살펴보자.

Class Diagram

우선 UObject, UClass 및 각종 Field 들의 관계를 간략하게 살펴보자. 아래 Diagram 중 UMyParentUMyChild 는 우리가 Custom 하게 상속 받은 Class 로 가정한다.

UClass

UClass 가 UObject 와 쌍으로 가장 자주 사용하게 되는 Class 이고 Blueprint로 만들어지는 Class 를 대변하는 UBlueprintGeneratedClass 의 부모 Class 이다. 여기서는 Reflection 기능을 거의 제공해주는 UStruct에 초점을 맞추기 때문에 자세히 살펴 보지는 않겠다. GetSuperClass 의 경우 아래와 같이 되어 있다는 정도만 알고 넘어가자.

/** Returns parent class, the parent of a Class is always another class */  
UClass* GetSuperClass() const  
{  
    return (UClass*)GetSuperStruct();  
}
...
/** Struct this inherits from, may be null */  
UStruct* GetSuperStruct() const  
{  
    return SuperStruct;  
}

UBlueprintGeneratedClass

UBluerpintGeneratedClass 는 UBlueprint 에서 만들어지는 Class 이다. 이 내용은 UBlueprint with _C 를 다룰 다른 포스팅에서 자세히 살펴보기로 한다.

UStruct

실제적으로 Reflection 기능을 제공하는 Class 이다. 간략히 살펴보면

  • SuperStruct: 부모 Class 를 표현하는 UStruct(UClass) Instance 의 주소이다. 햇갈리면 안되는데 여기서 부모Class 란 이 UStruct(UClass)가 표현하는 UObjet Class 의 부모 UClass 에 대한 Intance 라는 것이다. 아래 ObjectDiagram 에서 다시 다룬다.
  • Children: 사실상 해당 Class 의 해당 UFUNCTION(...) 목록을 담고 있다.
  • ChildProperties: 사실상 해당 Class 의 UPROPERTY(...) 목록을 담고 있다.

FProperty

FField 를 상속 받아 UPROPERTY(...) 에 해당하는 변수 정보 메타데이터를 가지고 있다고 보면 된다.

UField 계열과 FField 계열 두가지가 있는데, UPROPERTY(...) 계열이 UE5.1 쯔음 해서 UField -> FField 계열로 변경 되었다. 참고하도록 하자.

UClass <-> UObject

UObject 를 상속받은 UMyObject Class 를 만들었다고 했을때, 엔진이 구동 되면 UClass Instance 인 'MyObject: UClass' 와 UMyObject 의 ClassDefaultObject(이하 CDO) 인 'Default__MyObject: UMyObject' 가 생성된다.

MyObject->GetClass(), UMyObject::StaticClass() 로 'UClass' instance 를 가져 올 수 있고 해당 UClass Instance 로 부터 'GetDefaultObject()' 함수를 통해 CDO 에 접근 할 수 있다.

ClassDefaultObject

Class Default Object 의 목적은 말 그대로 Class 의 Default 값을 제공 하는 것이다. 언리얼 엔진에서 CDO 값은 어떻게 결정될까? 우선 Class 멤버변수 선언부, 생성자 상속 구조를 통해서 결정되고, Config 로 채운다움, BlueprintGeneratedClass 에서는 Data값으로 다시 덮어 씌워진다. 이 Default 값은, Editor 에서 변수 값이 변경 되었을때 기본값으로 Back 하는 기능(특정 Instance 와의 차이점 비교) 과 UObject Instance 를 NewObject 로 생성하지 않고, UClass 에서 CDO 를 통해 값을 가져 갈 수 있는 기능등을 제공 한다.

예를들어, 'B_MyBlueprint' 라는 Blueprint 를 만들었다면 해당 'B_MyBlueprint' 의 Package 를 로딩하고 'UClass B_MyBlueprint' 을 읽어 온 다음 CDO 를 통해 값을 가져 갈 수 있다.

또한 Config 로 비슷한 동작을 수행할 수 있다.

UMyConfig

#include "MyConfig.h"
#include "Engine/Engine.h"
#include "Misc/ConfigCacheIni.h"

UCLASS(config = Game)
class MYPROJECT_API UMyConfig : public UObject
{
    GENERATED_BODY()

public:
    // Configuration value
    UPROPERTY(config)
    int32 MyConfigValue;
};

DefaultGame.ini

[/Script/MyProject.MyConfig]
MyConfigValue=42
void PrintConfigValue()
{
    // Get the CDO (Class Default Object) of UMyConfig
    UMyConfig* DefaultConfig = GetDefault<UMyConfig>();

    // Retrieve the config value
    int32 ConfigValue = DefaultConfig->MyConfigValue;

    // Log the value using UE_LOG
    UE_LOG(LogTemp, Log, TEXT("Config Value from UMyConfig: %d"), ConfigValue);
}

Object Diagram

특정 시점에서 Object Instance 관계를 살펴보자. 위 Diagram 에서 'ObjectName: ObjectType' 으로 표기 되어 있다.

  • ChildObj: UMyChild -> Class UMyChild 를 NewObject<UMyChild>() 를 통해 'ChildObj' 라는 이름으로 Instance 를 생성 했다.
  • MyChild: UClass -> UMyChild 의 메타정보를 가지고 있는 'UClass' 이다. 이 Object(Instance) 의 이름이 'MyChild' 이다.
  • MyParent: UClass -> Instance MyChild: UClass 에서 GetSuperClass() 로 얻어 올 수 있는 UMyChild 의 부모인 UMyParent 의 메타정보를 가지고 있는 UClass Instance 이다.
  • Default__MyChild: UMyChild -> UMyChild 의 Default Object 이다. 이 CDO->GetClass() 는 당연히 'UMyChild: UClass' 이다.
  • Default__MyParent: UMyChild -> UMyParent UClass 와 쌍을 이루는 CDO 이다.

그런데, 실제로는 여기에 한가지가 빠져 있다. 위의 Class Diagram 을 다시 보고 오자. UClass 도 UObject Child Class 인걸 알 수 있다. 'UObject' 에서 GetClass() 를 하면 해당하는 UClass Instance 를 얻어 올 수 있다. 그러면 MyChild: UClass Instance 에서 GetClass() 를 하면? 'Class: UClass' 가 나온다. 그기서 GetSuperClass() 를 하면? 'Struct: UClass' 가 나온다.

Unreal HowTo

그럼 Unreal 에서는 UClass 의 내용을 어떻게 채워 놓는걸까? 이게 여러분들이 익히 잘 알고 있는 Unreal Build Tool(이하 UBT)Unreal Header Tool(이하 UHT) 의 역할이고 이중 특히 UHT 가 UCLASS, UPROPERTY, UFUNCTION 을 파싱해서 메타데이터를 생성하는 Code를 Generated 한다. 다른 부분도 그렇지만 이 부분은 특히 필자도 자세히 알지는 못한다. 그래도 맛은 보고 가자.

Sample Code 'UMyParent' 에 있는 내용중 UMyParent::StaticClass() 코드가 어떻게 만들어 지는지 대략 살펴보고 , MyNumA, MyNumB, GetAddedNumA 의 정보가 UClass Instance 에 어떻게 기입 되는지 살펴보자.

당연히 이 마법같은 일은 아래 3개가 조합으로 이루어 진다. GENERATED_BODY 에서 기본 골격을 #define Macro 조합(이라고 쓰고 떡칠이라고 읽는다) 으로 뼈대를 만들고, 세부 뼈대 내용을 generated 된 코드로 채워 넣는 식이다. Project 이름이 EkUe54 일 경우 generated 된 코드는 아래 폴더에 만들어진다. EkUe54/Intermediate/Build/Win64/UnrealEditor/Inc/EkUe54/UHT/

  • GENERATED_BODY()
  • MyParent.generated.h
  • MyParent.gen.cpp

UMyParent::StaticClass() 를 찾아서

GENERATED_BODY()UE_5.4/Engine/Source/Runtime/CoreUObject/Public/UObject/ObjectMacros.h

GENERATEDBODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,,LINE,_GENERATED_BODY);

이렇게 되어있는데, Source File 이름과 라인수를 조합해서 유니크한 이름의 GENERATED_BODY 골격을 만들어 낸다. 이 중 소스 파일이름과 LINE 넘버는 C++ Compiler 가 컴파일 시에 대체 시켜 준다. 어떻게 보면 목적 Code 를 바로 만들어 주는 C++ 의 몇 안되는 장점 중 하나다.

MyParent.generated.h 에 보면

#define FID_ProjectC_EkUe54_Source_EkUe54_MyParent_h_15_GENERATED_BODY \  
PRAGMA_DISABLE_DEPRECATION_WARNINGS \  
public: \  
    FID_ProjectC_EkUe54_Source_EkUe54_MyParent_h_15_RPC_WRAPPERS_NO_PURE_DECLS \  
    FID_ProjectC_EkUe54_Source_EkUe54_MyParent_h_15_INCLASS_NO_PURE_DECLS \  
    FID_ProjectC_EkUe54_Source_EkUe54_MyParent_h_15_ENHANCED_CONSTRUCTORS \  
private: \  
PRAGMA_ENABLE_DEPRECATION_WARNINGS

가 만들어져 있는데 따라서 MyParent.h 에 있는 GENERATED_BODY() 는 위 내용으로 대체 된다.

INCLASS_NO_PURE_DECLS 은 아래와 같이 되어있고

#define FID_ProjectC_EkUe54_Source_EkUe54_MyParent_h_15_INCLASS_NO_PURE_DECLS \  
private: \  
    static void StaticRegisterNativesUMyParent(); \  
    friend struct Z_Construct_UClass_UMyParent_Statics; \  
public: \  
    DECLARE_CLASS(UMyParent, UObject, COMPILED_IN_FLAGS(0), CASTCLASS_None, TEXT("/Script/EkUe54"), NO_API) \  
    DECLARE_SERIALIZER(UMyParent)

DECLARE_CLASS 는 ObjectMacros.h 에

#define DECLARE_CLASS( TClass, TSuperClass, TStaticFlags, TStaticCastFlags, TPackage, TRequiredAPI  ) \  
private: \  
    TClass& operator=(TClass&&);   \  
    TClass& operator=(const TClass&);   \  
    TRequiredAPI static UClass* GetPrivateStaticClass(); \  
public: \  
    /** Bitwise union of #EClassFlags pertaining to this class.*/ \  
    static constexpr EClassFlags StaticClassFlags=EClassFlags(TStaticFlags); \  
    /** Typedef for the base class ({{ typedef-type }}) */ \  
    typedef TSuperClass Super;\  
    /** Typedef for {{ typedef-type }}. */ \  
    typedef TClass ThisClass;\  
    /** Returns a UClass object representing this class at runtime */ \  
    inline static UClass* StaticClass() \  
    { \  
       return GetPrivateStaticClass(); \  
    } \
...

로 되어 있다. 짜잔... 드디어 UMyParent::StaticClass() 코드가 어디서 만들어 지는지 찾게 되었다.

MyNumA, MyNumB, GetAddedNumA 메타 데이터 기입을 찾아서

이 부분은 필자도 자세히 살펴 보지는 못했지만 위의 설명을 참고하면 어떤 동작들을 하는지 감을 잡기엔 충분 하리라 본다. 세부 내용은 엔진 업데이트마다 수시로 바뀐다. MyParent.gen.cpp 에 보면

static constexpr UECodeGen_Private::FMetaDataPairParam NewProp_MyNumA_MetaData[] = {  
    { "Category", "MyParent" },  
    { "ModuleRelativePath", "MyParent.h" },  
};  
static constexpr UECodeGen_Private::FMetaDataPairParam NewProp_MyNumB_MetaData[] = {  
    { "Category", "MyParent" },  
    { "ModuleRelativePath", "MyParent.h" },  
};
...
static const UECodeGen_Private::FIntPropertyParams NewProp_MyNumA;  
static const UECodeGen_Private::FIntPropertyParams NewProp_MyNumB;  
static const UECodeGen_Private::FPropertyParamsBase* const PropPointers[];  
static UObject* (*const DependentSingletons[])();  
static constexpr FClassFunctionLinkInfo FuncInfo[] = {  
    { &Z_Construct_UFunction_UMyParent_GetAddedNumA, "GetAddedNumA" }, // 3525413116  
};
...
const UECodeGen_Private::FIntPropertyParams Z_Construct_UClass_UMyParent_Statics::NewProp_MyNumA = { "MyNumA", nullptr, (EPropertyFlags)0x0020080000000005, UECodeGen_Private::EPropertyGenFlags::Int, RF_Public|RF_Transient|RF_MarkAsNative, nullptr, nullptr, 1, STRUCT_OFFSET(UMyParent, MyNumA), METADATA_PARAMS(UE_ARRAY_COUNT(NewProp_MyNumA_MetaData), NewProp_MyNumA_MetaData) };  
const UECodeGen_Private::FIntPropertyParams Z_Construct_UClass_UMyParent_Statics::NewProp_MyNumB = { "MyNumB", nullptr, (EPropertyFlags)0x0020080000000005, UECodeGen_Private::EPropertyGenFlags::Int, RF_Public|RF_Transient|RF_MarkAsNative, nullptr, nullptr, 1, STRUCT_OFFSET(UMyParent, MyNumB), METADATA_PARAMS(UE_ARRAY_COUNT(NewProp_MyNumB_MetaData), NewProp_MyNumB_MetaData) };  
const UECodeGen_Private::FPropertyParamsBase* const Z_Construct_UClass_UMyParent_Statics::PropPointers[] = {  
    (const UECodeGen_Private::FPropertyParamsBase*)&Z_Construct_UClass_UMyParent_Statics::NewProp_MyNumA,  
    (const UECodeGen_Private::FPropertyParamsBase*)&Z_Construct_UClass_UMyParent_Statics::NewProp_MyNumB,  
};

가 있다. 시간될 때 한번 살펴 보기 바란다. C++ 에서는 class 밖에 있는 global 한 변수는 모듈이 올라 올때 초기화 되는것을 알아두자.

profile
Game Client Programmer

0개의 댓글