이번 강좌에서는 하나의 모듈에서 다른 모듈을 참조하는 기능을 구현해보겠습니다. 

모듈간의 참조를 구현하기 위해서는 우선 언리얼 오브젝트의 초기화 과정에 대한 이해가 필요합니다. 


UCLASS, 리플렉션, 프레임워크


UClass에는 언리얼 오브젝트에 대한 클래스 계층 구조 정보와 멤버 변수, 함수에 대한 정보를 모두 기록

앞선 강좌에서 하나의 언리얼 오브젝트가 만들어지기 위해서는,
실제 컴파일 전에 언리얼 헤더 툴에 의해 헤더 파일을 분석하는 과정이 선행되며,
이 과정이 완료되면 Intermediate 폴더에 언리얼 오브젝트의 정보를 담은 메타 파일이 생성된다고 설명드렸습니다. 

언리얼 엔진이 컴파일 전에 먼저 메타 소스 파일과 헤더 파일을 생성하는 목적은 여러가지가 있겠지만,
기존의 C++ 문법에서 제공하지 못하는 런타임에서의 빠른 클래스 정보의 검색이라고 생각합니다. 

이 메타 정보는 언리얼 엔진이 지정한 UClass라는 특별한 클래스를 통해 보관됩니다. 

UClass에는 언리얼 오브젝트에 대한 클래스 계층 구조 정보와 멤버 변수, 함수에 대한 정보를 모두 기록하고 있습니다.

  하지만 단순히 검색하는 것에서 더 나아가, 런타임에서 특정 클래스를 검색해 형(Type)을 알아내, (-> 리플렉션)
인스턴스의 멤버 변수 값을 변경하거나 특정 인스턴스의 멤버 함수를 호출하는 것이 가능합니다. C++에서 말이죠. 
 

Java나 C#과 같은 C++ 다음 세대의 언어에서는 이와 유사한 기능을 리플렉션(Reflection)이라는 이름으로 제공합니다. 

정리하면 Java나 C#의 리플렉션 기능을 C++ 표준 문법에서는 제공하지 않으므로,
언리얼 엔진이 자체적으로 프레임웍을 만들어 제공한다고 이해하시면 되겠습니다.
 


클래스 기본객체

컴파일 단계에서 언리얼 오브젝트마다 UClass가 생성된다면,

실행 초기의 런타임 과정에서는 언리얼 오브젝트마다 클래스 정보와 함께 언리얼 오브젝트의 인스턴스가 생성됩니다. 

이 특별한 인스턴스는 언리얼 오브젝트의 기본 세팅을 지정하는데 사용되는데, 
이를 클래스 기본 객체 ( Class Default Object ) 줄여서 CDO라고 합니다. 


UCLASS 와 CDO

  언리얼 엔진에서 CDO를 만드는 이유는 언리얼 오브젝트를 생성할 때마다 매번 초기화 시키지 않고,
기본 인스턴스를 미리 만들어 놓고 복제하는 방식으로 메커니즘이 구성되어 있기 때문입니다. 

  지금 우리가 실습하는 단순한 언리얼 오브젝트라면 이러한 복제 과정이 불필요할 수도 있지만,
하나의 언리얼 오브젝트가, 예를 들어 복잡한 기능을 수행하는 캐릭터까지 담당할 정도로 기능이 확장되면,
굉장히 큰 덩어리의 객체로 커질 수 있습니다. 

  만일 게임 실행 중, 런타임에서 이 캐릭터를 한번에 100명을 스폰시킨다고 가정해봅시다. 
캐릭터를 하나씩 처음부터 생성하고 초기화시키는 방법보다, 미리 큰 기본 객체 덩어리를 복제한 후에 속성 값만 변경하는 방법이 보다 효과적이겠지요?

또 다시 정리하자면 하나의 언리얼 오브젝트가 초기화 될 때에는 두 개의 인스턴스가 항상 생성됩니다. 

아래는 이를 정리한 도식입니다.

 

