C++를 사용한 언리얼 엔진을 학습하던 중, 오브젝트 생성 시 UObject::CreateDefaultObject, New Object<T>, UWorld::SpawnActor<T> 와같은 각기 다른 호출방식이 있었습니다. 이에 대해 어떠한 상황에서 해당 함수들을 호출하는지 학습하고 기록하고자 이번 포스팅을 하게 되었습니다. 또한, 생성자 호출 시 추가적으로 어떠한 과정이 이루어지는지에 대해 알아보던중 CDO를 알게되어 추가적으로 작성하게 되었습니다. 

 

CDO(Class Default Object)

CDO는 엔진이 실행되며 클래스가 로드 될 때 초기화, 생성되는 인스턴스화 되지 않은 기본값을 가지고 있는 객체입니다.

  • C++이 제공하지 않는 언리얼만의 추가 기능들을 제공한다.
  • UClass 클래스에서 추가기능(메타정보)들을 보관한다.
  • 메모리에 한번만 존재하며, 클래스가 로드될 때 자동으로 생성된다.
  • 언리얼 오브젝트를 생성할 때 매번 초기화 하지않고 CDO를 복제해서 사용한다.
    • 객체 생성시 초기값에 대한 일관성 유지 및 재사용으로 인한 생성 비용 감소

 

CDO 생성 및 기본 프로퍼티 값 확인 예시 코드

// AMyCharacter 클래스의 CDO에 접근
AMyCharacter* DefaultCharacter = AMyCharacter::StaticClass()->GetDefaultObject<AMyCharacter>();

// 기본 프로퍼티 값 확인
float DefaultHealth = DefaultCharacter->Health;

 

 

 

CreateDefaultSubObject

  • 클래스의 서브 객체(Ex : Compoenet)들을 생성하고 초기화 하는데 사용한다.
  • 클래스 생성자에서 호출된다.
    • BeginPlay()와 같은 생성자가 아닌 곳에서 호출 시 동적할당으로 생성하게되어 에러가 발생한다.
  • 언리얼 엔진에서 메모리를 관리한다.
  • 생성된 객체는 에디터 단계에서 조작할 수 있다.
// Camera Component와 SpringArm Component를 캐릭터의 컴포넌트로 추가
FPSCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
FPSSpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));

 

 

 

 

NewObject

  • 동적으로 객체를 생성할 경우 사용된다.
  • 런타임 단계에서 객체를 생성하고 싶을시에 사용한다.
  • 생성자 단계에서 생성하지 않는 객체들의 생성을 위해 일반적으로 사용된다.
// 런타임 환경에서 객체를 생성하고 싶을 때
// 생성자 이후 객체를 생성 시
UDynamicCreateObject* DCObject = NewObject<UDynamicCreateObject>(this, UDynamicCreateObject::StaticClass());

 

 

 

SpawnActor

  • 액터 객체를 월드에서 동적으로 생성하는 함수
    • 생성자 단계 이후에서 생성해야한다.
  • Transform 정보를 가지는 액터를 스폰하기 위해 사용된다.
  • 액터 라이프사이플에 따른 관리를 받는다.
// 런타임 시 월드에서 액터 스폰
FVector Location(0, 0, 100);
FRotator Rotation(0, 0, 0);
AMonsterActor* SpawnedActor = GetWorld()->SpawnActor<AMonsterActor>(AMonsterActor::StaticClass(), Location, Rotation);

 

 

 

정리

함수 역할 사용 예 주 용도
CreateDefaultSubobject 클래스의 서브 객체(컴포넌트 등) 생성 CreateDefaultSubobject<UCameraComponent>(TEXT("Camera")); 주로 클래스 내부에서 컴포넌트를 생성하고 기본값 설정
NewObject 동적 객체 생성 NewObject<UMyCustomObject>(this, UMyCustomObject::StaticClass()); 런타임에서 새로운 UObject 객체 생성, 주로 데이터 객체 생성
SpawnActor 월드에서 액터 객체 동적 생성 SpawnActor<AMyActor>(AMyActor::StaticClass(), Location, Rotation); 월드에 실제로 존재하는 액터 객체 생성, 게임에서 상호작용 가능

델리게이트 Delegate

C++ 오브젝트 상의 멤버 함수를 가리키고 실행시키는 데이터 유형

특정 객체가 해야 할 로직을 다른 객체가 대신 처리할 수 있도록 만드는 보편적인 설계의 개념

델리게이트는 임의 오브젝트의 멤버함수에 동적으로 바인딩시킬 수 있으며, 그런 다음 그 오브젝트에서 함수를 호출할 수 있습니다.

또한 값으로 전달하는 경우 Heap에 메모리를 할당하게되는데, 델리게이트는 참조 전달을 위주로 하기 때문에 복사에도 안전합니다.

 

특징 

  • 특정 이벤트나 동작이 발생시에 호출할 함수를 미리 등록해주는 콜백 메커니즘
  • C++의 함수포인터와 비슷한 역할
    • std::function이 싱글캐스트 델리게이트와 비슷하다.
  • 결합도를 낮추고 유연한 이벤트 처리를 할 수 있다.
  • 델리게이트 선언시 어떤 데이터를 전달하고 받을것인지 인자의 수와 타입의 설계에 따라 사용되는 매크로가 결정된다.

 

델리게이트 선언 매크로

DECLARE_[델리게이트 유형]_DELEGATE_[함수 정보(RetVal_OneParam...]

 

  • Simple : 함수 하나만 바인딩 가능
    • DECLARE_DELEGATE
  • Multicast : 여러개의 함수가 한 이벤트에 바인딩이 가능하다
    • DECLARE_MULTICAST_DELEGATE, DECLARE_DYNAMIC_MULTICAST_DELEGATE
  • Dynamic : 블루프린트에서 바인딩이 가능하며, 런타임에서 바인딩 과 언바인딩이 가능하다.
    • DECLARE_DYNAMIC_DELEGATE,DECLARE_DYNAMIC_MULTICAST_DELEGATE

 

유형

  • DECLARE_DELEGATE : 1:1 형태로 전달시에 사용하며, C++에서만 지원한다.
  • DECALRE_MULTICAST : 1:N 형태로 전달시에 사용되며, C++에서만 지원한다.
  • DECLARE_DYNAMIC : 1:1형태로 전달하며, 블루프린트에서도 바인딩이 가능하다.
  • DECLARE_DYNAMIC_MULTICAST : 1:N형태로 전달하며, 블루프린트에서도 바인딩이 가능하다.

함수 정보

  • 인자가 없고 반환값이 없는 경우 - DECLARE_DELEGATE
  • 인자가 하나, 반환값이 없는 경우 - DECLARE_DELEGATE_OneParam
  • 인자가 하나, 반환값이 있는 경우 - DECLARE_DELEGATE_RetVal_OneParam
    • RetVal의 경우 블루프린트 형식을 지원하는 DYNAMIC에서만 선언이 가능하다.

 

바인딩

UObject 또는 공유 포인터 클래스 멤버에 델리게이트를 바인드 할 경우 오브젝트에 대한 레퍼런스를 낮은 응집도를 유지할 수 있으며, 오브젝트 소멸시 IsBound(), ExecuteIfBound() 함수를 호출하여 처리할 수 있다.

  • Bind() : 기존 델리게이트 오브젝트에 바인딩
  • BindStatic() : 정적 함수에 바인딩
  • BindUObject() : UObject 멤버 함수에 바인딩
  • AddStatic() : 함수 바인딩

실행

  • Execute() : 바인딩 되어있는지 확인
  • ExecuteIfBound() : 반환값이 없는 델리게이트의 경우
  • IsBound() : 모든 바인딩된 함수를 호출한다. (MULTICAST에서만 가능하다.)
    // 델리게이트 선언
    DECLARE_MULTICAST_DELEGATE(FSimpleMulticastDelegate);
    
    // 델리게이트 객체
    FSimpleMulticastDelegate OnEventTriggered;

    void TriggerEvent()
    {
        // 모든 바인딩된 함수 호출
        OnEventTriggered.Broadcast();
    }
};

