게임 프로그래밍 구조는 ECS를 직접 써봐야 선명해진다

profile_image
작성자 엔진구조개발자 가온
댓글 0건 조회 5회

플레이어 이동 속도 하나를 바꿨는데 전투 코드와 애니메이션 코드까지 함께 흔들린다면, 문제는 기능의 수보다 게임 프로그래밍 구조에 있을 가능성이 큽니다. 저도 작은 액션 게임을 만들 때 상속 중심 객체 구조가 가장 이해하기 쉽다고 믿었지만, 적 종류가 늘어날수록 한 클래스가 이동·체력·피격·렌더링·사운드 책임을 모두 떠안는 상황을 겪었습니다.

그래서 개인 프로젝트 한 편을 기존 객체 지향 구조에서 ECS(Entity Component System) 방식으로 옮겨 실제 제작 속도와 디버깅 난도를 비교했습니다. 처음 며칠은 코드가 오히려 낯설고 느렸지만, 기능이 스무 개를 넘어간 뒤부터는 데이터와 동작을 분리한 효과가 분명하게 나타났습니다. 이 글은 ECS가 모든 게임에 정답이라는 주장이 아니라, 직접 사용하면서 확인한 장점과 단점, 도입 범위를 현실적으로 기록한 후기입니다.

상속 트리가 흔들린 순간 ECS를 시험했다

작은 적 하나가 다섯 클래스로 갈라졌다

제가 만든 프로토타입에는 걷는 적, 날아다니는 적, 독을 거는 적이 있었습니다. 처음에는 기본 Enemy 클래스를 만들고 FlyingEnemy와 PoisonEnemy를 상속하면 충분했습니다. 그러나 날면서 독을 거는 적, 일정 체력 아래에서만 비행하는 적, 플레이어 편으로 전환되는 적이 추가되자 상속 트리가 게임 규칙을 표현하지 못했습니다. 다중 상속을 피하려고 인터페이스를 늘렸더니 객체의 실제 상태가 여러 위치에 흩어졌고, 피격 한 번을 추적하려면 네 파일을 오가야 했습니다.

ECS 실험에서는 개체를 고유 식별자인 엔티티로 두고 Position, Velocity, Health, PoisonDealer 같은 컴포넌트를 붙였습니다. MovementSystem은 Position과 Velocity가 있는 엔티티만 처리하고, PoisonSystem은 PoisonDealer와 공격 판정 데이터를 가진 엔티티만 읽게 했습니다. 날면서 독을 거는 적은 새 클래스를 만드는 대신 필요한 데이터를 조합해 만들었습니다. 게임 오브젝트의 종류가 아니라 능력의 조합으로 설계한다는 감각이 이때 처음 선명해졌습니다.

다만 첫날부터 생산성이 오른 것은 아닙니다. 객체의 메서드를 호출하던 습관 때문에 “대미지를 받는 주체는 어디에 있는가”라는 질문부터 다시 정의해야 했습니다. 기획 의도를 프로그램 구조로 바꾸는 역할이 궁금하다면 게임 기획자의 역할 설명도 참고할 만합니다. ECS에서는 기획 규칙을 상속 관계보다 데이터 조건으로 번역하는 능력이 특히 중요했습니다.

  • 효과가 컸던 기능: 상태 이상, 버프, 투사체 속성처럼 여러 개체가 공유하는 조합형 규칙이었습니다.
  • 효과가 작았던 기능: 타이틀 화면이나 한 번만 실행되는 연출처럼 개체 수가 적고 흐름이 고정된 코드였습니다.
  • 가장 먼저 드러난 문제: 컴포넌트 이름이 모호하면 기존 클래스보다 데이터 위치를 찾기 어려워졌습니다.
  • 예상 밖의 이점: 디자이너가 “이 적은 어떤 데이터를 가졌는가”를 기준으로 기능을 설명하기 시작했습니다.
처음부터 엔진 전체를 ECS로 바꾸지 말고, 상속 조합이 가장 자주 깨지는 전투 개체 한 종류만 옮겨보는 편이 학습 비용을 정확히 측정하기 좋았습니다.

직접 옮겨보니 성능보다 수정 속도가 먼저 달라졌다

컴포넌트는 작게, 시스템은 읽히게 만들었다

ECS를 선택할 때 흔히 캐시 효율과 대량 처리 성능부터 기대하지만, 제 프로젝트에서 먼저 체감한 변화는 수정 범위가 좁아진 것이었습니다. 독 대미지 주기를 바꿀 때 PoisonSystem과 관련 컴포넌트만 보면 됐고, 이동 로직은 건드리지 않았습니다. 기능을 제거할 때도 상속된 메서드의 호출 경로를 확인하는 대신 해당 컴포넌트를 떼면 되었으므로 실험적인 게임 규칙을 넣고 빼는 시간이 줄었습니다.