하나의 언리얼 오브젝트에 생성되는 두 객체 UClass와 CDO 

 

이러한 언리얼 오브젝트는 언리얼 엔진에서 항상 모듈 단위로 관리됩니다. 
언리얼 에디터를 띄우면 초기화라는 문구 옆에 %가 증가하는 것을 볼 수 있는데,
대부분의 과정이 에디터에 사용할 모듈들을 로딩하는 사용됩니다.

모듈 간의 의존성에 따라 모듈이 로딩하는 순서가 정해지며, 모듈이 로딩될 때마다,
모듈에 속한 언리얼 오브젝트가 모두 초기화됩니다. 

 

에디터 로딩 화면

 


언리얼 오브젝트 로딩과정



앞선 강좌에서 언리얼 오브젝트 클래스에서 생성자는 특별한 역할을 가진다고 설명드렸는데,
언리얼 오브젝트의 생성자인스턴스를 초기화해 CDO를 제작하기 위한 목적으로 사용됩니다.

이 생성자 코드는 초기화에서만 실행되고 실제 게임 플레이에서 생성자 코드는 사용할 일이 없다고 보면 됩니다.
(언리얼 엔진에서 게임 플레이에서 사용할 초기화 함수는 생성자 대신 Init 이나 혹은 BeginPlay 함수를 제공합니다. ) 

모듈내 언리얼 오브젝트의 로딩은 아래 그림과 같은 순서로 진행됩니다. 

 

모듈내 언리얼 오브젝트의 진행

 

에디터가 사용하는 모든 모듈의 로딩이 완료되면 초기화 수치는 100%가 되며, 이 때서야 비로소 에디터가 뜨게 됩니다. 
이를 확인해보기 위해 ABGameInstance 코드에 아래와 같이 생성자 선언과 구현을 추가해봅시다. 

 

//생성자 만들고 추가
UABCGameInstance::UABCGameInstance()
{
	UE_LOG(LogClass, Warning, TEXT("%s"), TEXT("Game Instance Constructor Call!"));
}

 

코드를 추가하고 붉은 라인에 F9로 브레이크 포인트를 걸고 F5로 에디터를 실행해봅시다.
에디터 로딩이 완료되기 전에 브레이크 포인트가 걸림을 확인할 수 있습니다.

이는 모듈이 자신이 속한 언리얼 오브젝트 초기화를 진행하기 위해 CDO를 생성하기 위해 생성자 코드를 실행한 화면입니다. 저의 경우 약 71% 로딩 중에 브레이크 포인트가 걸렸습니다.

 

게임인스턴스 생성자 71% 브레이크 걸림

 


언리얼 엔진 로그디버깅


이번에는 WebService 모듈로 가서 WebConnection 언리얼 오브젝트에도 동일하게 생성자를 만들어줍시다.
마찬가지로 생성자에만 로그를 찍겠습니다.

다만 이번에는 조금 다르게 로그 출력을 위한 카테고리를 직접 지정해봅시다.

로그 카테고리를 생성하기 위해 언리얼 엔진은
h에서, 사용하는     DECLARE_LOG_CATEGORY_EXTERN  매크로와
Cpp에서, 사용하는 DEFINE_LOG_CATEGORY                매크로를 제공합니다.


코드는 다음과 같습니다. 

//WebConnection.h
UCLASS()
class WEBSERVICEK_API UWebConnection : public UObject
{
	GENERATED_BODY()

public:
	UWebConnection();

};
DECLARE_LOG_CATEGORY_EXTERN(WebConnection, Log, All);



//WebConnection.cpp
#include "WebConnection.h" 

DEFINE_LOG_CATEGORY(WebConnection);

UWebConnection::UWebConnection()
{
	UE_LOG(WebConnection, Warning, TEXT("WebConnection Constructor Call!"));
}

 

