Unity 검색

DOTS 기술 소개: 엔티티 컴포넌트 시스템

2019년 3월 8일 테크놀로지 | 9 분 소요
다루는 주제
공유

이번 게시물에서는 새로운 데이터 지향 테크기술 스택(DOTS)  기술 중 한 가지인 ECS(엔티티 컴포넌트 시스템)에 대해 소개해보겠습니다. 현재까지의 개발 상황 및 향후 계획을 공유하고자 합니다.

지난 번 게시물에서 HPC#(High Performance C#)과 버스트를 Unity가 지향해야 할 로우레벨 기반 기술로 소개한 적이 있습니다. 이러한 수준의 스택을 개인적으로 “게임 엔진을 위한 엔진”이라고 부르고 싶습니다. 누구나 이 스택으로 게임 엔진을 만들 수 있습니다. 유니티라면 가능하고 결국 해낼 것입니다. 여러분도 물론 가능합니다. Unity가 마음에 들지 않는다면 자신만의 엔진을 만들거나 Unity를 원하는 대로 수정하실 수 있습니다.

Unity의 컴포넌트 시스템

Unity는 새로운 컴포넌트 시스템을 만들고 있습니다. Unity의 개발 중심에는 언제나 컴포넌트 개념이 자리잡고 있습니다. 게임 오브젝트에 Rigidbody 컴포넌트를 추가하면 오브젝트가 낙하하기 시작합니다. Light 컴포넌트를 추가하면 빛이 방출되기 시작하며, AudioEmitter 컴포넌트를 추가하면 사운드 재생이 시작됩니다.

이 컴포넌트는 프로그래머와 비프로그래머 모두에게 매우 자연스러운 개념으로서 UI를 직관적으고 쉽게 빌드할 수 있게 해 줍니다. 이 개념이 얼마나 체계적으로 구축되어 왔는지를 보면 놀라울 따름입니다. 유니티는 앞으로도 계속 이 개념을 개발의 중심으로 유지하고 싶습니다.

앞으로 좀 더 개선해야 할 부분은 Unity의 컴포넌트 시스템 자체를 구축하는 방식입니다. 이는 객체 지향적인 사고 방식을 통해 작성되었습니다. 컴포넌트와 게임 오브젝트는 “많은 C++” 오브젝트로 이루어져 있습니다. 이것을 생성/제거하려면 id->objectpointers의 전역 리스트를 수정하기 위한 mutex(mutual exclusion, 상호 배제) 락(lock)이 필요합니다. 모든 게임 오브젝트에는 이름이 지정되어 있으며, 각 게임 오브젝트마다 C++ 오브젝트를 참조하는 C# 래퍼 오브젝트가 있습니다. 이 C# 오브젝트는 메모리 공간 어디에든 접근할 수 있습니다. C++ 오브젝트 역시 메모리 공간 어디에든 접근할 수 있습니다. 하지만 많은 캐시 미스(cache miss)가 발생합니다. 이러한 문제를 해결하기 위해 많은 노력을 기울이고 있지만 아직까지는 완벽하지는 않습니다.

하지만 데이터 지향적인 접근 방식을 이용하면 문제를 더 획기적으로 해결할 수 있습니다. Unity의 새로운 컴포넌트 시스템을 이용하면 사용자의 관점에서 유용한 프로퍼티는 그대로 유지(Rigidbody 컴포넌트를 추가하면 낙하 시작)하면서 놀라운 성능 향상과 병렬성(parallelism)을 얻을 수 있습니다.

이 새로운 컴포넌트 시스템을 ECS(Entity Component System, 엔티티 컴포넌트 시스템)라고 합니다. 간단하게 말하면 현재 게임 오브젝트로 수행하는 작업을 새로운 시스템에서는 엔티티를 이용하여 수행하는 것입니다. 컴포넌트는 기존과 동일하게 컴포넌트로 불립니다. 그렇다면 무슨 차이점이 있을까요? 그것은 바로 데이터 레이아웃입니다.

