원문 캡처 · immibis.com
원문 캡처 · immibis.com

게임 설계에서 ECS(Entity Component System)는 아키텍처, 엔티티와 컴포넌트를 관리하는 라이브러리, 또는 그 둘을 모두 관리하는 시스템을 가리킬 수 있다. 이 글은 ECS와 혼동하기 쉬운 SoA와 데이터 지향 설계의 차이도 구분한다.

ECS가 가리키는 세 가지

ECS, 즉 Entity Component System의 정확한 의미를 두고 혼동이 있는 듯하다. 게임 설계와 다른 분야에서도 마찬가지다. 위키백과 문서의 첫 두 문장조차 ECS가 셀 수 있는 명사인지 아닌지 일치하지 않는다.

Entity component system(ECS)은 소프트웨어 아키텍처 패턴이다. ECS는 [...]로 구성된다.

객체 지향 프로그래밍(OOP)을 두고 “An OOP consists of ...”라고 말하지는 않는다.

여기서는 ECS의 여러 의미와 혼동하기 쉬운 개념을 구분해 본다. 이 개념들은 상당히 겹치고 얽혀 있지만, 조금이나마 나눠 설명할 수 있다.

엔티티/컴포넌트/시스템 아키텍처

소프트웨어는 엔티티, 컴포넌트, 시스템을 사용하는 아키텍처 스타일로 작성할 수 있다. 실제 코드에서 이 이름을 쓰는지와는 무관하다.

게임에 특화된 아키텍처인 ECS에서는 게임 세계가 엔티티로 이뤄진다. 하지만 게임 세계의 엔티티가 반드시 게임 코드 안에 실체화되는 것은 아니다. 즉, 하나의 Entity 구조체가 반드시 존재하는 것은 아니다. 엔티티는 3D 모델, 체력 표시줄, 물리 상자처럼 서로 다른 요소가 결합해 만들어질 수 있다. 게임 코드도 렌더링, 전투, 물리처럼 서로 다른 모듈, 즉 시스템으로 나눌 수 있다.

게임에 서드파티 물리 라이브러리를 사용해 본 적이 있다면, 물리 기능이 필요한 게임 객체마다 대응하는 ‘물리 객체’를 만들어야 했을 것이다. 물리 라이브러리는 물리 객체의 시뮬레이션을 맡고, 매 프레임 한 번씩 그 위치와 회전을 게임 객체에 복사해 렌더링한다. 둘 사이의 연결은 상당히 느슨하다. 기껏해야 struct Entity가 물리 객체를 가리키는 포인터를 담는다.

이처럼 분리되고 각 부분이 독립적으로 동작하는 방식은 ECS 스타일의 전형적인 사례다. 물리 객체는 컴포넌트이고 물리 라이브러리는 시스템이다. 네트워킹처럼 게임의 다른 부분에도 이런 방식을 적용할 수 있지만, 물리 분야가 가장 흔한 사례라고 본다.

ECS 아키텍처 스타일이 특정 메모리 저장 방식을 요구하는 것은 아니다. 이 스타일을 구현하는 자료 구조는 여러 가지다.

엔티티/컴포넌트 시스템

ECS 아키텍처를 사용할 때 엔티티와 컴포넌트를 관리하는 서드파티 또는 자체 소프트웨어 모듈을 쓸 수 있다. 이 모듈도 ‘시스템’이지만, 아키텍처 스타일을 정의할 때 쓰는 ‘시스템’과는 다른 의미다. ‘ECS’는 이 모듈을 가리키는 말로도 자주 쓰인다.

EnTT는 스스로를 ‘엔티티-컴포넌트 시스템’이라고 소개하는 라이브러리다. 예제를 보면 엔티티와 컴포넌트는 관리하지만 시스템은 관리하지 않는다. 따라서 이를 엔티티/컴포넌트 시스템이라고 부를 수 있다. 명확한 소통을 싫어한다면 ‘ECS’라고 부를 수도 있다. 모호함을 줄이려면 ‘EC 라이브러리’ 또는 ‘EC 프레임워크’라고 부르는 방법도 있다.