아래 그림과 같이 WebConnection 객체를 생성하고 로그를 찍어봅시다. 
모듈이 초기화된 후에 에디터가 로딩하므로 에디터에서 플레이 버튼을 누르지 않아도 생성자에 넣은 로그 코드가 실행된 것을 확인할 수 있습니다. 아래는 로그가 나온 결과입니다. 

 

에디터에 출력된 생성자 로그


 

생성자 코드에는 단순히 멤버 변수의 기본 값을 지정하는 데에도 사용되지만,
하나의 언리얼 오브젝트가 다른 언리얼 오브젝트를 생성하고 포함하는 형태로도 많이 사용됩니다.
즉 여러 개의 언리얼 오브젝트들이 합쳐진 거대한 언리얼 오브젝트를 생성하는 형태라고 보면 됩니다. 

이번에는 ArenaBattle 모듈에 있는 ABGameInstance 언리얼 오브젝트를 초기화할 때,
WebService 모듈에 있는 WebConnection 언리얼 오브젝트를 생성하고 이를 보조 언리얼 오브젝트로 만들어봅시다. 

이를 제작하기 위해서는 ArenaBattle 모듈이 WebService 모듈을 참조해야 합니다. 
우리가 지금까지 제작한 ArenaBattle 모듈과 WebService 모듈은 하나의 프로젝트에서 만들었지만

둘은 엄연히 물리적인 DLL파일로 분리된 전혀 다른 모듈이기 때문입니다. 

우리는 ArenaBattle 모듈에서 WebService 모듈 내의 WebConnection 언리얼 오브젝트를 사용해야 하기 때문에 ArenaBattle.Build.cs파일의 PublicDependencyModuleNames 프로퍼티에 WebService 모듈을 추가해줍시다. 

이렇게 Build.cs 파일을 설정하면 언리얼 빌드 툴에 의해 WebService의 Include와 Library 경로는
자동으로 ArenaBattle 모듈의 내부 빌드 설정에 추가됩니다.

이제는 아래와 같이 ArenaBattle 모듈 내 모든 폴더에서 WebService 모듈 내, Public과 Classes 폴더에 있는
헤더파일을 별도의 경로 지정 없이 바로 사용할 수 있게 되었습니다. 링크 과정도 마찬가지고요. 

//ABC.Build.cs

using UnrealBuildTool;

public class ABC : ModuleRules
{
	public ABC(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] {
        "Core", "CoreUObject", "Engine", 
        "InputCore", "HeadMountedDisplay", "WebServiceK" });
	}
}

 

이제 ABGameInstance에서 WebConnection을 선언하고 마음껏 사용해줍시다.
사용할 때 한가지 참고할 부분은 
언리얼 엔진에서는 언리얼 오브젝트를 생성하고 관리할 때 특별한 일이 없는 한 거의 대부분 포인터를 사용합니다.

"WebConnection.h" 헤더파일을 인클루드한 후 언리얼 오브젝트의 포인터를 정의해줍시다.
포인터로 동작할 때 가장 큰 문제는 메모리 관리라고 할 수 있는데,
멤버 변수에 UPROPERY 매크로를 사용해주면 언리얼 엔진이 알아서 메모리를 관리해줍니다. 

 

아래는 완성된 ABGameInstance의 헤더 파일입니다.
참고로 generated.h 헤더는 가장 마지막에 선언되어야 하는 규칙이 있다고 말씀드렸지요?
그래서 맨 마지막이 아닌 두 번째에 WebConnection.h를 추가했습니다. 

참고로 클래스 선언 시에 헤더 순서를 일일히 지정하는 번거로움을 없애기 위해
class 라는 전방 선언(Forward Declaration) 키워드를 추가했습니다.
언리얼 오브젝트 선언이 다 클래스 포인터이기 때문에 가능한 일이지요~

//ABCGameInstance .h
#include "Engine/GameInstance.h"
#include "WebConnection.h"
#include "ABCGameInstance.generated.h"

UCLASS()
class ABC_API UABCGameInstance : public UGameInstance
{
	GENERATED_BODY()
	

public:
	UABCGameInstance();
	virtual void Init() override;