void MyFunction1()
{
    UE_LOG(LogTemp, Warning, TEXT("Function 1 Triggered!"));
}

void MyFunction2()
{
    UE_LOG(LogTemp, Warning, TEXT("Function 2 Triggered!"));
}

void TestMulticastDelegate()
{
    MyClass Instance;
    Instance.OnEventTriggered.AddStatic(&MyFunction1); // 함수 바인딩
    Instance.OnEventTriggered.AddStatic(&MyFunction2); // 또 다른 함수 바인딩
    Instance.TriggerEvent(); // 두 함수 모두 호출
}

 

 

 

 

 

 

 

 


https://dev.epicgames.com/documentation/ko-kr/unreal-engine/delegates-and-lamba-functions-in-unreal-engine

이득우의 언리얼 C++ 게임 개발의 정석

프로젝트를 진행하기 전에 내가 어떤 프로젝트를 만들고 어떤 기능을 구현할까.. 라고 생각을 하던 도중

제가 플레이 했던 FPS게임 중 벽이 파괴되어 사실적인 전투를 묘사할 수 있는 게임이 있었습니다.

이러한 기능들을 프로젝트를 진행할 때 구현하고싶어 다양한 방법을 찾아보았습니다.

해당 기법은 Destructible Mesh라고 부르며, 다양한 방법들이 있었습니다.

이번 포스팅에서는 블루프린트를 사용해 구현한 파괴가능한 메시들에 대해 알아보겠습니다.

 

Destructible Mesh

  • 물리적으로 파괴 가능한 3D 모델
  • 특정 이벤트에 의해 분해, 재구성되는 특성을 가진다
  • 충격, 폭발, 물리적 충돌 등을 통해 모델이 분해되는 방식으로 구현된다
  • 파괴된 상태는 메모리에 저장되어 게임 내 파괴된 물체를 다시 불러오는 작업을 할 수 있다.

 

장점 및 단점

  • 플레이어가 물체를 파괴할수있어 게임 내 몰입감을 높이고 물리적 상호작용을 더 다양한 방식으로 표현할 수 있다.
  • 런타임 환경에서 파괴하여 플레이 시 생동감있는 표현을 보여줄 수 있다.
  • 나무, 금속, 유리 등 각기 다른 재질들의 파괴방식을 다르게 구현하는데 있어 설계의 난이도가 높다.
  • 파괴 시 계산하는 작업에 있어 메모리 사용량이 증가하며 성능에 영향을 줄 수 있다.

 

Dynamic Mesh Component

  • 동적 변형이 가능한 메쉬
  • 런타임 환경에서 물리적 충격, 폭발 등으로 해당 메쉬를 동적으로 변경할 수 있다 - 1
  • 파괴된 조각들이나 물리적 충돌을 처리하는 방식에 있어 성능 최적화가 가능하다.
  • 해당 물체의 각 부분을 세밀하게 제어할 수 있기때문에 물리적 처리를 더 디테일하게 조절할 수 있다.
  • 런타임 환경에서 실시간 렌더링을 하는 과정 중 성능에 문제가 생길 수 있다 - 2

1. Dynamic Mesh Coponent를 활용한 Destructible Mesh

 

2. 실시간 렌더링 프레임 순간적으로 20~30프레임으로 변형된다. - https://link.springer.com/article/10.1007/s11042-022-13049-x

 

 

 

Dynamic Mesh Actor을 사용한 블루프린트 클래스 내 구현

1. BeginPlay

 

Allocate Compute Mesh : 동적 메쉬의 메모리 할당 및 변형 처리를 담당한다.

Append Sphere Box : Sphere와 Box 두 가지 다른 형태의 충돌 영역을 하나로 결합하며, Sphere와 Box가 충돌될 경우를 처리하기 위해 사용한다.

 

 

2.OnRebuildGenerateMesh

OnRebuildGeneratedMesh : 동적 메쉬가 재구성 시 발생하는 이벤트

Append Box : 여러개의 박스 충돌 영역을 결합하여 충돌범위를 최적화 하는데 사용한다.

Enable Complex as Simple Collision : 복잡한 메쉬 충돌들을 단순한 충돌체로 대체한다. 복잡한 Dynamic Mesh 모델에 대한 물리적 계산을 더 빠르게 처리할 수 있다.

 

3. Destruction(인터페이스)

Break Hit Result : Collision 발생 시 그것에 대한 정보들을 가지고 있는 구조체

  • Location : 충돌이 발생한 위치
  • Normal : 충돌 지점에서의 법선 벡터
  • Impact Point : 충돌이 발생한 좌표
  • Impact Normal : 충돌 지점에서 물체의 표면 법선
  • Hit Actor : 충돌한 액터
  • Hit Component : 충돌한 컴포넌트

Apply Mesh Boolean : Boolean 연산을 메쉬에 적용하는데 사용되는 함수로 메쉬의 결합, 차집합, 교집합의 작업을 설정할 수 있다.

  • Union : 두 메쉬가 겹치는 부분을 포함한 전체 메쉬를 생성한다.
  • Subtract : 한 메쉬에서 다른 메쉬의 영역을 빼고 남은 부분을 생성한다.
  • Intersection : 두 메쉬가 겹치는 부분만을 포함하는 새로운 메쉬를 생성한다.

https://link.springer.com/article/10.1007/s11042-022-13049-x

 

지난 포스팅에서 언리얼 컨테이너 라이브러리의 정의 및 특징 그리고 STL과의 차이점에 대해 알아보았습니다.

이번시간에는 지난 포스팅에 작성하지 못한 TMap, TSet을 이어서 작성해보도록 하겠습니다.

언리얼 컨테이너 라이브러리 + TArray

TSet

  • std::set과 유사한 기능을 제공한다.
  • 중복된 값을 허용하지 않는다.
  • 동적 가변 배열의 형태로 데이터가 이루어져있다.
  • 해시 테이블 형태로 해시 기반의 Key데이터가 구축되어있어 빠른 조회가 가능하다.
    • 시간복잡도 O(1)를 보장한다.
  • 동적 배열 사이에 데이터가 비어있을 수 있다. 순서가 보장되지 않는다.

 

TSet의 주요 특징은 해당 사항이 있었습니다.

또한 추가 / 제거 / 삽입하는 함수들의 대부분이 TArray와 동일하기 때문에, 그 외 다른 주요 함수들에 대해 알아보겠습니다.

 

 

슬랙 (Slack)

  • 할당된 메모리에 엘리먼트가 없는것
  • Reset() 또는 Empty를 사용하여 메모리 할당 해제 없이 모든 엘리먼트를 제거하여 슬랙을 만들 수 있다.
  • TSet을 비우고 엘리먼트 수가 같거나 적은 TSet들을 Append할 시 효율적이다.

