ECS (Entity-Component-System) 아키텍처 패턴
ECS란
섹션 제목: “ECS란”ECS(Entity-Component-System)는 **데이터 지향 설계(Data-Oriented Design)**를 기반으로 한 아키텍처 패턴입니다.
전통적 OOP: ECS:GameObject Entity (ID만 보유) ├── Health Components (순수 데이터) ├── Transform Systems (순수 로직) └── Renderer핵심 원칙: 데이터와 로직을 분리하고, 같은 종류의 데이터를 연속 메모리에 배치해 캐시 효율을 극대화합니다.
기본 구조
섹션 제목: “기본 구조”Entity
섹션 제목: “Entity”using EntityId = uint32_t;
struct Entity { EntityId id; uint32_t version; // 재사용 시 구분
bool operator==(const Entity& o) const { return id == o.id && version == o.version; }};
class EntityManager { uint32_t nextId = 0; queue<EntityId> freeIds; vector<uint32_t> versions;
public: Entity create() { if (!freeIds.empty()) { EntityId id = freeIds.front(); freeIds.pop(); return {id, versions[id]}; } versions.push_back(0); return {nextId++, 0}; }
void destroy(Entity e) { versions[e.id]++; // 버전 증가로 기존 핸들 무효화 freeIds.push(e.id); }
bool isValid(Entity e) const { return e.id < versions.size() && versions[e.id] == e.version; }};Component (순수 데이터)
섹션 제목: “Component (순수 데이터)”struct Transform { float x, y, z; float rotX, rotY, rotZ; float scaleX = 1, scaleY = 1, scaleZ = 1;};
struct Velocity { float vx, vy, vz;};
struct Health { float current; float max;};
struct Renderable { uint32_t meshId; uint32_t materialId; bool visible = true;};컴포넌트 스토리지
섹션 제목: “컴포넌트 스토리지”SoA (Structure of Arrays) — 캐시 최적화
섹션 제목: “SoA (Structure of Arrays) — 캐시 최적화”template<typename T>class ComponentArray { vector<T> data_; vector<EntityId> entityToIndex_; // entity → 배열 인덱스 vector<EntityId> indexToEntity_; // 배열 인덱스 → entity
public: void insert(EntityId entity, T component) { size_t idx = data_.size(); data_.push_back(component); entityToIndex_[entity] = idx; indexToEntity_.push_back(entity); }
void remove(EntityId entity) { size_t removeIdx = entityToIndex_[entity]; size_t lastIdx = data_.size() - 1;
// 마지막 원소와 교환해 swap-remove data_[removeIdx] = data_[lastIdx]; EntityId lastEntity = indexToEntity_[lastIdx]; entityToIndex_[lastEntity] = removeIdx; indexToEntity_[removeIdx] = lastEntity;
data_.pop_back(); indexToEntity_.pop_back(); entityToIndex_.erase(entity); }
T& get(EntityId entity) { return data_[entityToIndex_[entity]]; }
// 시스템 순회: 연속 메모리 → 캐시 히트 span<T> all() { return data_; }};아키타입 (Archetype) 기반 구조
섹션 제목: “아키타입 (Archetype) 기반 구조”Unity DOTS / Flecs 스타일. 같은 컴포넌트 조합을 가진 Entity를 하나의 청크에 저장.
using ComponentMask = bitset<64>;
struct Archetype { ComponentMask mask; vector<uint8_t> chunkData; // 연속 메모리 청크 size_t entityCount = 0; size_t chunkCapacity;
// 각 컴포넌트의 청크 내 오프셋 map<size_t, size_t> componentOffsets; size_t entitySize = 0; // 엔티티당 바이트
template<typename... Ts> static Archetype create(size_t capacity = 128) { Archetype arch; arch.chunkCapacity = capacity; size_t offset = 0; // 컴포넌트별 오프셋 계산 ([&]{ arch.mask.set(ComponentTypeId<Ts>::id); arch.componentOffsets[ComponentTypeId<Ts>::id] = offset; offset += sizeof(Ts) * capacity; arch.entitySize += sizeof(Ts); }(), ...); arch.chunkData.resize(offset); return arch; }
template<typename T> T* getArray() { size_t typeId = ComponentTypeId<T>::id; return reinterpret_cast<T*>( chunkData.data() + componentOffsets[typeId]); }};System (순수 로직)
섹션 제목: “System (순수 로직)”class MovementSystem {public: void update(ComponentArray<Transform>& transforms, ComponentArray<Velocity>& velocities, float dt) { // 연속 메모리 순회 — SIMD 벡터화 가능 auto& velData = velocities.all(); for (size_t i = 0; i < velData.size(); i++) { EntityId entity = velocities.entityAt(i); if (!transforms.has(entity)) continue;
Transform& t = transforms.get(entity); Velocity& v = velData[i]; t.x += v.vx * dt; t.y += v.vy * dt; t.z += v.vz * dt; } }};
class RenderSystem {public: void update(ComponentArray<Transform>& transforms, ComponentArray<Renderable>& renderables) { for (auto& r : renderables.all()) { if (!r.visible) continue; EntityId e = renderables.entityOf(r); Transform& t = transforms.get(e); submitDrawCall(r.meshId, r.materialId, t); } }};World — 전체 조율
섹션 제목: “World — 전체 조율”class World { EntityManager entityMgr_; map<size_t, any> componentArrays_;
template<typename T> ComponentArray<T>& getArray() { size_t id = ComponentTypeId<T>::id; if (componentArrays_.find(id) == componentArrays_.end()) componentArrays_[id] = ComponentArray<T>{}; return any_cast<ComponentArray<T>&>(componentArrays_[id]); }
public: Entity createEntity() { return entityMgr_.create(); } void destroyEntity(Entity e) { entityMgr_.destroy(e); }
template<typename T> void addComponent(Entity e, T component) { getArray<T>().insert(e.id, component); }
template<typename T> T& getComponent(Entity e) { return getArray<T>().get(e.id); }
template<typename T> void removeComponent(Entity e) { getArray<T>().remove(e.id); }};사용 예시
섹션 제목: “사용 예시”World world;
// 엔티티 생성 + 컴포넌트 추가Entity player = world.createEntity();world.addComponent(player, Transform{0, 0, 0});world.addComponent(player, Velocity{1, 0, 0});world.addComponent(player, Health{100, 100});world.addComponent(player, Renderable{meshId, matId});
// 적 100마리for (int i = 0; i < 100; i++) { Entity enemy = world.createEntity(); world.addComponent(enemy, Transform{(float)i * 2, 0, 0}); world.addComponent(enemy, Velocity{-0.5f, 0, 0}); world.addComponent(enemy, Health{50, 50});}
// 시스템 업데이트 (매 프레임)MovementSystem moveSys;RenderSystem renderSys;
float dt = 0.016f;moveSys.update(world.getArrayRef<Transform>(), world.getArrayRef<Velocity>(), dt);renderSys.update(world.getArrayRef<Transform>(), world.getArrayRef<Renderable>());OOP vs ECS 성능 비교
섹션 제목: “OOP vs ECS 성능 비교”// OOP: 캐시 미스 다발 (가상 함수, 포인터 추적)class GameObject { virtual void update(float dt) = 0; // 가상 함수 → vtable 조회};vector<unique_ptr<GameObject>> objects; // 힙 포인터 → 캐시 미스
// ECS: 연속 메모리 → 캐시 라인 최적// Transform 배열: [T0][T1][T2]...[Tn] — 모두 L1 캐시// 10,000 엔티티 기준: OOP ~16ms vs ECS ~2msUnity DOTS와의 연계
섹션 제목: “Unity DOTS와의 연계”// Unity DOTS ECS (Burst Compiler + Jobs)[BurstCompile]public partial struct MovementJob : IJobEntity { public float DeltaTime;
void Execute(ref LocalTransform transform, in Velocity velocity) { transform.Position += velocity.Value * DeltaTime; }}
// 자동으로 아키타입 쿼리 → 연속 메모리 처리- Entity: 고유 ID만 보유, 컴포넌트와 직접 연결 없음
- Component: 순수 데이터, 로직 없음 (SoA로 연속 메모리 보장)
- System: 특정 컴포넌트 조합을 가진 Entity를 일괄 처리
- 아키타입: 동일 컴포넌트 조합을 청크 메모리에 밀집 배치
- OOP 대비 캐시 히트율 향상으로 대량 엔티티 처리에 강점
- Unity DOTS, Flecs, EnTT 등이 이 패턴을 채택