몇 가지 일반적인 데이터 액세스 패턴을 한번 살펴보겠습니다.
Unity에서 기존 방법으로 컴포넌트를 작성한다면 일반적으로 아마 다음과 같이 나타납니다.


class Orbit : MonoBehaviour
{
   public Transform _objectToOrbitAround;

   void Update()
   {
       //please ignore this math is all broken, that's not the point here :)
       var currentPos = GetComponent<Transform>().position;
       var targetPos = _objectToOrbitAround.position;
       GetComponent<RigidBody>().velocity += SomehowSteerTowards(currentPos,targetPos)
   }
}

이러한 패턴은 반복적으로 나타납니다. 하나의 컴포넌트는 같은 게임 오브젝트에서 다른 컴포넌트를 하나 이상 찾아 어떤 값을 읽거나 써야 합니다.

이 방법에는 다음과 같은 많은 문제점이 있습니다.

  • 단일 Orbit 컴포넌트에 대해 Update() 메서드를 호출한다고 가정해 봅시다. 다음으로 완전히 다른 컴포넌트에 대해 Update() 메서드가 호출될 수 있으며, 이로 인해 이 코드는 다음 번에 또 다른 Orbit 컴포넌트에 대해 이 프레임을 실행해야 하는데도 캐시에서 사라질 수 있습니다.
  • Update()는 Rigidbody를 찾기 위해 GetComponent()를 사용해야 합니다. 물론 캐싱될 수도 있지만 Rigidbody 컴포넌트가 제거되지 않았는지 주의해야 합니다
  • 사용 중인 나머지 컴포넌트가 메모리 내에서 완전히 다른 영역에 위치합니다.

ECS에서 사용하는 데이터 레이아웃은 이러한 패턴이 매우 일반적으로 발생하고 있음을 인식하고 메모리 레이아웃을 최적화하여 이 작업을 더 빠르게 수행할 수 있습니다.

ECS 데이터 레이아웃

ECS는 메모리에서 컴포넌트 세트가 완벽하게 동일한 모든 엔티티를 그룹화합니다.  이때 이 컴포넌트 세트를 원형(archetype)이라고 합니다. 예를 들어 “Position, Velocity, Rigidbody, Collider”는 하나의 원형으로 묶여 있습니다. ECS는 16k의 청크 단위로 메모리를 할당합니다. 각 청크에는 하나의 원형에 속한 엔티티의 컴포넌트 데이터만 포함됩니다.

런타임 시 Orbit 인스턴트별로 사용자 Update 메서드를 사용하여 다른 작업 대상 컴포넌트를 찾는 대신, ECS에서는 “Velocity, Rigidbody 및 Orbit 컴포넌트가 모두 있는 모든 엔티티를 대상으로 작업을 실행하고 싶습니다.”라고 정적으로 선언해야 합니다. 이러한 모든 엔티티를 찾으려면 특정 “컴포넌트 검색 쿼리”와 일치하는 모든 원형을 찾기만 하면 됩니다. 각 원형에는 해당 원형의 엔티티가 저장되어 있는 청크 리스트가 있습니다. Unity는 이러한 모든 청크에 대해 루프를 수행하며, 각 청크 내에서 빽빽하게 패킹된 메모리에 대해 선형 루프(linear loop)를 수행하여 컴포넌트 데이터를 읽고 씁니다. 각 엔티티에서 동일한 코드를 실행하는 이 선형 루프를 통해 버스트에 대한 벡터화에도 도움을 줍니다.

대부분의 경우 이 프로세스는 여러 잡(job)으로 분할됩니다. 이를 통해 ECS 컴포넌트를 작동하는 코드의 경우 코어를 거의 100% 활용하여 실행됩니다.

ECS는 이러한 모든 작업을 자동으로 수행하며 사용자는 각 엔티티에서 실행할 코드만 제공하면 됩니다. (물론 경우에 따라 청크 반복을 수작업으로 할 수도 있습니다.)

