게임 개발/언리얼 C++

언리얼C++ - 언리얼 오브젝트 리플렉션 시스템 1

싹난 감자 2024. 11. 12. 21:25

언리얼 프로퍼티 시스템(리플렉션)

프로그램이 실행시간(런타임)에 자기 자신을 조사하는 기능.

리플렉션 시스템의 주요 장점

리플렉션 시스템이 있어야 여러 속성을 동적으로 관리할 수 있음.

 

정적 관리

 코드가 작성될 때 어떤 값이 설정되거나 고정되어 있음.

 PlayerName = "bar"; 같은 경우, PlayerName은 "bar"로 고정됨.

 코드가 실행될 때마다 "bar"로 설정된 값을 사용

동적 관리

 프로그램이 실행되는 도중에 다양한 조건에 따라 값이나 속성을 변경하거나 접근할 수 있도록 함.

 즉, 미리 고정된 것이 아니라, 상황에 따라 다르게 설정될 수 있음

 ex) 게임에서 NPC의 대화 내용 변경, 게임 내 아이템의 속성 변화, 캐릭터 속성 조정 등

 

코드가 특정 속성이나 메서드를 동적으로 다룰 수 있게 도와주어 속성을 수정하고 데이터를 관리하는데 유리함

리플렉션을 사용하면 더 쉽게 유지보수하고 확장할 수 있는 구조로 개발할 수 있음

리플렉션 시스템이 제공되지 않는 환경에서는 구현이 불가능하지는 않으나 더 번거롭다.

 

C++는 리플렉션을 지원하지 않아 언리얼에서 자체적으로 구축, 언리얼 오브젝트만 사용가능

 

C#의 Invoke, GetMethods, GetProperties, GetFields 등 동적으로 메서드를 호출하거나 메타 데이터를 얻는 것들이 사실 원래 되는게 아니라 리플렉션을 지원하기 때문에 되는 것.

리플렉션이 없으면 클래스와 메서드의 존재 여부를 런타임에 확인하거나 호출할 방법이 없음. 리플렉션을 활용하면 게임 내 모드나 플러그인등을 동적으로 로드하여 실행할 수 있음

C++에는 그런게 없어서 원래 안되는데 언리얼에서 구현해 둔 것.


리플렉션 시스템에 보이도록 했으면 하는 유형이나 프로퍼티에 주석을 달면 UHT가 그 프로젝트를 컴파일할 때 해당 정보를 수집.

리플렉션이 있는 유형으로 마킹하려면 generated.h를 include해야함

 

GENERATED_BODY()를 선언해주고

UFUNCTION()와 UPROPERTY() 등의 매크로 사용

UPROPERTY로 지정된 멤버 변수는 프로퍼티(속성)이라고 함

UPROPERTY() 안에 EditAnywhere와 Category=Pawn과 메타 데이터를 추가하면 에디터와 연동해 게임을 제작할 때 활용할 수 있음

모든 멤버 변수를 UPROPERTY로 설정할 필요는 없지만 리플렉션되지 않은 UObject 포인터 그대로를 저장하면 가비지 컬렉터가 관리해주지 않는다.

UPROPERTY로 설정하지 않으면 직접 메모리를 관리해줘야함

 

UHT는 실제 C++ 파서가 아닌 언리얼 엔진에서 코드를 자동으로 생성하는데 필요한 기능만 제공하는 것, 실제 C++ 코드를 분석해서 컴파일을 진행하는 것이 아니기 때문에 정교하지 않음. 너무 복잡한 유형은 UHT가 읽어들이지 못함

 

프로퍼티 시스템의 계층 구조

  • UFieldUClass(C++ class)UFunction(C++ function)UProperty(C++ member variable or function parameter)
  • (Many subcalsses for different types)
  • UEnum(C++ enumeration)
  • UScriptStruct(C++ struct)
  • UStruct

UStruct라는 구조체에서부터 리플렉션이 시작된다

UClass라는 언리얼 오브젝트를 정리하는 클래스의 경우 UStruct를 상속받아 함수나 프로퍼티를 추가해 전체 클래스 정보를 구축한다.

구축된 언리얼 클래스는 StaticClass나 GetClass를 사용해 접근할 수 있다.

이 함수를 호출하면 리플렉션, UHT이 분석해서 만들어놓은 리플렉션 정보들을 보관한 특정 개체에 접근할 수 있다는 의미.

그렇게 객체에 접근해서 UProperty라는 언리얼 오브젝트를 사용해 순회하면 언리얼 오브젝트의 속성들을 조회하면서 값이나 데이터를 빼내올 수 있다.

 

