본문으로 바로가기

UnrealEngine의 UObject 와 Components

category UnrealEngine/Impl 2023. 9. 30. 21:35

언리얼 엔진의 모든 객체들은 UObject를 파생합니다.

UObject가 제공하는 MetaData, Reflection 생성, Garbage Collection, Serialization, Editor Visibiliity,

Class Default Object 등을 통해 언리얼엔진은 객체가 실행되는 세계를 구축할 수 있습니다.

 

그럼 왜 UObject가 필요한 것 일까요?

 

다른 게임 엔진을 보면 이해하기 쉽습니다. 

UnityMono를 통해 게임 객체들의 메모리 관리를 하고, 우리가 실제로 어떠한 객체를 사용할 때 잘 생각하지 않아도 알아서 정리를 해주니 메모리에 관한 걱정을 덜어줍니다.

 

그렇다면, 이러한 디자인을 도입하면 장단점은 대체 무엇이 있을까요?

 

장점 : 

1. 모든 것이 추적 가능합니다.

통합된 기본 클래스인 UObject를 사용하면, 파생된 모든 객체를 추적할 수 있습니다.

 

2. 상속 메커니즘

Equals, Clone, GetHashCode, ToString, GetName, GetMetaData 등 모든 객체에 적용하려는 속성과 인터페이스를 추가할 수 있습니다. 코드를 한 번만 작성하면 모든 객체에 적용이 가능합니다.

 

3. Memory 관리

Reference Count를 UObject에 추가시켜, 관리하기 용이하게 합니다. 이를 통해 GC는 ReferenceCount 에서 AddCount(), ReleaseCount() 같은 함수를 만들어두고 체크만 하면 됩니다. GC 방식을 사용하면 참조할 통일된 객체를 갖게 되므로 현재의 GC를 지원하는 대부분의 언어는 이와같이 디자인하게 됩니다.

 

4. Serialization

서로 다른 타입에 대한 직렬화를 지원하려면 각 타입에 대해서 별도의 방법을 작성하거나, 템플릿같은 곳에서 태그를 사용해야 합니다. 통합되어 있다면 Reflection이나 CDO 에 의해 직렬화를 편하게 할 수 있습니다.

 

5. 통계

예를 들어, 어떤 객체가 가장 많이 할당되었는지, 어떤 객체가 가장 오래 할당되었는지 등을 확인할 수 있습니다.

또한 통일된 인터페이스때문에 추적이 용이하고 편리한 다른 기능도 쉽게 구현할 수 있습니다.

 

6. 디버깅 편의성

예를 들어, 한 곳에서 Memory Leak 이 났다고 생각해봅시다. 통일된 Object가 없을 시 어떤 객체때문에 Leak이 났는지 확인하기가 어렵습니다. 

 

단점 :

1. 객체가 너무 거대해짐

상속의 오래된 문제점입니다. 모든 객체에 추가 기능을 제공하려고 할 수록 더 많은 수의 함수 인터페이스와 멤버 속성이 추가됩니다. 시간이 지남에 따라 이 객체들은 다양한 코드로 가득 차게 되고 사람이 볼 때 이해도가 크게 덜어집니다.

언리얼 엔진도 UObject를 보기만 해도 매우 무겁다는 사실을 알 수 있습니다. 수 십개의 인터페이스가 있고, 해당 인터페이스들을 다 아는 사람은 거의 없을겁니다.

 

2. 불필요한 메모리 오버헤드

몇 몇 변수를 사용하지 않더라도, 사용하는 오브젝트가 있다면 해당 변수를 UObject에 추가해야만 합니다. 

해당 변수를 사용하지 않는 오브젝트가 있으면 그것은 메모리를 차지하므로 낭비입니다.

 

3. 다중 상속의 한계