엔티티에서 컴포넌트를 추가하거나 제거하면 원형에 변화가 생깁니다. 이 경우 현재 청크에 있는 엔티티가 새로운 원형 청크로 이동하게 되어 해당 청크에 발생된 공백을 메꿀 수 있도록 이전 청크의 마지막 엔티티를 백스왑합니다.

또한 ECS에서는 컴포넌트 데이터로 할 작업을 정적으로 선언할 수 있으며, 읽기 전용 또는 읽기 쓰기로 선언할 수 있습니다. Position 컴포넌트를 읽기 전용으로 선언하면 ECS가 더 효율적으로 작업을 스케줄링할 수 있습니다. Position 컴포넌트에서 읽어와야 하는 다른 잡은 대기할 필요가 없게 됩니다.

이 데이터 레이아웃을 통해 오랫동안 골칫거리였던 로드 시간과 직렬화 성능 문제를 해결할 수 있습니다. ECS 데이터를 용량이 큰 씬에서 로딩/스트리밍할 때는 디스크에서 원시 데이터 몇 바이트만 로딩해서 있는 그대로 사용하면 됩니다.

핸드폰에서 메가시티(Megacity) 데모를 로드하는 데 몇 초밖에 걸리지 않는 이유가 여기에 있습니다.

이 콘텐츠는 Targeting Cookies 카테고리를 수락해야만 동영상을 시청할 수 있도록 허용하는 타사 제공업체에서 호스팅합니다. 이러한 제공업체의 비디오를 보려면 쿠키 환경 설정에서 Targeting Cookies 카테고리를 수락하시기 바랍니다.

엔티티의 특징

엔티티는 기존에 게임 오브젝트가 하던 작업을 수행할 뿐 아니라, 경량(lightweight)이기 때문에 더 많은 작업도 수행할 수 있습니다. 엔티티의 실체는 무엇일까요? 사실 이 게시물의 초안을 작성할 때 “엔티티를 청크 단위로 저장합니다(we store entities in chunks).”라고 썼다가 나중에 “엔티티의 컴포넌트 데이터를 청크 단위로 저장합니다(we store component data for entities in chunks).”라고 바꾸었습니다. 이것은 엔티티가 32비트 정수에 불과함을 이해하는 데 중요한 특징입니다. 엔티티에는 컴포넌트의 데이터 외에 저장하거나 할당할 만한 것이 없습니다. 용량이 매우 작기 때문에 게임 오브젝트를 사용하기에 부적합한 시나리오에서도 사용할 수 있습니다. 예를 들어 파티클 시스템의 개별 파티클 각각에 엔티티를 사용할 수 있습니다.

HPC#, 버스트, ECS를 기반으로 한 게임 엔진 구축

유니티에서 구축하려고 하는 그 다음 레이어는 매우 큰 프로젝트로서, “게임 엔진” 레이어이며 “렌더러”, “물리 시스템”, “네트워킹”, “입력”, “애니메이션” 등으로 구성되며 현재까지는 아직 구체적이지 않습니다. 이제 시작 단계이며 하루 아침에 구현 가능한 것은 아닙니다.

실망스럽다고요? 하지만 꼭 그렇게 볼 필요는 없습니다. ECS와 ECS에 탑재된 모든 요소는 C#으로 작성되었기 때문에 기존 Unity에서 실행 가능합니다. Unity 내부에서 동작하기 때문에 ECS 이전의 기능을 사용하는 ECS 컴포넌트도 작성할 수 있습니다. 완전한 ECS 메시 드로잉 시스템은 아직 출시되지 않았습니다. 하지만 완전한 ECS 버전이 출시될 때까지 ECS 이전의 Graphics.DrawMeshIndirect API를 사용하는 ECS MeshRenderSystem을 작성하여 구현할 수 있습니다. 이것은 유니티의 Megacity 데모에서 사용하는 기술이기도 합니다. 완전한 ECS 시스템상에서 로딩/스트리밍/컬링/애니메이션 작업을 할 수 있지만 최종 드로잉 작업은 할 수 없습니다.

