futurelabunseen / B-JeonganLee

UNSEEN 2nd Term Learning and Project Repo.
5 stars 0 forks source link

5강: 언리얼 오브젝트 리플렉션 시스템 I #26

Closed fkdl0048 closed 6 months ago

fkdl0048 commented 6 months ago

5강: 언리얼 오브젝트 리플렉션 시스템 I

언리얼 리플렉션 시스템

리플렉션(Reflection)은 프로그램이 실행시간에 자기 자신을 조사하는 기능입니다. 언리얼의 핵심기능이다.

C++은 리플렉션을 지원하지 않기 때문에 앞서 4강에서 말한 기능들을 언리얼에서 사용하기 위해 이 리플렉션 시스템을 자체적으로 구현하였다.

리플렉션 시스템은 옵션이므로 필요에 의해 사용하면 된다. 만약 보이고 싶은 유형이나 프로퍼티에 사용하면 UHT(Unreal Header Tool)가 프로젝트를 컴파일 할 때 해당 정보를 수집한다.

정리하자면 #include "FileName.generated.h"를 통해 UHT가 해당 파일을 컴파일할 때 리플렉션 정보를 수집한다. 이후에 언리얼 매크로를 사용하면 적절한코드를 Intermediate폴더에 생성해준다.

UPROPERTY매크로를 사용한 변수를 프로퍼티라고 칭하며, 해당 매크로 속성으로 지정된 값을 메타데이터라고 칭한다. 해당 매크로를 꼭 사용하여 변수를 다루는 것은 아니지만, 사용하지 않는다면 언리얼 엔진 시스템에서 관리 받지 못한다.

추가적으로 UHT는 실제 C++ 파서가 아니기에 정교하지 않다. 따라서 너무 복잡한 유형은 언리얼 헤더 툴이 처리하지 못할 수 있다.

C#에선 변수에 접근을 하기 위한 함수정도로 생각하면 될 것 같다.

프로퍼티에 대한 계층구조는 다음과 같다.

대략적으로 UStruct부터 리플렉션 기능이 시작된다고 보면 된다.

중요한 점은 이 리플랙션 시스템을 사용하여 구성된 언리얼 엔진이기에 이러한 기능을 이해하는 것이 중요하다.

언리얼 오브젝트의 구성

언리얼 오브젝트의 클래스 기본 오브젝트

언리얼 오브젝트 처리

클래스, 프로퍼티, 함수에 적합한 매크로로 마킹해 주면 UClass, UProperty, UFunction 으로 변합니다. 그러면 언리얼 엔진이 접근할 수 있어, 다수의 내부적인 처리 기능을 구현할 수 있습니다.

C++는 생성자를 통해 초기값을 지정해야 하지만, UPROPERTY매크로를 사용하면 자동으로 초기화된다. (0으로 초기화)

가비치 콜렉터의 이점을 받을 수 있다(매크로를 붙여야 함), 직렬화할 수 있다.(물론 UPROPERTY매크로를 붙여야 함)

클래스 디폴트 오브젝트를 통해서 여러 객체의 기본값을 효과적으로 관리할 수 있다. 즉, 필드에 배치된 몬스터의 객체에 대해서 100마리의 개개인의 객체를 수정하는 것이 아닌 클래스 디폴트 오브젝트를 통해 한번에 수정할 수 있다. (기본값을)

유니티에선 프리팹의 개념에서 디폴트인, 베이스 프리팹의 업데이트로 봐도 좋을 것 같다. 마찬가지로 리플랙션의 기능이다.

UObject는 실시간으로 형변환을 할 수 있다.. (안전함)

멀티에서도 자동으로 내부에서 구축이 되어 있기에 해당 프로퍼티를 관리하기에도 적합하다.

실습관련

클래스 정보 가져오기

void UMyGameInstance::Init()
{
 Super::Init();

 UE_LOG(LogTemp, Log, TEXT("========================="));

 UClass* ClassRuntime = GetClass(); // 런타임에 클래스 정보를 가져온다.
 UClass* ClassCompile = UMyGameInstance::StaticClass(); // 컴파일 시점에 클래스 정보를 가져온다.
 check(ClassRuntime == ClassRuntime); // 어서션함수로 두 클래스가 같은지 확인한다.
  ensure(ClassRuntime != ClassRuntime);

 UE_LOG(LogTemp, Log, TEXT("학교를 담당하는 클래스 이름 : %s"), *ClassRuntime->GetName()); // Check가 통과한다면 클래스 이름을 출력한다.

 UE_LOG(LogTemp, Log, TEXT("========================="));
}

UObject를 상속받은 클래스에서 GetClass()함수를 통해 런타임에 클래스 정보를 가져올 수 있다. 이를 통해 클래스 정보를 가져올 수 있다. 따로 컴파일 시점에 클래스 정보를 가져오는 방법도 있다.

check함수를 통해 두 클래스가 같은지 확인할 수 있다. 이를 통해 클래스 정보를 확인할 수 있다. 어서션함수는 디버깅에 매우 유용하다. (실제 빌드과정에선 제외되기에 테스트 코드 형태로 작성하거나 실제 로직에 사용해도 될 듯 하다.)

ensure함수도 있다. 이는 어서션함수와 비슷하지만, 릴리즈 빌드에서도 동작한다. 디버깅에 매우 유용하다. 즉 에디터가 꺼지지 않는다. 빨간색 에러로 알려준다.

CDO와 인스턴스의 관계

#include "MyGameInstance.h"

UMyGameInstance::UMyGameInstance()
{
 SchoolName = TEXT("기본 학교");
}

void UMyGameInstance::Init()
{
 Super::Init();

 UE_LOG(LogTemp, Log, TEXT("========================="));

 SchoolName = TEXT("세종대학교");

 UE_LOG(LogTemp, Log, TEXT("학교 이름 : %s"), *SchoolName); // 학교 이름을 출력한다.
 UE_LOG(LogTemp, Log, TEXT("학교 이름 기본값: %s"), *GetClass()->GetDefaultObject<UMyGameInstance>()->SchoolName); // 기본값을 출력한다.

 UE_LOG(LogTemp, Log, TEXT("========================="));
}

위쪽 생성자에서 만진 SchoolName은 CDO의 값을 만진 것이다. 따라서 GetDefaultObject함수를 통해 CDO의 값을 가져올 수 있다. 그 이후에 런타임에 변경한 SchoolName은 인스턴스의 값을 가져온 것이다.

정리하자면 한개의 클래스엔 하나의 CDO가 존재하며, 이를 통해 기본값을 관리할 수 있다. 이후에 인스턴스를 생성하면 CDO의 값을 복사하여 인스턴스를 생성한다. 즉, 객체마다의 값은 다를 수 있지만 CDO의 값은 동일하다.

헤더파일의 리플렉션 정보를 변경하거나, 생성자 코드에서 CDO값을 만져야 하는 경우엔 에디터를 끄고 컴파일해야 안전하다.

CDO가 초기화되는 시점은 약 에디터가 75%정도 로딩되었을 때이다. 즉, 엔진이 초기화되는 과정에서 CDO, 유클래스 정보들이 그때 만들어지는 것으로 이런 것들이 다 만들어진 후에 애플리케이션이 작동하는 것이다.

유니티와 매우 다른 방식으로 유니티는 툴 자체가 메인로직을 시작하는 시작점이라 할 때, 언리얼은 에디터로써의 역할만 하며, 메인은 코드가 전부이며 그 순서 또한 코드가 결정한다는 점이 새롭다.

정리