UE5) 창의력은 만점! 언리얼의 생성자 호출

JellyPower·2024년 10월 17일
0

언리얼 메모장

목록 보기
4/4
post-thumbnail

문득 궁금해졌다

  • 회사에서 일을 하다가 문득 “언리얼은 어떻게 생성자를 호출해주지?”가 궁금해졌다.
  • 생각해보자. 언리얼은 C++을 쓰고 C++은 기본적으로 리플렉션을 지원하지 않는다.
  • 그 말인 즉슨 내가 만든 class ATempCharacter : public ACharacter 라는 클래스는 실제 C++ 코드로 new ATempCharacter() 를 명시해주지 않는 한, 내가 만든 ATempCharacter라는 클래스를 인스턴스화 하고 생성자를 호출할 수 없는 것이다.
  • 아무리 언리얼이 UHT를 통해 리플렉션을 지원한다 한들, C++은 언어 자체가 생성자를 호출하려면 해당 객체를 무조건 명시적으로 코드를 통해 생성해줘야 하도록 돼있다. 게다가 C++은 생성자의 함수 포인터를 얻어올 수 있는 방법도 지원하지 않기 때문에 이는 더욱이 불가능하다.
  • 그런데, 언리얼은 이걸 해내고 있다. 그 증거로 우리는 직접 코드에
    new ATempCharacter() 를 명시해주지 않아도 블루프린트 클래스를 World에 드래그 앤 드랍만 해도 해당하는 하위 객체를 스폰할 수 있다.
  • 과연 어떻게 하는 것일까? 나는 너무 궁금해 언리얼 ACharacter클래스의 생성자에 중단점을 걸어두고 콜스택을 추적해봤다.

충격적인 결과…

  • 물론 아래 보여줄 코드는 언리얼 엔진의 코드와는 완전히 동일하지 않다. 언리얼은 기본적으로 템플릿과 매크로로 떡칠이 돼있기에 이해하기 쉽지않아 이를 풀어 쓴 코드라고 생각하면 된다.
  • 그러나 기본적인 원리는 아래와 같은 구조로 돼있다.
#include<stdio.h>
#include<unordered_map>

class TestClass {
public:

	// 생성자의 함수 포인터를 따오는 것과 똑같이 동작하게 하기 위해 __DefaultConstruct 함수를 만들고
	// placement new를 통해 생성자를 명시 호출해줍니다.
	void __DefaultConstruct(const char* InStr)
	{
		new(this) TestClass(InStr);
	}

	TestClass(const char* InStr) {
		printf("TestClass: %s\n", InStr);
	}

	void Print(const char* InStr)
	{
		printf("%s\n", InStr);
	}
};

int main()
{
	typedef void(TestClass::* ConstructorPtr) (const char* InStr);

	ConstructorPtr funcPtr = &TestClass::__DefaultConstruct;

	// 여기서 할당하고
	TestClass* Data = (TestClass*)malloc(sizeof(TestClass));
	// 이렇게 함수 포인터를 호출해줍니다.
	((Data)->*funcPtr)("asdf");

	return 0;
}
  • 위와 같은 방식으로 생성자의 함수 포인터와 클래스 사이즈를 UHT를 통해 generated.h 에 구워서 UClass* 에 데이터로 담아주면 인스턴스의 생성을 코드에서 명시적으로 해주지 않아도 똑같은 결과를 내줄 수 있다.

솔직히…

  • 저 코드를 찾아보면서 정말 이마를 탁 쳤다. 저런식으로 창의적인 코드를 짤 수 있다니…
  • 사실 좋은 방향으로 창의적이라는 뜻은 아닙니다… 개인적으론 좀 거시기한 코드라고 생각해요… 하하하
profile
게임엔진코드싸개(진)

0개의 댓글