수만 개의 유닛이 등장하는 게임에서 기존 MonoBehaviour 방식이 갖는 한계를 극복하고, Unity DOTS의 핵심인 ECS를 도입하여 CPU 캐시 히트율을 극대화하는 렌더링 최적화 기법을 다룹니다.
도입 및 개요
유니티 인디 게임 개발자 여러분, 대규모 전투나 수많은 군중이 등장하는 씬을 구현할 때 흔히 마주치는 프레임 드랍 현상에 대해 깊이 고민해본 적 있으신가요? 기존의 MonoBehaviour 기반 아키텍처는 객체 지향 프로그래밍(OOP) 패러다임에 뿌리를 두고 있어, 각각의 게임 오브젝트가 독립적인 메모리 공간에 흩어져 할당되는 메모리 단편화(Memory Fragmentation) 문제를 야기합니다. 수천 개의 좀비가 등장하는 게임을 가정해봅시다. 매 프레임마다 수천 개의 좀비 오브젝트가 각자의 Update() 함수를 호출하며 자신의 위치를 갱신하고 애니메이션을 처리해야 합니다. 이때 CPU는 메모리 곳곳에 산재해 있는 각 좀비의 데이터(Transform, Animator, 사용자 정의 스크립트 등)를 찾기 위해 끊임없이 캐시 미스(Cache Miss)를 발생시킵니다. 메모리 접근 속도는 CPU 연산 속도에 비해 턱없이 느리기 때문에, 이러한 캐시 미스는 심각한 성능 저하의 주범이 됩니다.
이를 해결하기 위해 등장한 것이 바로 Unity의 Data-Oriented Technology Stack(DOTS)이며, 그 중심에는 Entity Component System(ECS)이 있습니다. ECS 아키텍처는 데이터와 로직을 완벽하게 분리하는 데이터 지향 설계(Data-Oriented Design)를 채택하고 있습니다. 객체를 클래스 인스턴스로 취급하는 대신, 고유 식별자인 Entity와 순수 데이터의 집합인 Component, 그리고 이 데이터를 처리하는 로직인 System으로 시스템을 분할합니다. ECS의 가장 큰 장점은 메모리의 연속성에 있습니다. 동일한 컴포넌트 조합(Archetype)을 가진 엔티티들은 메모리 상의 연속된 블록(Chunk)에 차곡차곡 저장됩니다. System이 특정 컴포넌트 데이터를 순회하며 로직을 처리할 때, CPU는 메모리의 연속된 공간을 한 번에 읽어오므로 캐시 히트율(Cache Hit Rate)이 극적으로 상승합니다. 이는 곧 데이터 처리 속도의 비약적인 향상으로 이어집니다.
구체적인 최적화 사례를 살펴보겠습니다. 1만 개의 화살이 날아가는 씬을 최적화한다고 가정해봅시다. MonoBehaviour 환경에서는 1만 개의 화살 게임 오브젝트와 각각에 붙어 있는 물리 컴포넌트, 트랜스폼 컴포넌트가 개별적으로 연산됩니다. 하지만 ECS 환경에서는 '화살'이라는 엔티티에 Translation(위치), Rotation(회전), Velocity(속도) 등의 컴포넌트 데이터만 부여합니다. 그리고 MovementSystem이라는 단일 시스템이 이 1만 개의 데이터를 연속적으로 읽어들여 위치를 업데이트합니다. 여기에 Burst Compiler와 C# Job System을 결합하면 코드가 최적화된 네이티브 기계어로 컴파일되고, 멀티코어 프로세서를 100% 활용하여 병렬 처리가 가능해집니다. 결과적으로 프레임 타임은 수십 밀리초에서 1~2 밀리초 단위로 단축되며, 모바일 기기에서도 발열 없이 매끄러운 대규모 렌더링을 구현할 수 있게 됩니다.
또한, 렌더링 측면에서도 ECS는 강력한 이점을 제공합니다. Unity의 Entities Graphics 패키지를 활용하면, ECS 데이터를 기반으로 렌더링 시스템이 작동합니다. 이 패키지는 하드웨어 인스턴싱(Hardware Instancing)과 배칭(Batching)을 자동으로 최적화하여 드로우 콜(Draw Call)을 획기적으로 줄여줍니다. 개발자는 복잡한 렌더러 설정 없이도 수만 개의 동일한 메시와 매터리얼을 가진 유닛을 단일 드로우 콜 혹은 매우 적은 수의 드로우 콜로 화면에 그릴 수 있습니다. 물론 ECS를 도입하는 것은 객체 지향 프로그래밍에 익숙한 개발자에게 큰 패러다임의 전환을 요구하며, 코딩 스타일과 프로젝트 구조를 완전히 뒤바꿔야 한다는 부담이 있습니다. 그러나 성능의 한계에 부딪힌 대규모 인디 게임 프로젝트라면, DOTS와 ECS는 더 이상 선택이 아닌 생존을 위한 필수적인 최적화 솔루션이 될 것입니다.
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Burst;
[BurstCompile]
public partial struct ProjectileMovementSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;
foreach (var (transform, velocity) in SystemAPI.Query, RefRO>().WithAll())
{
transform.ValueRW.Position += velocity.ValueRO.Value * deltaTime;
}
}
}