따라서 기술을 적절히 조합할 필요가 있습니다. 이렇게 하면 모든 하위 시스템에서 호환되는 완전한 ECS 버전이 출시될 때까지 기다릴 필요 없이 게임 코드에 필요한 버스트 코드 생성과 ECS 성능을 활용할 수 있습니다. 단, 이러한 전환 단계에서 “두 가지 다른 시스템을 함께 사용할 때” 겪을 수 있는 충돌과 부자연스러움을 보고 느끼게 될 수 있습니다.

유니티는 ECS, HPC# 하위 시스템에 모든 소스 코드를 패키지 형태로 제공할 계획입니다. 각 하위 시스템을 검사, 디버그, 수정할 수 있으며 어떤 하위 시스템을 업그레이드할지에 대해 더 강력한 제어 권한을 갖게 될 것입니다. 예를 들어 다른 것들은 그대로 유지한 채 물리 하위 시스템 패키지만 업그레이드할 수도 있습니다.

게임 오브젝트의 미래

게임 오브젝트는 그대로 유지됩니다. 10년이 넘는 기간 동안 오브젝트를 사용한 많은 게임이 성공적으로 출시되었습니다. 이러한 기반은 계속 유지될 것입니다.

시간을 두고 지켜봐 주시기 바랍니다. 유니티의 노력으로 게임 오브젝트가 ECS에 맞게 변화하는 모습을 보실 수 있을 것입니다.

API 사용성/상용구 코드

ECS에 대한 사용자의 일반적인 인식은 많은 양의 코드를 입력해야 한다는 것입니다. 무언가를 구현하려 하다 보면 자연스럽게 많은 양의 상용구 코드(boilerplate code)를 사용하게 됩니다.

유니티는 상용구 코드 없이도 사용자의 의도를 쉽게 표현할 있도록 개선하고 있습니다.  현재 기본적인 성능 개발에 주력하고 있기 때문에 아직 구현된 개선 사항은 많지 않습니다. 하지만 ECS 게임 코드에 상용구 코드가 많이 사용되거나 MonoBehaviour보다 코드 작성을 더 많이 해야 할 이유는 없습니다.

Tiny 프로젝트 에서는 이미 람다 기반의 반복 API와 같은 일부 개선 사항이 적용되었습니다.

Tiny 프로젝트의 ECS 적용

Tiny 프로젝트 는 위에서 설명한 C# ECS를 사용한 우수 사례라고 볼 수 있습니다. Tiny 프로젝트는 다음과 같은 점에서 ECS를 구현한 우수한 사례가 될 것입니다.

  • ECS 전용 환경에서 실행 가능합니다. 과거에 사용하던 그 어떤 것도 필요 없습니다.
  • Tiny 프로젝트는 완전한 ECS이며 작은 규모의 게임에 필요한 모든 ECS 하위 시스템과 함께 제공된다는 의미입니다.
  • 유니티는 작은 규모의 프로젝트뿐만 아니라 모든 ECS 시나리오에서 엔티티를 수정할 수 있도록 Tiny 프로젝트 에디터를 지원할 예정입니다.

유니티 채용 정보

DOTS 스택에 필요한 여러 직무의 채용 공고가 있습니다. 특히 버뱅크(Burbank)와 코펜하겐(Copenhagen)에서 인재를 필요로 합니다. careers.unity.com을 확인하세요.

또한 Unity 엔티티 컴포넌트 시스템 및 C# 잡 시스템 포럼에 참여하여 의견을 제공하고 실험 기능과 프리뷰 기능에 대한 정보를 얻으시기 바랍니다.

2019년 3월 8일 테크놀로지 | 9 분 소요
다루는 주제