정렬 (Sorting)

  • 정렬 이후 배열이 수정된다면, 해당 순서를 보장할 수 없다
  • Key를 사용해 검색을 해서 시간복잡도가 O(1)이기 때문에 특별한 이유가 없으면 정렬을 하지 않아도 된다.

 

TMap

  • std::unordered_map과 유사한 동작을 가지고 있다.
  • TSet과 같이 해시 기반의 Key를 가지고 있어 검색 및 삽입속도가 빠르다.
  • Key와 Value 구조로 되어있으며, Key는 중복될 수 없지만, Value는 중복될 수 있다.
    • TMultiMap은 중복된 키를 저장할 수 있다.
    • TMap은 동일 Key를 사용하면 기존 것에서 대체되며, TMultiMap은 새로 저장한다.
  • iterator를 사용하여 모든 키-값을 조회할 수 있다. 
  • 데이터를 삭제해도 재구축이 일어나지 않는다
    • 비어있는 데이터가 있을 수 있다.

KeyFuncs

한 유형에 operator== 와 멤버가 아닌 GetTypeHash 오버로드가 있는 한, 그 유형은 변경 없이 TMap 의 키 유형으로 사용해도 됩니다. 하지만 그 함수 오버로드 없이 유형을 키로 사용하고 싶은 경우가 있습니다. 이러한 경우, 별도의 커스텀 KeyFuncs 를 제공해 주면 됩니다. 키 유형에 대해 KeyFunc 를 만들려면, 다음과 같이 두 개의 typedef 및 세 개의 static 함수 정의가 필요합니다:

  • KeyInitType - 키 전달에 사용됩니다.
  • ElementInitType - 엘리먼트 전달에 사용됩니다.
  • KeyInitType GetSetKey(ElementInitType Element) - 엘리먼트의 키를 반환합니다.
  • bool Matches(KeyInitType A, KeyInitType B) - A 와 B 가 동일하면 true, 아니면 false 를 반환합니다.
  • uint32 GetKeyHash(KeyInitType Key) - 키의 해시 값을 반환합니다. 보통 외부 GetTypeHash 함수를 호출합니다.

언리얼 공식문서 참조 - https://dev.epicgames.com/documentation/ko-kr/unreal-engine/map-containers-in-unreal-engine

 

 

이상으로 언리얼 컨테이너 라이브러리 중 자주 사용하는 TMap, TSet들에 대해서 알아보았습니다.

그 외에도 다양한 자료구조들이 존재하며, 각 자료구조들에 존재하는 함수들에 대해서도 공식문서를 참조하여 알아볼 수 있습니다.

 

이번학습으로 언리얼 엔진을 사용한 게임 개발시에도 어떤 라이브러리를 사용하는게 효율적인지 알 수 있었습니다.

또한, 언리얼 내부구조에서도 이진 트리형태의 TArray혹은, TMap를 사용하여 이루어져있다는것을 알 수 있었습니다.

 

WinAPI환경에서 2D게임 개발을 할 시에는 자료구조가 필요할 시 STL에 있는 자료구조들을 자주 사용했습니다.

언리얼 엔진을 접하면서 STL의 자료구조들과 같이 언리얼 내부에 있는 자료구조들이 있는지에 대해 알아보았는데, 언리얼 컨테이너 라이브러리(Unreal Container Library)에 포함되어있는 TArray, TMap, TSet의 자료구조 외에도 다양한 클래스 및 구조체들이 있었습니다.

효율적인 리소스 사용을 위해서 어떤것들이 더 효율적인지 비교하며 학습한 내용들을 이번 포스팅을 통해 알아보겠습니다.

 

언리얼 컨테이너 라이브러리(UCL)

  • 언리얼에서 제공하는 대표 컨테이너 라이브러리
  • 언리얼 오브젝트를 안정적으로 지원하며, 다수의 오브젝트 처리에 효율적이다.
    • UCL의 라이브러리는 가비지컬렉션을 통해 메모리 관리가 가능하다.
  • 대표적인 라이브러리는 TMap, TSet, TArray가 존재한다.
  • 접두사 T는 Template Library를 의미한다.

 

UCL과 C++의 STL의 차이점

  • C++ STL은 게임외에도 사용할 수 있도록 범용적으로 설계되어 있다.
  • STL에는 많은 기능들이 포함되어 있어 컴파일 시 UCL보다 많은 시간이 걸린다.
  • STL은 런타임 오버헤드가 최소화 되어있으며, UCL은 STL보다 런타임 환경에서 오버헤드가 더 발생할 수 있다.
  • UCL은 언리얼 오브젝트에 특화되어있다. 즉, 게임 개발이라는 한정된 범위가 존재한다.
  • UCL은 게임 개발에 적합한 API와 기능들을 가지고 있어, 게임 제작에 최적화된 구조로 설계되어있다.
  • UCL은 디버거 및 로거와 연동할 수 있어 문제가 발생했을 때 원인을 쉽게 파악할 수 있다.
  • UCL은 UPROPERTY()를 사용하여 블루프린터와 연동하여 사용할 수 있다.

즉, 언리얼 엔진 환경에서의 게임 개발시에는 UCL을 사용하는것이 더 적합하다고 할 수 있습니다.

 

 

TArray

  • 동적 배열(Dynamic Array)클래스이다.
  • STL의 vector와 동작 원리가 유사하다.
  • 기존 Array의 특징과 같이, 데이터가 순차적으로 모여있기 때문에 메모리를 효과적으로 사용할 수 있다.
  • 임의 데이터를 접근하는데에 있어 빠른 속도를 보장한다. 시간복잡도 O(1)
  • 탐색, 삽입, 삭제시에는 시간복잡도 O(N)이다.
  • 중복된 값을 허용한다.

배열 만들고 채우기

	// 정의
	TArray<int32> IntArray;

	// Init : n의 값을 n번 채운다.
	IntArray.Init(10, 5);
	// IntArray == {10, 10, 10, 10, 10}

	
	// Add, Emplace : 배열의 가장 마지막 위치에 추가한다. (Dynamic Array이므로 최대 사이즈 고려 X)
	IntArray.Add(10);			// 값 타입의 경우 해당 자료형의 임시 값을 추가해 복사한다.)
	IntArray.Emplace(10);		// 임시 변수를 생성하지 않는다.
	

	FString Arr[] = { TEXT("TEXT1"), (TEXT("TEXT2")) };
	TArray<FString> StrArray;

	// Append : 해당 배열의 크기에 다수의 엘리먼트를 한꺼번에 추가한다.
	StrArray.Append(Arr, sizeof(Arr));
	// StrArray == {TEXT1, TEXT2};


	//Insert : 단일 엘리먼트 또는 엘리먼트 배열 사본을 주어진 인덱스에 추가한다.
	StrArray.Insert(TEXT("TEXT3"), 1);
	// StrArray == {TEXT1, TEXT3, TEXT2};

 

정렬 (Sort)

	TArray<FString> FStrArray = {TEXT("!"), TEXT("Hello"), TEXT("Apple"), TEXT("Boat"), TEXT("World")};
	FStrArray.Sort();		// 안정적이지 않다. FString의 경우 대소문자 구분없이 사전식 비교후 정렬을 실행한다.
	// FStrArray == {!, Apple, Boat, Hello, World};

	FStrArray.HeapSort();	// 안정적이지 않다. FString의 경우 FString의 길이별로 정렬하지만, 같은 크기의 경우 순서를 보장하지 않는다.
	// FStrArray == {!, Boat, Hello, Apple, World};

	FStrArray.StableSort();	// Merge Sort형태로 구현되어있다. 순서를 보장한다.(A->Z의 순서 정렬)
	// FStrArray == {!, Boat, Apple, Hello, World};

 

 

