이론/설계

ECS (Entity Component System)

tsyang 2021. 9. 26. 23:51

ECS

 

정의

 "A different paradigm of writing code, where we model our programs in a data oriented way" 

 

 

유니티 ECS 메뉴얼에서는 ECS를 위와 같이 정의했다.

 

데이터 지향 설계방식은 아래 글을 참고.

 

https://tsyang.tistory.com/68

 

Data-oriented Design (데이터 지향 설계, DoD)

CppCon 2014 - Mike Acton의 "Data-Oriented Design and C++"을 주로 참고해서 씀. https://www.youtube.com/watch?v=rX0ItVEVjHc 인트로 1. 소프트웨어는 플랫폼이다. 2. 코드는 실제 세계의 모델을 중심으로 설..

tsyang.tistory.com

 

ECS의 3요소

ECS는 이름 그대로 다음의 3가지 요소를 사용한다.

 

  • Entity
  • Component
  • System

 

 

 

World는 우리의 게임 월드이고, 단순히 시스템과 엔티티의 모음이다.

Component 게임의 상태(데이터)를 저장한다. 아무것도 하지 않는다.

Entity는 컨테이너가 아니다. 그냥 index(id)이다.

System은 행동을 나타낸다. 그러나 게임의 상태(데이터)를 저장하지 않는다.

 

즉, 엔티티는 index(id)이다. 시스템은 필드가 없다. 컴포넌트는 함수가 없다.

 

출처 : gdc17 - Overwatch Gameplay Architecture and Netcode

위 그림은 오버워치에서 사용한 ECS시스템에서 시스템과 시스템이 사용하는 컴포넌트를 매칭시켜 놓은 것이다.

 

즉, 컴포넌트는 어딘가(World)에 몽땅 저장이 되어있고 System은 자신이 사용하는 컴포넌트를 읽는다. 이 때 각각의 개체는 Entity라는 id로 구분한다. 

 

System 역시 World에 저장이 되어있으며, World는 System을 돌며 업데이트를 해준다.

void Update(float deltaTime)
{
    foreach (var sys in m_systems)
    {
        sys.Update(deltaTime);
    }
}

 

이러한 방식이 OOP와 다른 점은 바로 행위와 상태를 분리해두었다는 것이다. OOP방식에서는 Entity가 데이터도 가지고 있고 행동도 했다. 반면 ECS에서는 엔티티는 단순히 데이터를 가리키는 인덱스일 뿐이고 시스템이 행위를 결정한다.

 

 

System은 어떻게 Entity를 사용할까? 

위와 같은 Entity들이 있다고 생각해보자. 나무는 위치와 나이를, 돌은 위치를, 고양이는 위치, 나이, 이동속도를 지닌다. 

 

시간의 흐름에 따라 나이를 먹게 해주는 AgeSystem이 있다고 하자. AgeSystem은 World의 모든 엔티티 중 Age 컴포넌트를 가진 엔티티들을 불러와 age에 시간을 더한다.

public class AgeSystem : ECS_System
{
    public override void Update(float deltaTime)
    {
        var entities = World.GetEntities(typeof(AgeComponent));
        foreach (var entity in entities)
        {
            entity.age += deltaTime;
        }
    }
}

코드는 아마 위와 같을 것이다.

 

Tree와 Cat은 Age컴포넌트를 가지고 있기 때문에 저 두 entity의 age는 업데이트 된다. 반면 Rock은 Age컴포넌트가 없으므로 AgeSystem에서 고려하는 대상이 아니다.

 

그렇다면 객체를 이동시켜주는 MoveSystem은 어떨까? 이경우 Position과 Speed 컴포넌트를 사용한다. 그리고 위의 셋 중 오직 Cat만이 MoveSystem의 관심 대상이다.

 

그렇다면 특정 컴포넌트를 가진 엔티티는 어떻게 뽑아낼 수 있을까? CppCon의 한 개발자는 bitwise연산을 통해 signiture라는 개념으로 엔티티를 추출했다.

 

유니티에서는 쿼리라는 용어를 사용한다. 마치 서버에서 원하는 데이터를 얻기위해 퀴리를 사용하는 것 처럼, ECS에서도 원하는 데이터를 얻기 위해 쿼리를 쓴다.

 

 

컴포넌트는 어떻게 저장하나?

ECS는 데이터 지향적인 설계를 위한 코딩 패러다임이라고 했다. 따라서 각각의 컴포넌트들은 같은 배열에 저장된다. 그리고 Entity는 인덱스이다. 

 

그렇다면 한 컴포넌트당 하나의 Array를 사용하는건 어떨까?

 

Entity는 왼쪽과 같다. 위와 같은 방법은 편하겠지만, 많은 메모리가 낭비되고 캐쉬 적중률이 낮다.

 

이러한 문제 때문에, 유니티의 경우 컴포넌트를 저장하기 위해 Archetype 이라는 개념을 사용한다.

 

출처 : https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/ecs_components.html

 

즉,  컴포넌트의 Set이 같다면 같은 아키타입으로 저장하는 것이다. 위 케이스에서는 개, 사람, 비둘기는 Cat과 같은 아키타입을 꽃과 버섯은 Tree와 같은 아키타입을 공유할 것이다. 

 

유니티에서는 추가적으로 Chunk라는 개념도 존재한다. 하나의 청크는 16kB의 크기를 갖으며 청크가 가득 차면 새로운 청크를 생성한다. (연결리스트로 저장) 이렇게 되면 캐쉬 미스가 좀 더 발생하겠지만, 데이터를 다루기는 굉장히 편해진다.

 

 

결론

ECS는 우선 익숙하지 않다. 따라서 구현하기 힘들다. 그러나 많은 장점이 있다. 우선 행위와 상태를 구분하므로써 커플링이 줄어든다. 테스트하기도 쉽고 멀티스레딩 하기도 용이하다. 그리고 데이터 지향적인 설계이기 때문에 OOP에 비하여 훨씬 빠른 퍼포먼스를 지닌다.

 

오버워치의 ECS 사례를 보면 꽤 고민한 흔적이 보인다. 이러한 방식으로 게임을 개발하는것도 복잡해 보인다. 그러나 Mike Acton은 ECS가 덜 Simple하기 때문이 아니라 우리가 익숙하지 않기 때문에 어려운 것이라고 말했다. 무엇보다 ECS는 OOP에 비하여 케이스마다 다르겠지만 보통 10배정도의 퍼포먼스를 보이며, 유니티의 Burst Compiler나 멀티 쓰레딩과 같이 활용하면 진짜 100배 이상의 퍼포먼스 차이를 보이니... 익숙해질 필요가 있어 보인다.

 

 

참고

1. https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/ecs_components.html

2. Unity at GDC - A Data Oriented Approach to Using Component Systems

3. CppCon 2015: Vittorio Romeo “Implementation of a component-based entity system in modern C++”

4. GDC17 - Overwatch Gameplay Architecture and NetCode

5. DOTS(Data-Oriented Tech Stack)란 무엇인가! (https://www.youtube.com/watch?v=hAp5nx2_Hpg)