ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ECS 구조와 Archetype/Chunk 개념 쉽게 정리 + 코드
    검색하기 귀찮아서 블로그에 박제 2025. 8. 3. 17:56
    728x90
    반응형

    🎮 ECS 구조와 Archetype/Chunk 개념 정리

    게임을 만들다 보면 객체지향(OOP) 방식의 상속 구조가 점점 복잡해져요.
    OOD구조로 Player → Fish → WildFish 같은 상속 트리가 생기면,
    나중에 새로운 행동을 추가할 때 코드가 꼬이기 쉽죠.

    이런 문제를 해결하는 방법 중 하나가 ECS(Entity-Component-System) 구조입니다.

    아 지피티로 만드니까 글씨깨지네염 하핳

     


    1️⃣ ECS 기본 개념

    • Entity (엔티티)
      • 게임 속 객체를 식별하는 ID만 있는 껍데기
      • 예: 1번 엔티티 = 금붕어, 2번 엔티티 = 플레이어
      • 실제 데이터는 엔티티 안에 없고, EntityManager가 관리
    • Component (컴포넌트)
      • 엔티티의 속성을 담는 데이터 조각
      • 엔티티 = 여러 컴포넌트의 조합
      • 예:
    public struct Transform { public float X, Y; }
    public struct Velocity { public float X, Y; }
    public struct Health { public float Value; }
    • System (시스템)
      • 특정 컴포넌트 조합을 가진 엔티티에 동작을 적용하는 로직
      • 예: MovementSystem은 (Transform, Velocity)를 가진 엔티티만 이동

    2️⃣ ECS의 메모리 최적화 구조: Archetype & Chunk

    ECS의 성능 핵심은 데이터를 연속된 메모리에 저장하는 거예요.
    이때 사용하는 구조가 Archetype과 Chunk입니다.

    Archetype (컴포넌트 조합 정의)
     ├─ Chunk0 (Transform[128], Velocity[128])
     ├─ Chunk1 (Transform[128], Velocity[128])
     └─ ...
     
    • Archetype
      • "이 엔티티는 어떤 컴포넌트 조합을 갖는가?" 정의
      • 예: (Transform, Velocity) 또는 (Transform, Velocity, Renderable)
      • 자신의 Chunk 리스트를 관리
    • Chunk
      • 실제 데이터를 담는 고정 용량 배열 블록
      • 예: Capacity = 128 → 엔티티 128개까지 저장 가능
      • slotIndex를 통해 각 엔티티의 데이터 위치를 식별

    Chunk 내부 구조

    Chunk (Capacity=4)
     ├─ Transform[4]  → [T0][T1][T2][T3]
     └─ Velocity[4]   → [V0][V1][V2][V3]
    • slotIndex = 엔티티가 차지하는 배열 인덱스
    • System은 0 ~ Count-1까지 순회하며 컴포넌트를 처리

    3️⃣ 최소 ECS 구현 (C#)

    ① 컴포넌트 정의

    public struct Transform { public float X, Y; }
    public struct Velocity { public float X, Y; }

    ② Chunk

    public class Chunk
    {
        public int Capacity;
        public int Count;
        public Dictionary<Type, Array> ComponentArrays = new();
    
        public Chunk(int capacity, Type[] componentTypes)
        {
            Capacity = capacity;
            Count = 0;
            foreach (var type in componentTypes)
                ComponentArrays[type] = Array.CreateInstance(type, capacity);
        }
    
        public int AddEntity(object[] components)
        {
            int slot = Count++;
            for (int i = 0; i < components.Length; i++)
                ComponentArrays[components[i].GetType()].SetValue(components[i], slot);
            return slot;
        }
    
        public void RemoveEntity(int slotIndex)
        {
            int last = Count - 1;
            if (slotIndex < last)
            {
                foreach (var kv in ComponentArrays)
                {
                    var arr = kv.Value;
                    arr.SetValue(arr.GetValue(last), slotIndex);
                }
            }
            Count--;
        }
    }

    ③ Archetype

    public class Archetype
    {
        public Type[] ComponentTypes;
        public List<Chunk> Chunks = new();
        public int ChunkCapacity = 128;
    
        public Archetype(params Type[] types)
        {
            ComponentTypes = types;
            Chunks.Add(new Chunk(ChunkCapacity, ComponentTypes));
        }
    
        public (int chunkIdx, int slotIdx) AddEntity(object[] components)
        {
            if (Chunks[^1].Count >= ChunkCapacity)
                Chunks.Add(new Chunk(ChunkCapacity, ComponentTypes));
            int chunkIdx = Chunks.Count - 1;
            int slotIdx = Chunks[chunkIdx].AddEntity(components);
            return (chunkIdx, slotIdx);
        }
    
        public void RemoveEntity(int chunkIdx, int slotIdx)
            => Chunks[chunkIdx].RemoveEntity(slotIdx);
    
        public bool Matches(params Type[] query)
            => query.All(t => ComponentTypes.Contains(t));
    }

    ④ EntityManager

    public class EntityManager
    {
        private int _nextId = 0;
        public Dictionary<int, (Archetype archetype, int chunkIdx, int slotIdx)> EntityMap = new();
        public List<Archetype> Archetypes = new();
    
        public int CreateEntity(Archetype archetype, object[] components)
        {
            int id = _nextId++;
            var (chunkIdx, slotIdx) = archetype.AddEntity(components);
            EntityMap[id] = (archetype, chunkIdx, slotIdx);
            return id;
        }
    
        public void DestroyEntity(int id)
        {
            if (!EntityMap.TryGetValue(id, out var info)) return;
            info.archetype.RemoveEntity(info.chunkIdx, info.slotIdx);
            EntityMap.Remove(id);
        }
    }

    ⑤ System 예시

    public class MovementSystem
    {
        private EntityManager _em;
        public MovementSystem(EntityManager em) { _em = em; }
    
        public void Update(float dt)
        {
            foreach (var archetype in _em.Archetypes)
            {
                if (!archetype.Matches(typeof(Transform), typeof(Velocity)))
                    continue;
    
                foreach (var chunk in archetype.Chunks)
                {
                    var transforms = (Transform[])chunk.ComponentArrays[typeof(Transform)];
                    var velocities = (Velocity[])chunk.ComponentArrays[typeof(Velocity)];
    
                    for (int i = 0; i < chunk.Count; i++)
                    {
                        transforms[i].X += velocities[i].X * dt;
                        transforms[i].Y += velocities[i].Y * dt;
                    }
                }
            }
        }
    }

    4️⃣ 핵심 요약

    1. Entity = ID만 가진 껍데기
    2. Component = 엔티티의 데이터 조각
    3. Archetype = 컴포넌트 조합 정의 + Chunk 리스트
    4. Chunk = 실제 데이터를 연속 메모리에 저장, slotIndex로 엔티티 위치 추적
    5. System = Archetype→Chunk→slotIndex 순으로 데이터 처리

    이 구조로 가면:

    • 상속 트리가 필요 없어짐
    • 데이터 접근이 연속적 → CPU 캐시 효율 최고
    • 엔티티 추가/삭제/이동이 빠르고 단순해짐

    네 물고기 게임을 ECS로 옮기면,

    • 물고기 = (Transform, Velocity, Renderable, Health)
    • 플레이어 = (Transform, Velocity, Renderable, PlayerInput)
    • System들이 필요한 컴포넌트만 보고 처리하면 돼요.
    728x90
    반응형

    댓글

Designed by Tistory.