쿼리

	TArray<FString> StrArray = { TEXT("!"), TEXT("Hello"), TEXT("Apple"), TEXT("Boat"), TEXT("World") };

	// Num : 배열의 Element가 몇개인지 확인하기
	int32 Count = StrArray.Num();
	// Count == 5

	// GetData : 배열 내 Element에 대한 포인터를 반환받는다.
	FString* StrPtr = StrArray.GetData();
	// Index 별로 개별 접근이 가능하다.
	// StrPtr[0] == "!"
	// StrPtr[1] == "Hello"
	// ...
	// StrPtr[5] - undefined behavior

	// Element 값 받아오기
	FString Elem1 = StrArray[1];
	// Elem1 == "Hello"

	// IsValidIndex(n) : 특정 인덱스가 유효한지에 대한 검사
	bool isValid1 = StrArray.IsValidIndex(1);
	bool isValid6 = StrArray.IsValidIndex(6);
	// isValid1 == true
	// isValid6 == false

	// Contains : 배열에 특정 엘리먼트가 들어가있는지 조회하기
	bool bContain = StrArray.Contains(TEXT("Hello"));
	bool bContain2 = StrArray.Contains(TEXT("NotHello"));
	// bContain == true
	// bContain == false

 

 

제거

	TArray<int32> ValArr;
	int32 Temp[] = { 10, 20, 30, 5, 10, 15, 20, 25, 30 };
	ValArr.Append(Temp, UE_ARRAY_COUNT(Temp));

	// Remove : 배열에서 일치하는 엘리먼트를 모두 지운다.
	ValArr.Remove(20);
	// ValArr == {10,30,5,10,15,25,30}

	// RemoveSingle : 배열에서 처음으로 일치하는 엘리먼트를 하나 지운다.
	ValArr.RemoveSingle(10);
	// ValArr == {10,5,10,15,25,30}

	// RemoveAt : 인덱스N의 엘리먼트를 지운다.
	ValArr.RemoveAt(2);
	// ValArr == {10,5,15,25,30}


	// RemoveAll : 일치하는 엘리먼트를 모두 지운다.
	ValArr.RemoveAll([](int32 Val) {
		return Val % 3 == 0;
		});
	// ValArr == {10,5,25}

	// Empty : 배열에 있는 모든 것을 제거한다.
	ValArr.Empty();
	// ValArr == []

 

힙(Heap)

  • TArray는 이진 힙 데이터 구조체를 지원하는 함수가 존재하며, 이진 트리 유형의 구조로 되어있다.
  • Heapify 함수룰 사용하여 기존 배열을 Heap으로 변환할 수 있다.
	TArray<int32> HeapArr;
	for (int32 Val = 10; Val != 0; --Val)
		HeapArr.Add(Val);
	// HeapArr == [10,9,8,7,6,5,4,3,2,1]
	

	// Heapify : 이진 힙으로 변경시킨다.
	HeapArr.Heapify();
	// HeapArr == [1,2,4,3,6,5,8,10,7,9]

 

Heapify() 결과 - 이진 트리 형태의 Heap으로 변경되었다.

 

 

 

	// HeapPust : 힙에 새로운 엘리먼트를 추가하며, 내부적으로 노드 순서가 변경된다.
	HeapArr.HeapPush(4);
	// HeapArr == [1,2,4,3,4,5,8,10,7,9,6]

HeapPush를 통해 Element 4를 추가한 경우

 

 

슬랙

  • 배열이 추가될 때마다 매번 재할당을 피하기 위해 얼로케이터는 요청보다 더 넉넉한 메모리를 제공하여 Add 호출시 재할당에 드는 퍼포먼스 비용을 물지 않도록 한다.
  • 엘리먼트를 삭제한다고해도 메모리가 해제되지는 않으며, 배열에 slack(여유분)을 가지고 있는다.
  • 초기 슬랙의 값은 0이며, GetSlack함수를 사용해 해당 배열의 슬랙 크기가 얼마나 되는지 알 수 있다.
  • Max 함수를 사용해 재할당이 일어나기 전까지 배열에 저장할 수 있는 최대 엘리먼트를 알 수 있다.
	TArray<int32> SlackArray;
	// SlackArray.GetSlack() == 0
	// SlackArray.Num()      == 0
	// SlackArray.Max()      == 0

	SlackArray.Add(1);
	// SlackArray.GetSlack() == 3
	// SlackArray.Num()      == 1
	// SlackArray.Max()      == 4

	SlackArray.Add(2);
	SlackArray.Add(3);
	SlackArray.Add(4);
	SlackArray.Add(5);
	// SlackArray.GetSlack() == 17
	// SlackArray.Num()      == 5
	// SlackArray.Max()      == 22

 

원시메모리

  • TArray는 할당된 메모리를 둘러싼 포장 단위
  • AddUninitialized, InsertUninitialized : 배열에 초기화 되지 않은 공간에 Element를 추가한다.
    • 해당 엘리먼트 타입의 생성자는 호출하지 않는다.
    • Memcpy 호출로 전체 구조체를 덮어 쓰려는 경우 생성자 호출을 피할 떄 효율적이다.

 

 

 

이상으로 언리얼 컨테이너 라이브러리가 어떤것인지, 어떤 특징을 가지고 있는지

그리고 STL과 어떤 차이점이 있는지 비교하며 알아보았습니다.

 

또한 언리얼 C++의 주요 컨테이너 라이브러중 하나인 TArray와 해당 라이브러리의 주요 함수들이 어떤것들이 있는지에 대해 알아보았습니다.

 

다음 포스팅에서는 이어서 TSet, TMap이 어떤 특징을 가지고 있는지, 그리고 STL의 어떤 자료구조와 유사한지에 대해 알아보겠습니다.

 


https://dev.epicgames.com/documentation/ko-kr/unreal-engine/array-containers-in-unreal-engine

https://designerd.tistory.com/entry/UE-%EC%96%B8%EB%A6%AC%EC%96%BC-C%EC%9D%98-TArray-TSet-TMap-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%99%80-%ED%99%9C%EC%9A%A9%EB%B0%A9%EB%B2%95

언리얼 엔진에서 제공하는 다양한 클래스들은 내부 GC(Garbage Collection)을 통해 메모리를 관리한다는 것을 지난 포스팅을 통해 알게되었습니다.

UE5 - Garbage Collection 

 

이에 추가적으로 레벨에 스폰된 액터가 언제 생성되며, 언제 사라지는지, 그리고 어떻게 더이상 사용하지 않는다 라고 판단하여 GC가 해당 액터를 소멸시키는지에 대해 조금 더 자세히 파악하고자 이번 포스팅을 작성하게 되었습니다.

이러한 이유로 UE5를 사용한 액터의 라이프사이클에 대해 알아보겠습니다.

 

액터 라이프사이클 분해도 - https://dev.epicgames.com/documentation/ko-kr/unreal-engine/unreal-engine-actor-lifecycle

 

액터 라이프사이클

 