	UPROPERTY()
	class UWebConnection* WebConnection;
};

 


인스턴스 생성


이제 소스 파일의 생성자 코드에서는 WebConnection의 인스턴스를 생성해서 멤버 변수로 지정해주어야 합니다.
A라는 언리얼 오브젝트가 초기화를 위해, B라는 언리얼 오브젝트를 생성할 때 B는 A의 서브오브젝트(Subobject)라고 합니다.

B의 외부 참조(Outer)는 A가 되고요. 따라서 생성자 코드에서 언리얼 오브젝트의 인스턴스를 생성하고,
관리하고자 한다면 언리얼 엔진이 제공하는 API인 CreateDefaultSubobject라는 API를 쓰는 것이 좋습니다.
참고로 게임 실행 코드에서는 NewObject를 사용해 언리얼 오브젝트의 인스턴스를 생성합니다. 

아래는 완성된 소스 파일 코드입니다.

//ABCGameInstance.cpp
#include "ABCGameInstance.h"

UABCGameInstance::UABCGameInstance()
{
	UE_LOG(LogClass, Warning, TEXT("Game Instance Constructor Call Start!"));
	WebConnection = CreateDefaultSubobject<UWebConnection>(TEXT("MyWebConnection"));
	UE_LOG(LogClass, Warning, TEXT("Game Instance Constructor Call End!"));
}

void UABCGameInstance::Init()
{
	Super::Init();
	UE_LOG(LogClass, Warning, TEXT("%s"), TEXT("Game Instance Init!"));
}

 

CreateDefaultSubobject 함수에서 사용하는
첫 번째 문자열 인자는 서브오브젝트를 관리하기 위한 내부 해시(Hash)값을 생성하는데 사용합니다.

따라서 아무 문자열 값을 사용해도 무방하지만, 다른 서브오브젝트를 생성할 때 이전에 사용한 값을 사용하면 안됩니다. 바로 뻗으니 주의하시기 바랍니다. 주로 코드를 복붙할 때 많이 에러가 발생합니다. 

 

이제 빌드를 걸어서 실행하면 아래와 같은 순서로 초기화가 진행됩니다. 

1. WebConnection 언리얼 오브젝트의 CDO생성   사용하는 모듈이 초기화되서 CDO가 생성되어 로그가 찍힘)

2. ABGameInstance 언리얼 오브젝트의 CDO생성 시작

3. WebConnection 언리얼 오브젝트의 CDO생성 (에디터와 독립된 게임을 시뮬레이션 해야하기 때문에 또 CDO를 생성)

4. ABGameInstance 언리얼 오브젝트의 CDO 생성 종료

로그를 확인해봅시다.

 

 

생성자 호출 순서 로그의 확인

 

두 모듈간의 의존성이 생기면서 아까와 반대로 WebConnection 모듈이 먼저 초기화됩니다.

 


 

WebConnection 2번 호출???

1) 모듈 DLL이 초기화될 때 언리얼 오브젝트의 UClass 인스턴스가 초기화되고 CDO가 생성됩니다.
추가로 CDO는 런타임에서 GetClass()->ClassDefaultObject로 가져올 수 있습니다. 

2) 게임 에디터도 일종의 언리얼 어플리케이션이다보니,
사용하는 모듈이 초기화되서 CDO가 생성되어 로그가 찍히고요,
플레이버튼을 누르면 에디터와 독립된 게임을 시뮬레이션 해야하기 때문에 또 CDO를 생성합니다.

1번은 모듈 초기화 떄
2번은 에디터 플레이 떄 ?

 

에디터에서 게임 실행시 호출화면

에디터에서 실행시에는 WebConnection 1번만 호출된다.

실행시 모듈을 만든게 빠져서, 1번만 호출된다?

 


[참고] [1-5] 클래스 기본 객체 (Class Default Object)

 

 

 

+ Recent posts