로그디버깅 업그레이드

 


이번 시간에는 언리얼 오브젝트가 가지는 특징 중 하나인 리플렉션 기능에 대해 살펴보겠습니다.
이를 위해서 앞으로 로그를 많이 사용할 예정인데, 로그 매크로를 추가해 기존 로그 기능을 업그레이드하겠습니다.

//OOO 메인게임.h

#include "Engine.h"

DECLARE_LOG_CATEGORY_EXTERN(ArenaBattle, Log, All);

#define AB_LOG_CALLINFO (FString(__FUNCTION__) + TEXT("(") + FString::FromInt(__LINE__) + TEXT(")"))
#define AB_LOG_CALLONLY(Verbosity) UE_LOG(ArenaBattle, Verbosity, TEXT("%s"), *AB_LOG_CALLINFO)
#define AB_LOG(Verbosity, Format, ...) UE_LOG(ArenaBattle, Verbosity, TEXT("%s %s"), *AB_LOG_CALLINFO, *FString::Printf(Format, ##__VA_ARGS__)) 


//OOO 메인게임.cpp
#include "ArenaBattle.h"

DEFINE_LOG_CATEGORY(ArenaBattle);
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ArenaBattle, "ArenaBattle" );

 

이렇게 AB로 시작하는 매크로를 선언하고 컴파일을 하면 
이제 UE_LOG 매크로 대신 AB_LOG 혹은 AB_LOG_CALLONLY 매크로를 동일하게 사용해주면 됩니다.
아래는 새로운 매크로로 로그 코드를 변경한 예시입니다.
반드시 헤더를 추가해야 AB_LOG를 사용 할 수가 있다.

#include "ABC.h"
#include "ABCGameInstance.h"

UABCGameInstance::UABCGameInstance()
{
	AB_LOG(Warning, TEXT("Constructor Call Start"));
	WebConnection = CreateDefaultSubobject<UWebConnection>(TEXT("MyWebConnection"));
	AB_LOG(Warning, TEXT("Constructor Call End"));
}

void UABCGameInstance::Init()
{
	Super::Init();
	AB_LOG_CALLONLY(Warning);
}

 

이 매크로를 사용하면 아래 그림과 같이 로그 매크로를 사용한 시점의 함수와 라인 정보가 자동으로 함께 표시됩니다.

추가로 언리얼의 출력 로그 창에서 경고 메시지만 보이도록 필터링 하는 것도 가능합니다.
앞으로 강좌에서
함수 정보만 출력하고 싶은 경우에는 AB_LOG_CALLONLY
매크로를 추가 정보도 같이 포맷문자열로 출력하고싶은 경우에는 AB_LOG를 사용하겠습니다.


참고로 로그 매크로안에 설정할 수 있는 Verbosity 값은 총 Fatal, Error, Warning, Display등이 있습니다. Fatal을 사용하면 강제로 크래시를 일으킵니다. 하지만 로그에서 Fatal을 쓸바에야 check나 verify같은 Assertion 매크로를 사용하는게 좋습니다. 

언리얼에서 사용가능한 Assertion 매크로들은 아래 링크를 참고하시면 됩니다. 

https://docs.unrealengine.com/latest/INT/Programming/Assertions/




UClass

 

로그 기능이 준비되면 이제 본격적으로 언리얼 오브젝트의 타입 정보를 담고 있는 UClass에 대해 알아봅시다. 

UClass 정보는 컴파일타임이든 런타임이든 원할 때 아무 때나 가져올 수 있습니다.

컴파일타임에서는 언리얼 오브젝트 선언에 있는 StaticClass를 사용하고,
런타임에서는 언리얼 오브젝트 인스턴스에서 GetClass()함수를 사용해 가져올 수 있습니다.

(참고로 우리는 StaticClass함수를 선언한 적이 없지만, 이 함수는 언리얼 헤더툴에 의해 자동으로 생성됩니다. ) 

아래는 이를 사용해 본 코드입니다.
두 포인터의 값은 동일한 UClass 객체를 가리킵니다.

void UABCGameInstance::Init()
{
	Super::Init();
	AB_LOG_CALLONLY(Warning);


	UClass* ClassInfo1 = WebConnection->GetClass();       //UClass 런타임
	UClass* ClassInfo2 = UWebConnection::StaticClass();   //UClass 컴파일타임

	if (ClassInfo1 == ClassInfo2)
	{
	AB_LOG(Warning, TEXT("ClassInfo1 == ClassInfo2"));
	}
}

 
※ 빌드 중 오류 Trying to recreate class '%s' outside of hot reload! 발생
※ 그래서 빌드 폴더 삭제후 재실행 하니 잘됨