액터 생성 단계 (Spawning)

  1. 디스크에서 로드하기 (Load Map / Add to World)
    • 레벨을 변경하거나 불러올 시 언리얼 엔진 내부에서 호출되는 UnrealEngine::LoadMap이 발생하거나, UWorld::AddToWorld를 호출하는 경우같은, 이미 레벨에 있는 액터들에 대해 발생한다.
    • 패키지/레벨에 있는 액터가 디스크에서 로드되는 단계
    • 디스크에서 로드가 완료되면, 직렬화된 액터에서 PostLoad를 호출한다.
    • 호출된 해당 액터의 컴포넌트가 초기화 된 후, 레벨이 시작될 때 해당 액터의 AActor::BeginPlay()가 호출된다.
  2. 에디터가 플레이 되는 경우 (Play in Editor)
    • 에디터의 액터가 새 월드에 복제된다.
    • UObject::PostDuplicate를 호출하여 해당 액터를 복제, 직렬화한다.
    • 해당 액터에 정의된 각 컴포넌트를 생성, 초기화한다.
    • 레벨이 시작될 때 AActor::BeginPlay()가 호출된다.
  3. 스폰 (Spawn Actor)
    • 플레이를 위해 액터를 설정하기위해 호출된다.
    • 월드에 스폰된 후 직렬화 및 액터 및 액터의 컴포넌트 생성 및 변수를 초기화 한다.
    • 액터에 정의된 각 컴포넌트를 생성, 초기화, 호출한다.
    • AActor::BeginPlay()를 통해 호출된다.
  4. 디퍼드 스폰 (Deferred Spawn)
    • Expose On Spawn (스폰 시 노출)로 설정된 프로퍼티가 존재한다면, 해당 액터는 디퍼드 스폰이 가능하다.
      • Deffered Spawn : 액터의 특정 설정을 초기화한 뒤, 활성화는 나중에 하는 방식.
      • 특정 이벤트가 발생시 호출하는 액터 또는 성능최적화를 할 때 사용한다.

 

게임 실행 단계 (Game Play)

  • 액터가 배치된 레벨 및 어플리케이션이 실행되고 있는 단계.
  • 해당 액터가 매 Tick(프레임)마다 호출되는 단계
  • 또는 데미지 처리와 같은 상태 업데이트를 하는 단계
  • 언리얼 엔진 GC에서 해당 객체들을 Mark하여 메모리 해제를 방지한다. 

 

액터 라이프사이클 종료 (End Play)

  • Destory 함수에 대한 명시적 호출을 한다.
  • 에디터에서 플레이가 종료된 경우
  • 레벨 트랜지션 발생 시
  • 액터의 수명이 만료될 경우
    • 수명이 다해가는것을 보장하기 위해 언리얼 엔진 내부에서 해당 엑터의 EndPlay를 여러곳에서 호출하여 검증한다. 
  • 어플리케이션 종료 (모든 액터 소멸)

위와같이 라이프사이클 종료를 확인하기위해 여러개의 검증을 걸친 뒤, 액터를 더이상 사용하지 않는다 판단 할 시 액터는 내부에서 RF_PendingKill로 표시되어 언리얼 엔진이 해당 액터의 메모리 할당을 해제합니다.

 

※ EndPlay가 호출 되어도 트랜지션한 레벨에서 해당 액터가 사용된다면, 액터는 재사용 될 수 있다.

 

GC에 의한 오브젝트 제거 및 리소스 해제

  • UObject::BeginDestroy : 오브젝트가 메모리를 해제하고, 게임 플레이가 아닌 기타 리소스들을 처리한다.
  • UObject::IsReadyForFinishDestroy : 가비지 컬렉션에서 해당 오브젝트가 영구적으로 제거될 준비가 되었는지 확인하며, false 반환 시 다음 가비지컬렉션까지 소멸을 연기시킨다.
  • UObject::FinishDestroy : 오브젝트가 실제로 소멸되며, 해당 함수 호출 뒤 메모리가 해제된다.

 

 

위의 4단계를 통해 액터가 생성되기에는 어떠한 방법이 있는지, 어떤 검증을 걸쳐 액터가 소멸된다고 판단하는지에 대해 알아보았습니다.

기존의 GC포스팅을 하였을 때 어떻게 엔진에서 소멸자를 담당하여 메모리를 효율적으로 사용하는지에 대해 알고 있었지만, 이번 포스팅을 통해 GC를 통해 마크 앤 스윕단계를 거치기 전에 액터의 생명주기를 알 수 있었습니다.

 


 

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/unreal-engine-actor-lifecycle#%EC%95%A1%ED%84%B0%EB%9D%BC%EC%9D%B4%ED%94%84%EC%82%AC%EC%9D%B4%ED%81%B4%EC%A2%85%EB%A3%8C

개요

 

언리얼 엔진을 다루는 게임 클라이언트 개발자가 되기위해 학습을 하던 중, 콘텐츠 드로어에서 생성할 수 있는 다양한 클래스 들이 있었습니다.

폰, 액터, 캐릭터, 오브젝트와 같은 기본 클래스들이 있었으며, 각각 빙의할 수 있다, 걸어다닐 수 있다, 월드에 배치할 수 있다. 와 같은 기본적인 내용들만 적혀있었습니다.

또한 C++ 클래스를 작성하였을 때에는 UPROPERTY, UCLASS, UFUNCTION와 같은 자바의 어노테이션과 같은 상단에 적혀있는 매크로들이 존재하였습니다.

이와같이 다양한 클래스들과 매크로같은 기능들이 존재하였으며, 언리얼 엔진을 의도한대로 다루기 위해서는 기본적인 요소를 파악해야 한다고 생각하여 포스팅을 작성하게 되었습니다.

 

매크로

언리얼 엔진의 매크로는 클래스, 함수, 속성 등의 정보를 블루프린터,리플렉션과 같은 에디터와 런타임에서 사용할 수 있도록 메타데이터로 선언하는 기능입니다.

즉, 언리얼 엔진으로 프로그래밍시 언리얼 엔진이 새 클래스, 함수, 변수를 인식할 수 있게 도와주는 기능입니다.

 

특징

  • 리플렉션(Reflection)
    • 매크로로 선언된 객체의 속성 정보들을 런타임시에 조회, 제어 할 수 있다.
  • 블루프린트 연동
    • C++ 클래스에서 선언한 함수, 변수, 클래스들을 블루프린트에서 조회, 사용할 수 있다.
    • Ex) 변수에 UPROPERTY(VisibleAnywhere)를 선언함으로써, 블루프린트 내에서 조회 할 수 있다.

 

UCLASS()

  • 클래스 선언부에 사용되며, 해당 클래스는 언리얼 엔진에서 추적하여 조회, 사용 할 수있게 선언한다.
  • 런타임 환경에서 특정 클래스를 조회, 인스턴스 멤버 변수,함수를 호출할 수 있게 도와준다.
  • 클래스 디폴트 오브젝트(Class Default Object, CDO)라는 하나의 오브젝트를 유지한다.
  • 매개변수 Blueprintable, BlueprintType, NotBlueprintable, Abstract 등을 통해 접근 제어할 수 있다.
    • Blueprintable : 블루프린트에서 상속 가능하도록 선언한다.
    • BlueprintType : 블루프린트 변수로 사용할 수 있다.
    • NotBlutprintable : 블루프린트에서 상속할 수 없다.
    • Abstract : 추상 클래스로 선언한다. 객체를 직접 생성할 수 없다.