일부는 ECS 아키텍처의 시스템을 라이브러리가 관리해서는 안 된다고 주장한다. 그런 관점에서는 모든 EC 라이브러리가 ECS 라이브러리이기도 하며, 이때 S는 아무 역할도 하지 않는다.

‘엔티티/컴포넌트 시스템’의 ‘시스템’은 ‘엔티티/컴포넌트/시스템 아키텍처’에서 말하는 ‘시스템’과 분명히 다른 뜻이다.

이를 ‘엔티티 컴포넌트 시스템’이라고 부를 수도 있다. 엔티티의 컴포넌트, 즉 엔티티 컴포넌트를 관리하는 시스템이라는 뜻이다. 하지만 이는 같은 아키텍처를 가리키므로 실질적인 차이가 없다. 엔티티가 없으면 엔티티 컴포넌트도 존재할 수 없다. 엔티티 컴포넌트를 관리하는 시스템은 필연적으로 엔티티도 관리한다.

엔티티/컴포넌트/시스템 시스템

여기서 더 혼란스러워진다. 일부 EC 시스템에는 시스템을 관리하는 기능도 있다. 이런 시스템은 ‘엔티티/컴포넌트/시스템 시스템’ 또는 ‘ECS 시스템’이 된다. ‘ECS’가 이 가운데 하나를 뜻할 때도 있다.

예를 들어 EntityX는 개념상 EnTT와 비슷하지만 시스템도 관리한다. EntityX는 스스로를 ‘엔티티 컴포넌트 시스템’이라고 부르며, 그 말이 무슨 뜻인지 명확히 하려 하지 않는다.

SoA와 데이터 지향 설계

구조체 배열과 배열 구조체

구조체 배열(Structure of Arrays, SoA)은 대략 다음과 같은 구현 패턴이다.

struct Game {
 vector<vec2> position;
 vector<vec2> velocity;
 vector<float> health;
};

이는 더 흔한 배열 구조체(Array of Structures) 방식과 다르다.

struct Entity {
 vec2 position;
 vec2 velocity;
 float health;
};
struct Game {
 vector<Entity> entities;
};

SoA는 여러 엔티티에 같은 연산을 수행할 때 CPU 캐시를 더 효율적으로 사용한다. 예를 들어 다음과 같은 연산이 해당한다.

position += velocity*timestep;

SoA가 ECS 아키텍처의 장점 가운데 하나로 꼽히기 때문에, 둘은 서로 혼동되기도 한다. 객체 지향 아키텍처에서는 대개 구조체를 가리키는 포인터 배열을 쓰도록 제한되지만, ECS 아키텍처는 SoA를 비롯해 더 다양한 구현 방식을 허용한다.

하지만 둘은 같은 것이 아니다. 앞의 예는 SoA지만 컴포넌트가 없으므로 ECS가 아니다. 반대로 다음 ‘메가구조체 설계’는 엔티티를 컴포넌트로 나누지만 SoA를 사용하지 않는다.

struct Motion {
 vec2 position;
 vec2 velocity;
};
struct Combat {
 float health;
};

struct Entity {
 optional<Motion> motion;
 optional<Combat> combat;
};
struct Game {
 vector<Entity> entities;
};

데이터 지향 설계

데이터 지향 설계(Data-Oriented Design, DoD)는 실제로 많은 경우 SoA와 큰 반복문을 사용하는 방식으로 나타나기 때문에 SoA와 혼동되는 일이 잦다.

