UCLASS, 리플렉션, 프레임워크
UClass에는 언리얼 오브젝트에 대한 클래스 계층 구조 정보와 멤버 변수, 함수에 대한 정보를 모두 기록
앞선 강좌에서 하나의 언리얼 오브젝트가 만들어지기 위해서는, 실제 컴파일 전에 언리얼 헤더 툴에 의해 헤더 파일을 분석하는 과정이 선행되며, 이 과정이 완료되면 Intermediate 폴더에 언리얼 오브젝트의 정보를 담은 메타 파일이 생성된다고 설명했다.
언리얼 엔진이 컴파일 전에 먼저 메타 소스 파일과 헤더 파일을 생성하는 목적은 여러가지가 있겠지만, 기존의 C++ 문법에서 제공하지 못하는 런타임에서의 빠른 클래스 정보의 검색이라고 생각한다. 이 메타 정보는 언리얼 엔진이 지정한 UClass라는 특별한 클래스를 통해 보관된다.
UClass에는 언리얼 오브젝트에 대한 클래스 계층 구조 정보와 멤버 변수, 함수에 대한 정보를 모두 기록하고 있지만 단순히 검색하는 것에서 더 나아가, 런타임에서 특정 클래스를 검색해 형(Type)을 알아내, (-> 리플렉션)
인스턴스의 멤버 변수 값을 변경하거나 특정 인스턴스의 멤버 함수를 호출하는 것이 가능하다.
Java나 C#의 리플렉션 기능을 C++ 표준 문법에서는 제공하지 않으므로, 언리얼 엔진이 자체적으로 프레임웍을 만들어 제공한다.
클래스 기본객체
컴파일 단계에서 언리얼 오브젝트마다 UClass가 생성된다면, 실행 초기의 런타임 과정에서는 언리얼 오브젝트마다 클래스 정보와 함께 언리얼 오브젝트의 인스턴스가 생성된다. 이 특별한 인스턴스는 언리얼 오브젝트의 기본 세팅을 지정하는데 사용되는데, 이를 클래스 기본 객체 ( Class Default Object ) 줄여서 CDO라고 한다.
UCLASS 와 CDO
언리얼 엔진에서 CDO를 만드는 이유는 언리얼 오브젝트를 생성할 때마다 매번 초기화 시키지 않고, 기본 인스턴스를 미리 만들어 놓고 복제하는 방식으로 메커니즘이 구성되어 있기 때문이다.
지금 우리가 실습하는 단순한 언리얼 오브젝트라면 이러한 복제 과정이 불필요할 수도 있지만, 하나의 언리얼 오브젝트가, 예를 들어 복잡한 기능을 수행하는 캐릭터까지 담당할 정도로 기능이 확장되면, 굉장히 큰 덩어리의 객체로 커질 수 있다.
만일 게임 실행 중, 런타임에서 이 캐릭터를 한번에 100명을 스폰시킨다고 가정해보자. 캐릭터를 하나씩 처음부터 생성하고 초기화시키는 방법보다, 미리 큰 기본 객체 덩어리를 복제한 후에 속성 값만 변경하는 방법이 보다 효과적이다 그러므로 하나의 언리얼 오브젝트가 초기화 될 때에는 두 개의 인스턴스가 항상 생성된다.
아래는 이를 정리한 도식이다.
이러한 언리얼 오브젝트는 언리얼 엔진에서 항상 모듈 단위로 관리된다.
언리얼 에디터를 띄우면 초기화라는 문구 옆에 %가 증가하는 것을 볼 수 있는데, 대부분의 과정이 에디터에 사용할 모듈들을 로딩하는 사용된다.
모듈 간의 의존성에 따라 모듈이 로딩하는 순서가 정해지며, 모듈이 로딩될 때마다, 모듈에 속한 언리얼 오브젝트가 모두 초기화된다.
언리얼 오브젝트의 생성자는 인스턴스를 초기화해 CDO를 제작하기 위한 목적으로 사용된다.
이 생성자 코드는 초기화에서만 실행되고 실제 게임 플레이에서 생성자 코드는 사용할 일이 없다고 보면 된다.
(언리얼 엔진에서 게임 플레이에서 사용할 초기화 함수는 생성자 대신 Init 이나 혹은 BeginPlay 함수를 제공한다. )
모듈내 언리얼 오브젝트의 로딩은 아래 그림과 같은 순서로 진행된다.
에디터가 사용하는 모든 모듈의 로딩이 완료되면 초기화 수치는 100%가 되며, 이 때서야 비로소 에디터가 뜨게 된다. 이를 확인해보기 위해 ABGameInstance 코드에 아래와 같이 생성자 선언과 구현을 추가해보자.
//생성자 만들고 추가
UABCGameInstance::UABCGameInstance()
{
UE_LOG(LogClass, Warning, TEXT("%s"), TEXT("Game Instance Constructor Call!"));
}
코드를 추가하고 붉은 라인에 F9로 브레이크 포인트를 걸고 F5로 에디터를 실행해보자.
에디터 로딩이 완료되기 전에 브레이크 포인트가 걸림을 확인할 수 있다. 이는 모듈이 자신이 속한 언리얼 오브젝트 초기화를 진행하기 위해 CDO를 생성하기 위해 생성자 코드를 실행한 화면이다. 예시의 경우 약 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를 추가했다.
//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)값을 생성하는데 사용한다. 따라서 아무 문자열 값을 사용해도 무방하지만, 다른 서브오브젝트를 생성할 때 이전에 사용한 값을 사용하면 바로 뻗으니 안된다.
이제 빌드를 걸어서 실행하면 아래와 같은 순서로 초기화가 진행된다.
- WebConnection 언리얼 오브젝트의 CDO생성 (사용하는 모듈이 초기화되서 CDO가 생성되어 로그가 찍힘)
- ABGameInstance 언리얼 오브젝트의 CDO생성 시작
- WebConnection 언리얼 오브젝트의 CDO생성 (에디터와 독립된 게임을 시뮬레이션 해야하기 때문에 또 CDO를 생성)
- ABGameInstance 언리얼 오브젝트의 CDO 생성 종료
로그를 확인해보자.
두 모듈간의 의존성이 생기면서 아까와 반대로 WebConnection 모듈이 먼저 초기화된다.
WebConnection 2번 호출???
1) 모듈 DLL이 초기화될 때 언리얼 오브젝트의 UClass 인스턴스가 초기화되고 CDO가 생성된다.
추가로 CDO는 런타임에서 GetClass()->ClassDefaultObject로 가져올 수 있다.
2) 게임 에디터도 일종의 언리얼 어플리케이션이다보니, 사용하는 모듈이 초기화되서 CDO가 생성되어 로그가 찍히고,
플레이버튼을 누르면 에디터와 독립된 게임을 시뮬레이션 해야하기 때문에 또 CDO를 생성한다.
1번은 모듈 초기화 떄
2번은 에디터 플레이 떄 ?
에디터에서 게임 실행시 호출화면
에디터에서 실행시에는 WebConnection 1번만 호출된다.
실행시 모듈을 만든게 빠져서, 1번만 호출된다?
'게임엔진 > Unreal' 카테고리의 다른 글
[Unreal] Actor 와 ActorComponent 의 개념 (vs. Unity 에서의 GameObject 와 비교) (0) | 2023.10.26 |
---|---|
[Unreal] 가비지 컬렉터 (GC) 정리 (0) | 2023.10.26 |
[Unreal] 언리얼 빌드 시스템 + Target.cs (0) | 2023.08.28 |
[Unreal] 스레드와 단일 스레드로 실행시키기 (-norenderthread) (0) | 2023.08.12 |
[Unreal] UENUM 메타데이터 종류 (0) | 2023.07.07 |