UFUNCTION()

  • 함수 선언부에 사용되며, 함수에 대한 메터 데이터를 추가하여 블루프린트, 네트워크와 같은 기능들에 대해 제어한다.
  • 매개변수 BlueprintCallable, BlutprintImplementableEvent, BlueprintNativeEvent, Server, Client, NetMulticast 등이 있다.
  • BlueprintCallable : 블루프린트에서 함수를 호출할 수 있도록 한다.
  • BlueprintImplementableEvent : C++ 에서 구현하지 않아도 블루프린트에서 구현 할 수 있는 이벤트를 정의한다.
  • BlueprintNativeEvent : C++ 함수에서 구현하였지만, 블루프린트에서 재 구현이 가능하도록 한다.
  • Server, Client, NetMulticast : 각각 서버, 클라이언트, 모든 클라이언트에서 실행하도록 한다.

UPROPERTY()

  • 클래스 메버 변수에 메타데이터를 추가하여 언리얼 엔진에서 변수의 동작들을 제어하도록 도와준다.
  • EditAnywhere : 언리얼 에디터에서 수정할 수있도록 도와준다.
    • Ex) 블루프린트에서 해당 멤버변수의 값을 수정한다.
  • BlueprintReadWrite : 블루프린트에서 수정,사용할 수 있도록 선언한다.
  • Replicated : 네트워크에서 동기화 한다.
  • Transient : 변수를 저장하지 않고 런타임에서만 사용하도록 설정한다.

 

기본 클래스

언리얼 엔진에서 게임을 만들면 다양한 객체들이 있습니다.

플레이어, 주변환경(나무, 벽, 돌 등..), 장비(무기, 갑옷, 총알 등..)을 구현하게 되며, 그에 어울리게 구현되어있는 클래스들이 있습니다.

기본 클래스 중 가장 대표적인 Actor, Pawn, Character, Object들에 대해 알아보겠습니다.

 

UObject Class

  • 언리얼 엔진 라이브러리 모든 클래스들의 최상위 부모 클래스.
  • 컨텐츠를 구성하는 기본 단위
  • UObject를 상속받는 클래스들은 GC를 통해 메모리 관리를 할 수 있다.
  • 게임플레이에 필요한 로직, 데이터 구조와같은 게임에서 필요한 기본 기능들을 포함하고 있다.

 

AActor Class

  • 게임 월드의 Transform을 가지는 객체들의 행동을 구현시에 사용하는 클래스
  • 게임 내부의 물리적 충돌, 렌더링, 네트워크 등의 처리가 가능하다.
  • 컴포넌트 기반으로 설계되어 메쉬, 라이트와 같은 다양한 구성 요소들을 추가할 수 있다.
  • UObject를 상속받고 있다.
  • 동적객체, 정적객체들을 구현시에 사용한다.
    • 동적 객체(총알, 미사일, 차량 등..), 정적 객체(건물, 나무 등..)

 

APawn Class

  • 게임 월드에서 플레이어 혹은 AI에 의해 제어될 수 있는 Actor
    • PlayerController 또는 AIController을 통해 행동을 결정할 수 있다.
    • AActor클래스와 다르게 컨트롤러와 연결 할 수 있다.
  • AActor 클래스를 상속받고있다.
  • 카메라, 플레이어가 조작할 수 있는 탈것, AI를 통해 행동하는 캐릭터등에 사용한다.

 

ACharacter Class

  • 캐릭터의 이동, 애니메이션을 담당하는 클래스
  • APawn클래스를 상속받고 있다.
  • Component기반으로 설계되어 캐릭터의 이동, 물리적 충돌, 외형등을 런타임 환경에서 제어할 수 있다.
    • Capsule Component : 캐릭터의 충돌을 처리
    • Character Movement Component : 캐릭터의 이동관련 처리
    • Mesh Component : 캐릭터의 외형(스켈레탈 메쉬 등)을 표현
  • 플레이어 캐릭터, AI 캐릭터 등 이동이 필요한 것들

 

이상으로 클래스들에 필요한 기본적인 기능들에 대해 알아보았습니다.

다음 클래스 관련 포스팅은 객체(액터)가 언제 어떻게 생성되는지, 그리고 어떻게 사라지는지, 어떻게 관리하는지에 대한

언리얼 엔진의 객체 라이프사이클에 대해 알아보겠습니다.

렌더링 파이프라인은 CPU와 GPU의 자원을 사용하여 모델, 텍스처, 조명같은 3D 리소스들을 각각의 순차적인 과정을 통해 2D 이미지로 렌더링 하는 과정을 의미합니다.

언리얼 엔진에도 내부적으로 렌더링 파이프라인이 존재하며, 어떤 과정을 통해 이루어지는지에 대해 이번 포스팅을 통해 알아보겠습니다.

 

 

언리얼 엔진5 렌더링파이프라인 구조

 

 

 

Frame

렌더링을 시작하기 전에 이전 프레임과 현재 프레임 사이에 어떤 변화가 일어났는지 확인합니다.

CPU에서 동작하며, 마지막 프레임에서 일어난 모든 변화를 확인합니다.

다음 렌더링을 시작하기전에 모든 것을 진행하는 과정입니다.

※ 프레임 : 연속적인 영상을 구성하는 개별적인 정지 이미지

 

 

Visibility Processes / Occlusion Culling

가시성을 이용한 렌더링을 통해 게임 퍼포먼스 최적화를 담당하는 영역입니다.

카메라 Frustum에는 존재하지만, 씬 안에있는 다른 액터에 의해 가려져있거나 숨겨져있는 액터를 제외하여 성능을 향상시킵니다.

 ※ Culling : 보이지않는 지오메트리들을 렌더링에서 제외, 최적화하여 퍼포먼스를 향상시키는 기술

  • Distance Culling
    • 카메라와의 거리에 따라 렌더링 여부를 결정하는 기법
    • 설정한 거리외 오브젝트를 렌더링 하지 않음으로, GPU의 퍼포먼스 값을 향상시킬 수 있다.
    • 외부환경에서 내부환경으로 이동할 때 사용한다. (건물이나 구조물에 접근하기 전 내부가 보이지 않게)
  • Frustum Culling
    • 보이는 카메라 스페이스 밖의 Asset들을 렌더링 하지않는다.
    • 게임 내 POV값을 조절하는데 사용하며, Freeze Rendering명령어를 통해 확인할 수 있다.
  •  Precomputed Visibility
    • 가시성 데이터를 사전에 계산하여 런타임 환경에서 Occlusion Culling의 실시간 부하를 줄이는 기법
    • 화면에 표시할 필요가 없는 오브젝트들을 미리 계산하여 해당 레벨에서 GPU와 CPU의 렌더링 부하를 감소시킨다.
    • 고정된 시점(탑뷰, 2.5D게임)의 경우에서 효율적으로 사용할 수 있다.
  • Nanite Culling
    • 언리얼5에서 추가된 기능
    • 대규모 Static Mesh들을 LOD 시스템을 자동으로 처리하여 폴리곤들을 효율적으로 렌더링하는 기법
    • Level의 모든 Mesh들을 로드가능한 작은 클러스터로 분해시키고 렌더링 할 필요가없는 데이터를 로드시키지 않는다.
    • Culling을 엔진에서 자동으로 처리한다. 별도의 LOD를 만들지 않아도 된다.
    • 애니메이션이나 Skeletal Mesh와 같은 동적 Mesh들은 지원하지 않는다.

Depth Pass