각 유형에는 고유한 플래그 세트(부가 정보)들이 들어가 있어 리플렉션을 사용해 필요한 정보만 필터링해서 얻을 수 있다.

 

Unreal Build Tool(UBT)와 Unreal Header Tool(UHT)이 함께 컴파일이 되기 전에 소스를 분석해 전체 시스템이 자동으로 구축된다.

 

StaticClass같은 함수는 컴파일 타임에서 리플렉션 정보에 직접 접근할 수 있는 함수인데 이런 함수들은 generated.h 파일에 선언되어 있어 실제 선언할 때는 지정한 적 없는 함수이지만 UHT가 자동으로 생성해줌.


언리얼 오브젝트의 구성

  • 언리얼 오브젝트에는 특별한 프로퍼티와 함수를 지정할 수 있음
    • 관리되는 클래스 멤버 변수 : UPROPERTY
    • 관리되는 클래스 멤버 함수 : UFUNCTION
    • 에디터와 연동되는 메타데이터를 심을 수 있음
  • 모든 언리얼 오브젝트는 클래스 정보와 함께 함
    • 클래스를 사용해 자신이 가진 프로퍼티와 함수 정보를 컴파일 타임과 런타임에서 조회할 수 있음
  • 언리얼 오브젝트는 NewObject API를 사용해 생성.

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

언리얼 클래스 정보에는 클래스 기본 오브젝트(Class Default Object)가 함께 포함되어 있음

CDO는 언리얼 객체가 가진 기본 값을 보관하는 템플릿 객체

한 클래스로부터 다수의 물체를 생성해 게임 콘텐츠에 배치할 때 일관성 있게 기본 값을 조장하는데 유용하게 사용됨

CDO는 클래스 정보로부터 GetDefaultObject 함수를 통해 얻을 수 있음

UClass 및 CDO는 엔진 초기화 과정에서 생성되므로 콘텐츠 제작에서 안심하고 사용 가능

언리얼 오브젝트 처리

클래스, 프로퍼티, 함수에 적합한 매크로로 마킹하면 UClass, UProperty, UFunciton이 되어 언리얼 엔진이 관리하게 되며 언리얼의 고유한 처리 기능을 구현할 수 있음

프로퍼티 초기화

UProperty로 선언된 속성들은 자동으로 0으로 초기화됨. (가비지 값이 들어가지 않음)

또한 언리얼의 가비지 컬렉터가 자동으로 메모리를 관리해줌.

Serialization

UObject가 직렬화 될 때 UProperty로 명시하면 자동으로 언리얼 오브젝트에서 꺼내 읽고 쓰기가 가능

프로퍼티 값 업데이트

UClass의 CDO를 사용해 하나의 클래스를 여러 곳에서 사용할 때 기본 값을 효과적으로 관리할 수 있음. CDO의 값을 변경하면 해당 클래스를 사용한 모든 인스턴스 값을 업데이트 해줌. 프리팹?

에디터 통합

UProperty 안에 메타 데이터를 넣으면 에디터에서 활용 가능

런타임 유형 정보 및 형변환

리플렉션 시스템을 사용하면 런타임에서 정보를 얻을 수 있고 안전하게 형 변환이 가능해짐

Super 키워드도 리플렉션 시스템이 있기 때문에 사용 가능한 것

Cast함수를 사용해 클래스 형변환을 할때에도 안전하게 할 수 있다.

if문을 사용해 실패 시 null을 보장해주기 때문에 안전하게 코딩이 가능하다

네트워크 리플리케이션

Serialization과 같이 네트워크 통신을 통해서도 해당 UProperty를 자동으로 전송하고 받을 수 있는 시스템이 구축되어 있음


예제

MyGameInstance 생성

 

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"

/**
*
*/
UCLSS()
class OBJECTREFLECTION_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()
public:
	UMyGameInstance(); //생성자로직
	
	virtual void Init() override;
	
private:
	UPROPERTY()
	FString SchoolName;
};

MyGameInstance.h

#include "MyGameInstance.h"

UMyGameInstance::UMyGameInstance() //생성자
{
}

void UMyGameInstance::Init()
{
	Super::Init(); //원래 로직 실행
	
	UE_LOG(LogTemp, Log, TEXT("========================="));
	UClass* ClassRuntime = GetClass();
	UClass* ClassCompile = UMyGameInstance::StaticClass();
	//check(ClassRuntime == ClassCompile);
	check(ClassRuntime != ClassCompile);
	
	
	//한글 인코딩 UTF-8 설정
	UE_LOG(LogTemp, Log, TEXT("학교를 담당하는 클래스 이름 : %s"), *ClassRuntime->GetName());
	UE_LOG(LogTemp, Log, TEXT("========================="));
}

 

 