하지만 DoD는 더 높은 추상화 수준의 개념이다. 프로그램을 세계 모델이나 코드 구조가 아니라 데이터의 흐름을 중심으로 생각하는 방법이다. 현대 CPU에서, 그리고 이 용어가 생긴 2009년 당시에도, 비디오 게임 데이터를 효율적으로 처리하는 방법은 대개 작은 요소로 이뤄진 동질적인 배열을 큰 반복문으로 훑는 것이었다. 하지만 항상 그런 것은 아니다.

예를 들어 1980~1990년대 프로세서에서는 연결 리스트가 상당히 효율적이었다. 이런 프로세서에는 캐시나 데이터 프리페처가 없었고, 메모리에 접근할 때는 어느 바이트를 읽든 비용이 같았다. 연결 리스트는 추가 조회 없이 다음 항목으로 곧바로 이동할 수 있다. 그런 구형 프로세서나 캐시가 없는 현대의 소형 마이크로컨트롤러용 코드를 쓴다면, 데이터 지향적으로 생각한 결과 연결 리스트를 많이 사용하게 될 수도 있다.

대규모 서버 클러스터에서는 데이터 지향적 사고가 MapReduce 방식으로 이어질 수도 있다.

데이터 지향적으로 생각하는 사례는 다음과 같다.

  • 게임 엔진을 각 엔티티가 따로 움직이는 방식이 아니라, 완성된 이전 게임 상태를 완성된 새 상태로 바꾸는 과정으로 생각한다. 예를 들어 foreach(Entity e) e.pos += e.vel * timestep;처럼 처리한다. 이렇게 하면 평가 순서에서 비롯되는 문제를 피할 수 있다. 이는 ECS 아키텍처의 S이기도 하다.
  • 앞서 설명한 SoA는 각 엔티티의 다른 필드 때문에 CPU 캐시가 오염되는 일을 줄이면서 상태 변환을 수행하게 해준다.
  • 열 지향 데이터베이스도 같은 작업을 대규모로 수행한다.
  • 네트워크 스위치는 특정 헤더 조합, 예를 들어 Ethernet+VLAN+MPLS+IPv4에 맞춰 하드웨어로 구현될 수 있다. 보통 고정된 바깥쪽 계층만 패킷 전달에 쓰이므로 안쪽 헤더가 맞지 않아도 정확성 문제는 생기지 않는다. 다만 그런 헤더가 트래픽 셰이핑 같은 기능에 쓰일 수 있어, 예상하지 못한 헤더 조합의 패킷은 기본 분류 방식으로 처리된다.
  • 데모신에서 사용하는 SDF 레이 마칭 셰이더는 메시 지오메트리 같은 요소를 건너뛰고 화면에 픽셀을 직접 그리는 것을 목표로 한다.
  • Factorio에서는 엔티티가 전력망에 연결될 때마다 ‘전기 노드’를 사용한다. Factorio는 대부분 AoS 기반이지만, 전기 노드는 주된 AoS 바깥에 따로 저장되며 연속된 메모리 블록에 모여 있다. 전력망은 이 노드들을 순회한다. 이는 ECS/SoA를 제한적으로 사용하는 사례다.
  • ‘아키타입 ECS’는 같은 컴포넌트 집합을 가진 엔티티를 묶어 SoA보다 더 나아간다. 이렇게 하면 빈 공간이 생기지 않는다. 예를 들어 위치, 속도, 체력을 가진 엔티티용 SoA와 위치, 속도만 가진 엔티티용 SoA를 따로 만든다. 그러면 체력 배열에 빈 칸이 생기지 않고, 전투 시스템이 체력이 없는 엔티티를 건너뛸 필요도 없다.

DoD가 SoA를 뜻하는 것은 아니다. 위치와 속도를 처리하는 반복문에서는 위치와 속도를 서로 교차해 저장하는 편이 빠를 수도 있고, 두 개의 별도 배열에 저장하는 편이 빠를 수도 있다. 데이터 지향적 사고라면 실제로 두 방식을 모두 벤치마크해 어느 쪽이 더 빠른지 확인해야 한다.