Frame, Visibility / Occlusion Culling을 통해 렌더링을 해야하는 대상들을 파악 후 렌더링을 하는 단계입니다.

  • 카메라의 시점에서 각 픽셀까지의 거리(Depth)를 계산하여 Depth Buffer(Z-Buffer)의 텍스처에 저장하는 렌더링 단계.
  • 화면의 각 픽셀들이 카메라에서 얼마나 떨어져 있는지에 대해 0.0 ~ 1.0의 정규화된 값으로 저장한다.

Buffer : 데이터를 효율적으로 전달하기위해 사용하는 메모리 영역

 

  • Early Z-Pass
    • 기본적으로 활성화 되어있는 Pass
    • Depth 데이터를 먼저 계산하고 저장하여, 이후 색상 및 셰이더 계산을 최적화 하는 단계
    • 가려진 오브젝트, 픽셀들은 실행하지 않는다.

 

 

Base Pass

  • 화면에 보이는 객체의 시각 정보를 카메라에 출력하기 위해 색상, 재질, 조명 정보를 계산하는 단계
  • Base Color(색상), Roughness(표면의 거침 정도), Metalness(금속성), Specular(반사율)등을 G-Buffer에 저장한다. 
  • Direct Lighting(직접 조명), Indirect Lighting(간접 조명)으로 나누어 계산, G-Buffer에 저장한다.
    • G-Buffer에 저장된 조명 값들은 Lighting Pass에서 처리한다. 
  • UV좌표를 기반으로 텍스처 데이터를 불러온다.
  • Texture의 복잡성을 낮추거나, 해상도를 조절하여 GPU 부하를 감소시키는 단계

 

Static Lighting

  • 빌드시 Static Lighting 데이터를 생성한다.
  • CPU 기반으로 실행되며, 반사, 그림자 등 정적 조명 효과를 계산한다.
  • 정적 객체 및 조명에만 적용되며, 빌드 시 많은 계산을 요구하므로 Scene이 복잡할수록 빌드 시간이 길어진다.
  • Static 객체, 조명을 배치, UV값 설정, 빌드를 담당한다.

 

Dynamic Lighting

  • 조명과 객체들의 그림자, 반사, 색상 변화들을 실시간으로 처리한다.
  • 동적, 정적 객체에 실시간으로 조명을 적용, 처리한다.
  • 낮-밤의 전환, 폭발등의 동적인 환경을 담당한다.
  • 런타임 환경에서 계산되어 GPU,CPU를 많이 사용하여 성능에 문제가 발생할 수 있다.
  • Capsule Shadows : 스켈레탈 메시의 그림자를 가장 단순하게 처리하여 보여준다.
  • Raytracing : 정확하게 렌더링된 프레임에 가장 가까운 결과물을 실시간으로 제공한다.
  • Virtual Shadow Maps : 화면 크기에 따라 다른 해상도로 렌더링, 정확히 필요한 곳에서 높은 디테일 확보, 멀어질수록 최적화 하기위한 낮은 해상도로 예비 전환하는 역할

 

 

Reflections

  • 빛이 물체 표면에서 반사되어 다른 물체로 전달되는 시각적 효과를 표현한다.
  • 물, 유리, 금속 등 반사 특성이 강한 표면을 표현하거나 거울에 비친 캐릭터 등에 활용한다.
  • Screen Space Reflection
    • 화면에 보이는 정보들을 기반으로 런타임 환경에서 반사값들을 계산하여 처리한다.
  • Lumen 
    • Ray Tracing를 사용하여 실시간으로 전역 조명과 함께 반사값을 계산, 처리한다.
    • 씬의 복잡성과 관계없이 최적화된 성능을 제공한다.

 

Additional Features, Post Processing

  • 렌더링 파이프라인에서 기본적인 렌더링 처리 외 더 세부적인 효과를 처리하는 단계
  • Particle Effects, Bloom, Depth of Field 등의 기능들이 있다.
  • 렌더링 파이프라인의 후처리를 통해 시각적 퀄리티를 향상시킨다.
  • Post Processing이 D3DX 렌더링파이프라인의 Output Meger의 역할을 담당한다.

 

Performance

  • 다양한 방식으로 씬을 최적화하거나, 다른 씬보다 비용이 많이 드는 씬을 실시간으로 확인하는 단계
  • 성능에 문제가 되는 렌더링들을 모니터링 및 트러블슈팅을 확인할 수 있다.

Scalability Example

  • 프로젝트 한가지 버전 빌드시 나머지 지원 플랫폼으로도 사용할 수 있게 처리하는 단계
  • 저사양 모바일부터 차세대 플랫폼들에 대해 엔진에서 다양한 파라미터들을 확인, 수정할 수 있다.

Unreal Engine5 - Render : https://dev.epicgames.com/community/learning/tutorials/7BY6/unreal-engine-7eb4ec?source=Jke

 

Visibility and Occlusion Cuilling : https://dev.epicgames.com/documentation/ko-kr/unreal-engine/visibility-and-occlusion-culling-in-unreal-engine

 

 

 

언리얼 엔진과 C++코드를 사용한 학습을 진행하던중, 수학적인 계산이 필요한 경우가 있었습니다.

학습을 진행하던 중 KismetMathLibrary의 함수를 사용하게 되었고, 사용한 함수 외에도 유용한 다른 함수들이 어떤것이 있는지에 대해 알아보았습니다.

이번 포스팅에서는 KismetMathLibrary의 함수들에 대해 알아보겠습니다.

 

Kismet

언리얼 엔진에서의 Kismet은 UE3에서 도입된 비주얼 스크립팅 시스템입니다.

코드를 작성하지 않고 노드 기반 인터페이스로 이벤트, 조건, 액션 등을 연결하여 동작을 제어할 수 있었습니다.

UE4부터 비쥬얼 스크립팅은 BluePrint을 채용하게 되었고, 기존의 Kismet은 엔진 내부에서 다양한 기능을 가지는 범용 클래스로 변경되었습니다.

 

그 중 UKismetMathLibrary는 수학 계산과 관련된 함수들을 제공하는 클래스입니다.

해당 클래스의 주요 함수들에 대해 몇가지 알아보겠습니다.

 

 

UKismetMathLibrary

FindLookAtRotation : Start에 있는 객체가 Target 객체를 바라보는 회전 값을 반환합니다.

FRotator UKismetMathLibrary::FindLookAtRotation(const FVector& Start, const FVector& Target)
{
	return MakeRotFromX(Target - Start);
}

 

ComposeRotators : Rotator A와 B의 합한 값을 반환합니다.

FRotator UKismetMathLibrary::ComposeRotators(FRotator A, FRotator B)
{
	FQuat AQuat = FQuat(A);
	FQuat BQuat = FQuat(B);

	return FRotator(BQuat*AQuat);
}

 

RandomFloatInRange, RandomIntegerInRange, RandomInitVector : 주어진 범위 내에서 무작위 값을 생성합니다.

Cos, Sin, Tan : 삼각함수 값을 반환합니다.

※ 해당 함수들은 FMath의 함수들을 반환합니다. 즉, FMath 클래스 내 함수와 동일합니다.

double UKismetMathLibrary::RandomFloatInRange(double Min, double Max)
{
	return FMath::FRandRange(Min, Max);
}	

 

 

Lerp : 선형 보간을 수행하며, A와 B를 V의 비율에 따라 계산합니다.

  • V가 0일경우 A의 100%, 1일경우 B의 100%의 값을 계산합니다.