반대로 컴포넌트를 무조건 한두 필드로 쪼개면 관리가 더 어려워졌습니다. 처음에는 CurrentHealth, MaxHealth, RegenerationRate를 각각 분리했는데, 인스펙터와 저장 데이터에서 세 값을 함께 확인하는 일이 훨씬 많았습니다. 결국 생명력이라는 동일한 수명과 변경 이유를 가진 값은 Health 컴포넌트 하나로 묶었습니다. 제가 얻은 기준은 단순합니다. 같이 읽히고, 같이 저장되며, 대체로 같이 제거되는 데이터라면 함께 두는 편이 실용적입니다.

또 하나의 변화는 수학 코드의 위치였습니다. 벡터 정규화나 거리 계산을 각 객체 메서드 안에서 반복하지 않고, 이동 시스템에서 연속된 Position과 Velocity 데이터를 처리하도록 정돈했습니다. 이 방식은 개인 수학 라이브러리를 붙이기도 편했습니다. 다만 SIMD나 구조체 배열을 사용한다는 이유만으로 빨라진다고 단정하면 위험합니다. 실제 프레임 캡처에서 반복 횟수, 메모리 이동, 분기 패턴을 확인한 뒤 최적화해야 했습니다.

제가 사용한 이전과 이후의 차이

확인 항목상속 중심 구조ECS 적용 뒤
새 적 조합하위 클래스 또는 조건문 추가기존 컴포넌트 조합으로 생성
피격 흐름 추적가상 함수와 이벤트 경로 확인Damage 이벤트 소비 시스템 확인
데이터 저장객체별 직렬화 규칙이 섞임저장 대상 컴포넌트를 명시
단위 테스트객체 초기화 의존성이 큼필요 데이터만 만든 뒤 시스템 실행
초기 학습 비용익숙하고 낮음쿼리와 수명 규칙 학습 필요
  • 한 프레임에서 두 번 이상 반복되는 쿼리는 프로파일링 대상으로 기록했습니다.
  • 렌더링 표현과 게임 규칙 데이터를 분리해 헤드리스 테스트에서도 전투 시스템을 실행했습니다.
  • 시스템 이름에는 Update 대신 의도를 넣어 ApplyDamage, IntegrateVelocity처럼 호출 결과를 드러냈습니다.
  • 컴포넌트에 복잡한 동작을 넣지 않고 데이터 유효성 검사는 생성 경계에서 수행했습니다.
  • 최적화 전후에는 동일한 적 수와 동일한 입력 기록을 재생해 프레임 시간을 비교했습니다.

한 달 사용 후 발견한 함정은 데이터 수명과 순서였다

엔티티 삭제는 즉시 처리하지 않았다

가장 오래 붙잡힌 버그는 적이 죽는 순간 엔티티를 즉시 삭제하면서 발생했습니다. 충돌 시스템이 대상을 제거한 뒤 같은 프레임의 보상 시스템이 이미 무효가 된 식별자를 읽었고, 재현 빈도가 낮아 원인을 찾기 어려웠습니다. 이후에는 DestroyRequested 태그를 붙이고 프레임의 정해진 지점에서 일괄 삭제했습니다. 생성·변경·삭제의 적용 시점을 문서로 고정하니 유사한 문제가 크게 줄었습니다.

시스템 순서도 숨은 의존성이 되기 쉬웠습니다. 입력, 이동, 충돌, 대미지, 사망 판정 순서가 바뀌면 같은 입력에도 결과가 달라졌습니다. 저는 시스템 등록 순서를 코드 한곳에서 명시하고, 각 시스템이 읽는 컴포넌트와 쓰는 컴포넌트를 짧은 표로 남겼습니다. 작업 규모가 커지면 발표 자료와 사례를 통해 다른 개발팀의 설계 실패를 살펴보는 것도 도움이 됩니다. 게임 개발자가 기술과 제작 경험을 공유하는 행사 맥락은 GDC 관련 지식백과 설명에서 확인할 수 있습니다.