UE_LOG(LogTemp, Log, TEXT("========================="));
UClass* ClassRuntime = GetClass();
UClass* ClassCompile = UMyGameInstance::StaticClass();
check(ClassRuntime != ClassCompile);
//강제로 check함수가 false를 반환하게 하면
//크래시가 발생하며 에디터가 종료된다.
//실제 게임으로 빌드할 때는 포함되지 않는다
//GetClass()와 UMyGameInstance::StaticClass()가 가리키는 객체가 같지 않으면 문제가 발생할 수 있음
//개발하며 check함수로 계속 확인해주어야한다

Assertion failld: ClassRuntime과 ClassCompile이 같지 않음

 

Assertion

해당 지점에서 개발자가 반드시 참(true)이어야 한다고 생각하는 사항을 표현한 논리식

어써션이 위반되는 경우(즉, 논리식 결과가 거짓)는 프로그램에 버그나 기타 문제가 있는 것을 암시

 

check함수로 인해 에디터가 종료되는 것이 불편하다면 ensure를 사용한다.

UE_LOG(LogTemp, Log, TEXT("========================="));
UClass* ClassRuntime = GetClass();
UClass* ClassCompile = UMyGameInstance::StaticClass();
//ensure(ClassRuntime != ClassCompile);
//ensureMsgf로 에러가 발생할 경우 메세지를 남길 수 있다.
ensureMsgf(ClassRuntime != ClassComplie, TEXT("일부러 에러를 발생시킨 코드"));

ensure()

ensureMsgf()

private:
	UPROPERTY()
	FString SchoolName; //초기화하지 않았기 때문에 빈 문자열

MyGameInstance.h

#include "MyGameInstance.h"

UMyGameInstance::UMyGameInstance() //생성자
{
	SchoolName = TEXT("기본학교"); //CDO 기본값 설정
}

void UMyGameInstance::Init()
{
	Super::Init(); //원래 로직 실행
	
	UE_LOG(LogTemp, Log, TEXT("========================="));
	UClass* ClassRuntime = GetClass();
	UClass* ClassCompile = UMyGameInstance::StaticClass();
	
	//한글 인코딩 UTF-8 설정
	UE_LOG(LogTemp, Log, TEXT("학교를 담당하는 클래스 이름 : %s"), *ClassRuntime->GetName());
	SchoolName = TEXT("청강문화산업대학교"); //개별적으로 생성된 것
	//객체의 값을 변경해도 기본 값은 없어지지 않음
	//ClassDefaultObject 템플릿 객체에 저장되어 있음
	
	UE_LOG(LogTemp, Log, TEXT("학교 이름 : %s"), *SchoolName); //청강문화산업대학교
	UE_LOG(LogTemp, Log, TEXT("학교 이름 기본값: %s"), *GetClass()->GetDefaultObject<UMyGameInstance>()->SchoolName);
	//클래스 정보에서 바로 형변환이 되어 디폴트 오브젝트를 가져올 수 있음
	UE_LOG(LogTemp, Log, TEXT("========================="));
}

MyGameInstance.cpp

 

컴파일 전

 

생성자에서 기본값을 저장했지만 디폴트 오브젝트 값을 출력하면 빈 문자열이 나옴

CDO는 에디터가 활성화되기 전에 초기화되기 때문에 생성자에서 값을 설정해도 에디터에서 인식하지 못하는 경우가 있음

헤더 파일에서 리플렉션 정보의 구조를 변경하거나 생성자 코드에서 CDO의 기본값을 변경하는 경우 에디터를 끄고 컴파일해서 다시 실행해주는 것이 안전.

컴파일 후

 

 

 

CDO 기본값 설정 부분에 브레이크 포인트를 걸면

에디터 로딩이 75%가 지난 시점에 걸림

엔진이 초기화되는 과정에서 CDO와 UClass 정보들이 만들어진다는 뜻.

이러한 것들이 온전히 생성된 이후에 에디터나 게임이 가동된다.

 

 

 


이득우의 언리얼 프로그래밍 Part1 | 인프런

https://www.inflearn.com/course/%EC%9D%B4%EB%93%9D%EC%9A%B0-%EC%96%B8%EB%A6%AC%EC%96%BC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-part-1/dashboard

 

이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해 강의 | 이득우 - 인프런

이득우 | 대기업 현업자들이 수강하는 언리얼 C++ 프로그래밍 전문 과정입니다. 언리얼 엔진 프로그래머라면 게임 개발전에 반드시 알아야 하는 언리얼 C++ 기초에 대해 알려드립니다., [사진] 언

www.inflearn.com