double UKismetMathLibrary::Lerp(double A, double B, double V)
{
	return A + V*(B-A);
}	

 

 

MakeTransform : 물리적인 객체를 이동하거나 회전시킬 때 사용합니다.

FTransform UKismetMathLibrary::MakeTransform(FVector Translation, FRotator Rotation, FVector Scale)
{
	return FTransform(Rotation, Translation, Scale);
}

 

 

 

FInterpTo : UI 애니메이션 또는 숫자 등의 속도를 제어합니다.

double UKismetMathLibrary::FInterpTo(double Current, double Target, double DeltaTime, double InterpSpeed)
{
	return FMath::FInterpTo(Current, Target, DeltaTime, InterpSpeed);
}

 

 

NormalizeAxis : 해당 각도를 -180 ~ 180의 값으로 변환하여 반환합니다.

float UKismetMathLibrary::NormalizeAxis(float Angle)
{
	return FRotator::NormalizeAxis(Angle);
}

 

 

GetDirectionUnitVector : From에서 To까지의 벡터 값을 받아옵니다. 위치가 같을시 (0,0,0)을 반환합니다.

FVector UKismetMathLibrary::GetDirectionUnitVector(FVector From, FVector To)
{
	return (To - From).GetSafeNormal();
}

 

 

ProjectPointOnToPlane : 정규화된 벡터로 정의된 평면에 해당 벡터를 투영합니다.

※ 화면 상의 위치를 특정 평면으로 매핑하는 경우 사용합니다.

FVector UKismetMathLibrary::ProjectPointOnToPlane(FVector Point, FVector PlaneBase, FVector PlaneNormal)
{
	return FVector::PointPlaneProject(Point, PlaneBase, PlaneNormal);
}

 

이 외에도 다양한 함수들이 존재하며, 그 중 언리얼 엔진을 사용한 클라이언트 개발자가 프로젝트 제작 시 자주 사용하는 것들에 대해 알아보았습니다.

언리얼 공식 사이트 - UKismetMathLibrary 클래스 

 

 

C++을 사용한 WinAPI 및 D3DX 환경에서는 메모리 공간을 효율적으로 사용하기 위해 생성자 및 소멸자를 사용하여 메모리 누수를 방지하였습니다. 

언리얼 엔진을 통한 게임 개발시에는 생성한 객체 및 자원들을 개발자가 아닌 엔진에게 메모리 관리를 담당하여 개발자의 편의성 및 프로그램 메모리를 관리 및 최적화 하는 가비지 컬렉션(Garbage Collection)이 존재합니다.

이번 포스팅에서는 언리얼 엔진의 GC에 대해 알아보겠습니다.

 

 

Garbage Collection

GC는 동적으로 생성한 객체들이 저장되는 영역인 Heap영역에 메모리가 할당되어 있는 객체들을 대상으로 사용됩니다.

언리얼 엔진을 사용하는 환경에서의 장단점에 대해 알아보겠습니다.

 

 

 

언리얼 엔진에서의 GC 특징

  • 마크 앤 스윕(Mark and Sweep) 알고리즘을 기반으로 동작한다.
    • Mark : GC 루트에서 시작해 연결된 객체를 "사용 중"으로 마킹하는 단계
    • Sweep : 마킹되지 않은 객체를 해제한다.
  • UObject를 관리한다.
  • AddToRoot() 또는 RemoveFromRoot()를 사용하여 특정 객체를 GC루트로 추가/제거 할 수 있다.
    • 최초 탐색 목록으로 설정하여 메모리가 회수되지 않는다.
  • 관리되는 모든 언리얼 오브젝트의 정보를 저장하는 GUObjectArray 전역변수를 통해 관리하며, 각 요소에 설정된 Flag를 통해 RootSet, Garbage 플래그를 구분하여 시스템이 자동으로 회수한다.

 

마크 앤 스윕 (Mark and Sweep)

  1. 힙 영역에서의 최초 검색을 시작하는 루트 오브젝트를 표시한다.
  2. 루트 오브젝트가 참조하는 객체를 찾아 마크(Mark)한다.
  3. 마크된 객체로부터 다시 참조하는 객체를 찾아 마크하고, 이를 반복한다.
  4. GC가 더이상 참조하지않는 (마크되지 않은)객체들의 메모리를 회수한다.(Sweep).

※ 탐색하는 과정은 DFS(깊이 우선 탐색) 방식으로 이루어지며, 필요에 따라 BFS(너비 우선 탐색)으로 이루어진다.

※ 또한 해시 테이블을 사용하여 탐색 중 방문한 객체를 기록, UObject의 고유 식별자를 키로 저장하여 중복 탐색을 방지한다.

 

 

장점

  • 개발자가 아닌 엔진에게 메모리 관리를 담당하여, 논리적으로 메모리를 관리, 최적화를 할 수 있다.
  • 해제된 메모리에 대한 접근을 방지하여 개발자의 실수를 방지할 수 있다.
    • 메모리 누수(Leak) : delete를 사용하지 않아 힙에 메모리가 그대로 남아있을 때
    • Dangling : 이미 해제한 객체를 다시 한번 해제할려 할 때
    • Wild : 값이 초기화 되지않아 잘못된 주소(nullptr)를 가르킬 때 

단점

  • 개발자가 객체를 직접 소멸시에도 GC는 해당 객체를 Unreachable 처리 전까지는, 추적을 계속한다.
    • 오버헤드가 발생할 수 있다.
  • GC가 메모리는 해제하는 시간을 명확하게 파악하기 어렵다.
  • 언리얼 엔진의 GC는 UPROPERTY를 사용하지 못하는 일반 C++ 클래스는 관리하지 않는다.
    • 개발자가 FGCObject 클래스를 상속, AddReferenceObjects 함수를 구현하여 관리할 수 있다.

 

 

가비지 콜렉터의 구성 옵션

편집 - 프로젝트 세팅 - 엔진 - 가비지컬렉션 설정 옵션

 

언리얼5에서 추가된 GC의 기능

1. GC 병렬화 (Parallel Garbage Collection)

  • GC작업이 멀티스레드 환경에서 병렬로 수행하도록 최적화가 되어 처리 속도가 향상되었다.
  • 객체 참조 그래프 생성 및 탐색 과정에서 병렬처리가 이루어진다.

2. Incremental GC 성능 향상 (점진적 가비지 컬렉션)

  • GC작업을 여러 프레임에 걸쳐 나눠 수행하여 큰 규모의 객체 그래프에서도 GC로 인한 지연을 최소화한다.
  • gc.TimeLimit, gc.NumRetriesBeforeForcingGC와 같은 설정을 통해 GC가 실행되는 방식을 조절할 수 있다.

3. 클러스터 기반 GC

  • 객체를 클러스터 단위로 관리하여 관련 객체를 함께 처리할 수 있다.
  • 개별 객체마다 사용되던 GC 비용을 최소화 하여 성능을 향상시킬 수 있다.

그 외에도 다양한 기능들이 언리얼5에서 추가, 개선되었으며 공식 홈페이지의 학습 문서를 통해 확인할 수 있습니다.

 

 


출처 및 참고내역

 

https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-object-handling?application_version=4.27

https://dev.epicgames.com/community/learning/knowledge-base/ePKR/unreal-engine-garbage-collector-internals

https://dev.epicgames.com/community/learning/knowledge-base/xaY1/unreal-engine-primer-debugging-garbage-collection-performance

+ Recent posts