본문 바로가기

프로그래밍/TIL

자체 게임 엔진 개발하기 (HopStep Engine) - Reflection과 Property

 자체 엔진 만들기를 시작하면서 가장 먼저 목표로 삼았던 것은 리플렉션 시스템의 구현이다. 리플렉션이라는 개념은 C#을 사용하면서 어느 정도는 알고 있다고 생각했지만, 직접 구현을 하는 것은 그보다 더 깊은 이해도를 줄 것이라고 생각했다. 또 이 리플렉션 시스템이야 말로 UE4의 코어 시스템 중 하나라고 생각하기도 했고.

 

 리플렉션(Reflection)이라는 것은 거울에 비친 자기 자신을 보듯이, 시스템의 어떤 타입이 자기 자신이 어떻게 생겼는지를 알 수 있는 시스템이라고 설명할 수 있을 것 같다. 어떤 타입이 어떤 프로퍼티와 어떤 메서드를 가지고 있는지를 확인하고, 한 발 더 나아가서 동적으로 이를 런타임에 수정할 수 있는 시스템 구축이 나의 목표이다.

 

 C#에서는 어떤 타입의 리플렉션 정보는 해당 타입이 정의된 어셈블리가 가지고 있게 된다. 예전에 CLR via C#을 정리하면서 확인했었던 것 같다.

CLR via C# 23장 어셈블리 로딩과 리플렉션 (tistory.com)

 

CLR via C# 23장 어셈블리 로딩과 리플렉션

Notion에서 보기 이번 장에서는 컴파일 타임에 전혀 알지 못했던 타입에 대해 타입의 세부 정보를 알아내고 인스턴스를 생성하는 등의 방법(리플렉션)에 대한 것을 배운다. 이는 주로 동적으로 확

highfence.tistory.com

 

 그래서 해당 어셈블리 내부에 정의된 타입을 확인하고 싶다면 아래 코드와 같이 접근하면 되었다.

 

using System;
using System.Reflection;

public static class Program 
{
    public static void Main() 
    {
        string dataAssembly = 
            "System.Data, version=4.0.0.0, culture=neutral, PublicKeyToken ...";
        LoadAssemAndShowPublicTypes(dataAssembly);
    }

    public static void LoadAssemAndShowPublicTypes(string assemId)
    {
        // 현재 앱 도메인에 명시적으로 어셈블리를 로드한다.
        Assembly a = Assembly.Load(assemId);

        // 로드된 어셈블리로부터 public으로 노출된 각각의 타입별로 루프가 수행된다.
        foreach (Type t in a.ExportedTypes)
        {
            Console.WriteLine(t.FullName);
        }
    }
}

 이러한 어셈블리 <-> 타입의 관계가 상당히 편리하다고 느꼈기 때문에 내 엔진에서 또한 이러한 구조를 적용시킬 예정이다. 내가 알기론 언리얼에서는 이 어셈블리에 해당하는 역할을 해주는 객체가 없다고 알고 있기 때문에, 요 부분은 언리얼과 좀 더 차이가 생길 것 같다.

 


 

 리플렉션을 구현할 때 가장 먼저 생각나는 것은 Type 객체였다. HopStep엔진에서는 앞글자를 따서 HType 클래스로 선언해주었다. 언리얼에서 이와 동일한 역할을 하는 녀석은 UClass일 것이다. 런타임에는 UObjectBase::GetClass()로 접근할 수도 있고, 컴파일 타임에는 StaticClass() 메서드로 접근할 수도 있다.

 

 UClass는 UStruct : UField : UObject라는 상속 관계를 가지고 있다. 간략하게 상속관계에 얽힌 타입들의 역할을 살펴보았다.

 

 UField는 Metadata를 담당하고 있다. Unreal의 Metadata는 FName - FString 페어의 맵 타입이다. Editor Serializable한 값인 것 같은데, 음.. 이 정도로 상위 클래스라면 엄청나게 코어한 역할을 맡고 있을텐데 나는 아직 잘은 모르겠다.

 UStruct는 프로퍼티와 상속 관계를 담당하는 듯 하다.

 그렇다면 UClass는?? 함수와 Class Default Object(CDO)를 관리하는 것 같다.

 

 다시말해서 UClass는 메타데이터, 프로퍼티, 상속, 함수 그리고 CDO의 정보가 있는 타입 클래스다.

 그리고 간단하게 생각하면 나의 HType 클래스도 이러한 정의를 따라 구현 요소를 갖추면 된다는 것이다. 그리고 이 작업이 끝나면 직렬화(Serialization)을 여기 끼얹으면 된다.

 


 Type에 대한 작업을 하던 도중 Property를 만나게 되었다. 어렴풋이 생각하기에 Property는 자신이 어떤 유형의 Property인지에 대한 정보와 그 유형의 Value를 가지고 있어야 한다고 생각하고 있었다.

 

 그런데 언리얼 구현부를 살펴보니, Value에 대한 것은 딱히 없고 메모리 오프셋 관련 정보들만을 쥐고 있었다.

 