디버깅 화면 없이 ECS를 쓰는 것도 추천하기 어렵습니다. 객체 디버거에서는 인스턴스 하나를 펼치면 상태가 보였지만, ECS에서는 엔티티가 가진 컴포넌트 조합과 최근 변경 이력을 별도로 보여줘야 했습니다. 저는 선택한 엔티티의 컴포넌트 목록, 생성 프레임, 마지막 수정 시스템, 삭제 예약 여부를 표시하는 작은 도구를 만들었습니다. 기능 개발 이틀 정도를 투자했지만 이후 상태 이상 중복 적용과 잘못된 스폰 문제를 찾는 시간이 훨씬 짧아졌습니다.

실전에서 지킨 운영 규칙

  1. 구조 변경을 지연합니다. 시스템 순회 중 추가·삭제 요청은 명령 버퍼에 모으고 안전한 동기화 지점에서 반영했습니다.
  2. 식별자를 오래 보관하지 않습니다. 저장이 필요하면 세대 번호를 포함한 핸들을 사용하고 접근할 때마다 유효성을 확인했습니다.
  3. 이벤트의 수명을 한 프레임으로 제한합니다. 피격이나 사망 이벤트가 다음 프레임까지 남아 두 번 소비되지 않도록 정리 시스템을 두었습니다.
  4. 시스템 순서를 테스트합니다. 중요한 전투 흐름은 예상 상태를 프레임 단위로 검증해 등록 순서가 우연히 바뀌는 일을 막았습니다.
  5. 관찰 도구를 기능으로 취급합니다. 엔티티 검색, 컴포넌트 필터, 일시 정지 후 한 단계 실행을 초기 개발 일정에 포함했습니다.
ECS의 어려움은 데이터를 나누는 데 있지 않았습니다. 그 데이터가 언제 태어나고 누가 바꾸며 어느 시점에 사라지는지를 팀 전체가 같은 규칙으로 이해하게 만드는 일이 핵심이었습니다.

프로토타입 개발자와 엔진 개발자는 다르게 선택해야 한다

짧은 프로젝트라면 혼합 구조가 더 빨랐다

주말 게임잼이나 두세 주짜리 프로토타입에서는 완전한 ECS 전환이 투자 비용을 회수하지 못할 수 있습니다. 카메라, 메뉴, 컷신처럼 개체 수가 적고 실행 순서가 직관적인 기능까지 억지로 시스템과 컴포넌트로 나누면 파일과 등록 코드만 늘어납니다. 제가 가장 만족한 방식은 씬과 사용자 인터페이스는 익숙한 객체 구조로 유지하고, 다수의 적·투사체·상태 효과가 움직이는 시뮬레이션 영역만 ECS로 처리하는 혼합형이었습니다.

비용도 라이브러리 가격만 보면 안 됩니다. 무료 오픈소스 구현을 사용하더라도 학습 시간, 디버그 도구 제작, 직렬화 변환, 팀 문서 작성이 실제 비용으로 남습니다. 기능별 투자 효과를 따질 때는 예산을 목적과 성과에 연결하는 계획예산 제도의 기본 개념처럼, “ECS 도입” 자체보다 해결할 병목과 측정할 결과를 먼저 정하는 접근이 유용했습니다. 저는 적 1,000개 업데이트 시간, 새 상태 효과 추가에 걸린 시간, 관련 버그 수를 전환 전후로 비교했습니다.

빠르게 플레이 감각을 검증하려는 1인 개발자라면 기존 엔진의 게임 오브젝트 구조를 유지하면서 투사체나 군중 처리 한 부분에만 ECS를 시험해 보시길 권합니다. 반면 수천 개 개체의 시뮬레이션과 자체 수학 라이브러리를 다루는 엔진 개발자라면 데이터 배치, 쿼리 비용, 구조 변경 시점을 직접 통제할 수 있는 ECS를 핵심 구조로 검토할 가치가 큽니다. 두 사람에게 필요한 선택이 다른 이유는 기술 수준이 아니라 프로젝트에서 가장 비싼 문제가 서로 다르기 때문입니다.

  • 프로토타입 중심 독자: 이동과 피격 시스템 두 개만 옮기고, 하루 안에 디버그 화면까지 만들 수 있는 범위로 제한합니다.
  • 엔진 중심 독자: 컴포넌트 메모리 배치, 쿼리 반복 비용, 멀티스레드 접근 규칙을 먼저 측정합니다.
  • 공통 기준: 클래스 수 감소가 아니라 기능 추가 시간과 재현 가능한 프레임 비용으로 성공 여부를 판단합니다.
  • 중단 기준: 개체 조합이 거의 늘지 않고 도구 제작비가 기능 개발비보다 커지면 기존 구조로 돌아갑니다.

게임 프로그래밍 구조는 ECS를 직접 써봐야 선명해진다

댓글목록

등록된 댓글이 없습니다.