실행 화면 같다는 결과

 


리플렉션

 

UClass에는 언리얼 오브젝트의 타입 정보를 검색하고 함수를 호출하거나 속성에 값을 설정하는
리플렉션 기능이 있습니다.

다만 이를 위해서는 그냥은 안되고

언리얼 오브젝트 클래스의
멤버 변수에는 UPROPERTY 매크로를
멤버 함수에는 UFUNCTION 매크로를
지정해주어야 합니다.

이를 테스트하기 위해 WebConnection 언리얼 오브젝트에 아래와 같이 Host와 URI라는 속성을 문자열로 정의하고 인자가 없는 함수에 UFUNCTION 매크로로 등록해 준 후 CDO에서 속성의 기본 값을 지정하고, 함수는 호출 되었다는 로그를 남겨줍시다. 

 

//WebConnection.h

class WEBSERVICEK_API UWebConnection : public UObject
{
public:	
    UPROPERTY()
		FString Host;

	UPROPERTY()
		FString URI;

	UFUNCTION()
		void RequestToken();
}


////WebConnection.cpp
UWebConnection::UWebConnection()
{
    UE_LOG(WebConnection, Warning, TEXT("WebConnection Constructor Call!"));
    Host = TEXT("localhost:8000");
    URI = TEXT("/");
}

void UWebConnection::RequestToken()
{
    UE_LOG(WebConnection, Warning, TEXT("Request Token Call!"));
}

 

UClass에 등록된 속성과 함수들은 TFieldIterator에 의해 모두 검색이 가능합니다.

UProperty 타입을 사용하면 속성 값만 검색하는 것이 가능하며,
런타임에서 특정 인스턴스에 할당된 속성 값을 가져올 수 있습니다.

아래는 WebConnection 인스턴스에 들어있는 값을 출력한 예시입니다.

//ABCGameInstance.cpp
void UABCGameInstance::Init()
{
	Super::Init();
	AB_LOG_CALLONLY(Warning);


	UClass* ClassInfo1 = WebConnection->GetClass();
	UClass* ClassInfo2 = UWebConnection::StaticClass();
	if (ClassInfo1 == ClassInfo2)
	{
	 AB_LOG(Warning, TEXT("ClassInfo1 == ClassInfo2"));
	}

////추가
	for (TFieldIterator<UProperty> It(ClassInfo1); It; ++It)
	{

		AB_LOG(Warning, TEXT("Field : %s, Type : %s"), *It->GetName(), *It->GetClass()->GetName());

		UStrProperty* StrProp = FindField<UStrProperty>(ClassInfo1, *It->GetName());

		if (StrProp)
		{
			AB_LOG(Warning, TEXT("Value = %s"), *StrProp->GetPropertyValue_InContainer(WebConnection));
		}
	}

}

 

함수 목록은 TFieldIterator<UFunction>을 사용할 수 있으며, 
아니면 아래 코드와 같이
NativeFunctionLookupTable 배열을 사용해 현재 클래스에 어떤 C++ 함수가 있는지 파악할 수 있습니다.

  함수의 이름만 알면 FindFunctionByName 함수를 사용해 언리얼 함수 객체 UFunction을 얻어올 수 있으며 이를 사용해 특정 인스턴스 내 함수 호출이 가능합니다.
  아래는 리플렉션 기능으로 함수를 호출한 예시입니다.
이번 코드에서는 C++ 11부터 지원하는 범위기반 for 루프(Range based for loop)를 사용해 간결하게 표현해보았습니다.  auto문 캐스팅


범위기반 for 루프에 대해 궁금하시면 아래 링크를 참고하시는 것을 추천합니다.

https://www.unrealengine.com/ko/blog/ranged-based-for-loops 


//ABGameInstance.cpp
  for (const auto& Entry : ClassInfo1->NativeFunctionLookupTable)
   {
       AB_LOG(Warning, TEXT("Function = %s"), *Entry.Name.ToString());
       UFunction* Func1 = ClassInfo1->FindFunctionByName(Entry.Name);
       if (Func1->ParmsSize == 0)
       {
         WebConnection->ProcessEvent(Func1, NULL);
       }
    }

 

실행화면

Host 값과 Value이 출력된 결과

 




언리얼 오브젝트 클래스에서
UPROPERTY와 UFUNCTION 매크로로 지정된 멤버 변수와 멤버 함수는 모두 검색이 가능합니다.

그리고 이들 필드의 타입 정보이름만 알고 있으면,
특정 인스턴스 내 값을 변경하거나 함수를 호출하는 것이 가능합니다. 

 

 

[참고] [1-6] UClass와 리플렉션

+ Recent posts