template <class TPropertyType>
class FProperty
{
public :

    TPropertyType* Value = nullptr;
    
    void SetValue(TPropertyType*);
    TPropertyType* GetValue();
}

 

 내 생각엔 뭔가 이런 구문들이 있어야할 것 같은데... 라고 생각하고 관련 사용처를 보다가 내가 잘못생각했음을 깨달았다. Property는 해당 인스턴스에 종속적인 객체가 아니라 해당 타입에 종속적인 객체여야 하는데, 특정 인스턴스의 Value를 가지고 있다는 것은 말이 안되잖아!

 

 언리얼의 프로퍼티들은 자기가 속한 타입의 컨테이너를 받아서 자기가 가지고 있던 메모리 오프셋 정보를 바탕으로 해당 인스턴스에서 자기가 위치하고 있을 메모리의 포인터를 반환하는 형식으로 구현이 되어있다.

 

void* ContainerVoidPtrToValuePtrInternal(void* ContainerPtr, int32 ArrayIndex) const
{
    return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
}

for (TFieldIterator Iter(GetClass()); Iter; ++Iter)
{
    auto Property = *Iter;
    if (FIntProperty* IntProperty = CastField<FIntProperty>(Property))
    {
        int32 Value = IntProperty->GetSignedIntPropertyValue(Property->ContainerPtrToValuePtr<int32>(Obj));
    }
}

 뭐 이런식이다. 해당 프로퍼티의 유형을 나타내기 위해 FProperty를 특수화한 IntProperty들 같은 클래스가 존재하고, 이 특수화 클래스들은 자신의 유형에 맞는 Value 캐스팅 메서드를 제안해준다.

 

 또 해당 클래스는 TFieldIterator를 통해 어떤 프로퍼티들이 있는지 순회해볼 수 있는데, 이는 추후에 좀 더 봐야겠다. 이를 위해 Property Chain 같은 게 존재하는 거 아닐지.

 

 그리고 이 Property들이 어떻게 담기는지를 확인하려면 Unreal Header Tool을 좀 봐야하나 싶다.

 

 


 

 관련 구조를 잘 정리해놓은 페이지를 하나 발견했다.

 텐센트의 영향인지 언리얼 엔진은 중국 페이지 중에 정리가 잘 되어있는 페이지가 참 많다고 느낀다.

 

UE4类型系统、语言修饰符和元数据 - 可可西 - 博客园 (cnblogs.com)

 

UE4类型系统、语言修饰符和元数据 - 可可西 - 博客园

在编译之前,通过UHT扫描头文件中特定的宏来生成相关代码(*.generated.h / *.gen.cpp),然后再一起编译链接进游戏,来生成类型系统、扩展语言修饰符和收集元数据UMetaData

www.cnblogs.com