예를 들어, C는 A 와 B 를 상속받는다고 해봅시다. A와 B가 둘 다 Object로 부터 상속을 받게 되면 C는 다이아몬드 상속을 받게 됩니다. 물론 virtual 키워드를 통해 가상 상속을 받는다고 해도 C는 필요의 여부에 관계없이 상속을 받는 다는 것이 문제가 될 수 있습니다.

 

Components

우리의 게임 세계에서는 각 각의 액터들이 각자의 역할을 수행하며 자연스럽게 다양한 능력을 갖추어 작업을 하고 있습니다.

게임 세계가 점점 화려해 질수록, 요구되는 스킬은 더 많아지고 자주 바뀌게 되는데 이러한 조합으로 인해 액터들의 갯수가 많아질 수 밖에 없습니다.

이를 해결하기 위해 액터들의 기본 적인 요소만 뭉치고, '스킬' 같은 것들을 컴포넌트로 추상화 하여 

각각의 액터가 조립하고 처리하도록 합니다.

 

이것이 Component Pattern의 원리 입니다. 각각의 액터들은 자신의 능력 방식을 Component들을 조립하여 나타냅니다.

 

언리얼 엔진에서 기본적으로 제공되는 Component들을 보면 앞에 U가 붙어 있습니다.

Component들은 UObject를 기반으로 한 자식클래스입니다. 즉, ComponentUObject들의 공통 기능도 가지고 있다는 뜻입니다.

 

 

OwnedComponents는 이 액터가 소유한 모든 컴포넌트를 가지고 있습니다.

일반적으로 SceneComponentRootComponent로 존재합니다.

TArray<UActorComponent*> InstanceComponents는 인스턴스화된 컴포넌트들을 가지고 있습니다.

인스턴스화된 컴포넌트라는것은 무엇일까요?

 

우리가 어떠한 무기를 들고있는 군인을 액터로 만들었다고 해봅시다. 각각의 액터는 서로 다른 무기를 가지고 있을텐데요.

이때 InstanceComponents를 다를 겁니다.

하지만 OwnedComponents는 포괄적입니다. 

 

액터를 레벨에 배치하려면 RootComponent를 인스턴스화 해야합니다.

 

이제 Component들의 자식에 대해서 이야기 해 봅시다. ( 가장 일반적인 Component들만 나열합니다. )

 

ActorComponent는 모든 Component들의 부모 클래스입니다.

이동, 인벤토리나 속성 관리 같은 '추상적인' 동작에 유용합니다. 위에서 보이듯이 TransformUSceneComponent부터 추가됩니다. 즉, 월드에서 위치를 지정해주거나 회전같은 행동이 없다는 것입니다.

 

ActorComponent 의 자식들중에서 가장 중요한 ComponentSceneComponent 입니다.

SceneComponent 에서는 두 가지 기능을 제공합니다.

하나는 Transform 이고, 또 하나는 SceneComponent들을 뭉치는 것(AttachChildren)입니다.

 

즉, SceneComponent를 통해 월드에서 위치를 지정해주거나, 회전같은 행동이 가능해 진다는 것입니다.

근데 궁금한 점은, 왜 ActorComponent에서 컴포넌트들을 뭉치는게 불가능할까요?

 

예를 들어, 자동차를 액터로 하나 만든다고 생각해 봅시다.

한 가지 방법은 차체를 하나의 RootComponent로 생각하고, 자동차의 네 바퀴를 모두 RootComponent의 하위 SceneComponent로 뭉치게 하면 됩니다.

액터는 여러 SceneComponent들을 가져와 여러 Mesh객체를 렌더링할 수 있게됩니다.

이제 RootComponent를 이동하게 되면, 여러 SceneComponent들도 함께 움직이게 될 것입니다.

 

PrimitiveComponent는 기하학적 '표현'이 추가된 Component 입니다.

일반적으로 시각적인 요소를 렌더링 하거나, 물리적 객체와 충돌하거나 겹치는 데 사용됩니다.

예를 들어 SkeletalMesh, StaticMesh, Sprite 또는 Billboard, Particle, Collision 등입니다.