-
ECS 구조와 Archetype/Chunk 개념 쉽게 정리 + 코드검색하기 귀찮아서 블로그에 박제 2025. 8. 3. 17:56728x90반응형
🎮 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️⃣ 핵심 요약
- Entity = ID만 가진 껍데기
- Component = 엔티티의 데이터 조각
- Archetype = 컴포넌트 조합 정의 + Chunk 리스트
- Chunk = 실제 데이터를 연속 메모리에 저장, slotIndex로 엔티티 위치 추적
- System = Archetype→Chunk→slotIndex 순으로 데이터 처리
이 구조로 가면:
- 상속 트리가 필요 없어짐
- 데이터 접근이 연속적 → CPU 캐시 효율 최고
- 엔티티 추가/삭제/이동이 빠르고 단순해짐
네 물고기 게임을 ECS로 옮기면,
- 물고기 = (Transform, Velocity, Renderable, Health)
- 플레이어 = (Transform, Velocity, Renderable, PlayerInput)
- System들이 필요한 컴포넌트만 보고 처리하면 돼요.
728x90반응형'검색하기 귀찮아서 블로그에 박제' 카테고리의 다른 글
내 최애 캐릭터로 ai 영상만들기 (0) 2025.08.02 git 브랜치 정리 (2) 2025.07.30 gsutil을 사용하여 WaymoDataset을 다운받는 법 (1) 2024.10.17 fatal error: google/dense_hash_map: No such file or directory (0) 2024.08.29 cv2.error: When the input arrays in add/subtract/multiply/divide functions have different types, the output array type must be explicitly specified in function "arithm_op" (0) 2024.03.08 - Entity (엔티티)