콘텐츠 대표 이미지 - Unity DOTS 완전 정복하기

Unity DOTS 완전 정복하기

데이터 중심 설계로 게임 성능을 폭발적으로 끌어올리는 방법 🚀

안녕! 오늘은 Unity 개발자라면 한 번쯤 들어봤을 DOTS(Data-Oriented Technology Stack)에 대해 깊이 있게 파헤쳐볼 거야. 게임 개발하다 보면 "아 이거 최적화 어떻게 하지?" 하는 순간이 꼭 오잖아? 특히 수천, 수만 개의 오브젝트를 다뤄야 할 때 말이야. 바로 그럴 때 DOTS가 진가를 발휘하지! 😎

DOTS는 단순한 기술이 아니라 Unity의 게임 개발 패러다임을 완전히 바꿔놓은 혁명적인 시스템이야. 전통적인 GameObject 기반 개발에서 벗어나 데이터 중심 설계로 전환하면서, CPU의 성능을 극한까지 끌어올릴 수 있게 됐어. 마치 자동차 엔진을 통째로 바꾸는 것과 비슷하다고 할까? 🏎️

Unity DOTS 아키텍처 ECS Entity Component System 데이터와 로직 분리 Job System 멀티스레드 병렬 처리 CPU 코어 활용 극대화 Burst Compiler 네이티브 코드 최적화 초고속 컴파일 ⚡ 🚀 💎 성능 향상 = 10배 ~ 100배 전통적인 GameObject 방식 대비

🎯DOTS가 뭐길래 이렇게 난리야?

먼저 DOTS가 정확히 뭔지부터 알아보자. DOTS는 Data-Oriented Technology Stack의 약자로, Unity가 2018년부터 본격적으로 개발하기 시작한 차세대 게임 개발 프레임워크야. 기존의 객체 지향(Object-Oriented) 방식에서 데이터 지향(Data-Oriented) 방식으로 패러다임을 완전히 전환한 거지.

"그래서 뭐가 다른데?" 라고 물을 수 있어. 간단하게 말하면, 기존 방식은 GameObject 하나하나가 독립적인 객체로 존재하면서 각자의 데이터와 로직을 가지고 있었어. 근데 이게 성능 면에서는 엄청난 낭비였던 거야. CPU는 메모리에서 데이터를 가져올 때 연속된 메모리 공간에 있는 데이터를 훨씬 빠르게 읽을 수 있거든. 🧠

전통적인 GameObject 방식의 문제점

예를 들어볼게. 1만 개의 적 캐릭터가 있다고 치자. 전통적인 방식에서는:

GameObject 방식: • 각 GameObject는 Transform, Renderer, Collider, Script 등 여러 컴포넌트를 가짐
• 메모리 상에서 이 데이터들이 여기저기 흩어져 있음
• CPU가 각 GameObject를 처리할 때마다 메모리 곳곳을 뒤져야 함
• 캐시 미스(Cache Miss)가 빈번하게 발생 → 성능 저하 💥

이게 바로 캐시 효율성 문제야. CPU는 메모리보다 훨씬 빠른 캐시 메모리를 가지고 있는데, 데이터가 흩어져 있으면 이 캐시를 제대로 활용할 수 없어. 마치 도서관에서 책을 찾는데, 같은 주제의 책들이 여기저기 흩어져 있는 것과 비슷해. 찾는 데 시간이 엄청 걸리겠지?

DOTS의 해결책: 데이터 중심 설계

DOTS는 이 문제를 완전히 다른 방식으로 접근해:

DOTS 방식: • 같은 종류의 데이터를 연속된 메모리 공간에 배치 (Structure of Arrays)
• 위치 데이터는 위치끼리, 속도 데이터는 속도끼리 모아둠
• CPU가 순차적으로 데이터를 읽으면서 처리
• 캐시 효율성 극대화 → 성능 폭발적 향상 🚀

이제 같은 주제의 책들이 한 서가에 쭉 정리되어 있는 거야. 찾기도 쉽고, 한 번에 여러 권을 가져올 수도 있지. 이게 바로 DOTS의 핵심 철학이야!


🏗️DOTS의 세 가지 핵심 기둥

DOTS는 크게 세 가지 핵심 기술로 구성되어 있어. 이 세 가지가 유기적으로 결합되면서 엄청난 시너지를 만들어내지. 하나씩 자세히 알아보자!

1. ECS (Entity Component System) 🎮

ECS는 DOTS의 가장 기본이 되는 아키텍처야. 이름에서 알 수 있듯이 Entity, Component, System 세 가지 요소로 구성되어 있어.

Entity (엔티티)
엔티티는 그냥 ID야. 정말 단순하게 고유한 식별자일 뿐이지. GameObject처럼 무거운 객체가 아니라, 그냥 숫자 하나라고 생각하면 돼. 예를 들어 "적 캐릭터 #1234" 이런 식으로 말이야.

// Entity 생성 예시
Entity enemy = entityManager.CreateEntity();
// 이게 끝! 엄청 가볍지?

Component (컴포넌트)
컴포넌트는 순수한 데이터 덩어리야. 로직이 전혀 없고, 오직 데이터만 담고 있어. 예를 들어 위치, 속도, 체력 같은 거지.

// Component 정의 예시
public struct PositionComponent : IComponentData
{
    public float3 Value;
}

public struct VelocityComponent : IComponentData
{
    public float3 Value;
}

public struct HealthComponent : IComponentData
{
    public int CurrentHealth;
    public int MaxHealth;
}

여기서 중요한 건 struct를 사용한다는 거야. class가 아니라! struct는 값 타입이라서 메모리에 연속적으로 배치되거든. 이게 바로 캐시 효율성의 비밀이야. 💡

System (시스템)
시스템은 순수한 로직 덩어리야. 특정 컴포넌트 조합을 가진 엔티티들을 찾아서 처리하는 역할을 해. 데이터와 로직이 완전히 분리되는 거지.

// System 정의 예시
public partial class MovementSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;
        
        // 위치와 속도 컴포넌트를 가진 모든 엔티티 처리
        Entities.ForEach((ref PositionComponent position, 
                         in VelocityComponent velocity) =>
        {
            position.Value += velocity.Value * deltaTime;
        }).ScheduleParallel();
    }
}

보이지? Entities.ForEach로 조건에 맞는 모든 엔티티를 한 번에 처리해. 그리고 ScheduleParallel()로 병렬 처리까지! 이게 바로 DOTS의 파워야. 🔥

💡 실전 팁: ECS를 처음 접하면 "왜 이렇게 복잡하게 나눠?" 라고 생각할 수 있어. 하지만 이 분리가 핵심이야. 데이터와 로직이 분리되면 재사용성이 엄청나게 높아지고, 테스트하기도 쉬워져. 예를 들어 MovementSystem은 플레이어든 적이든 총알이든 상관없이 위치와 속도만 있으면 다 처리할 수 있거든!

2. C# Job System ⚙️

Job System은 멀티스레드 프로그래밍을 안전하고 쉽게 만들어주는 시스템이야. 전통적인 멀티스레딩은 정말 어렵고 버그 투성이가 되기 쉬운데, Job System은 이걸 Unity가 알아서 관리해줘.

왜 멀티스레드가 중요할까?
요즘 CPU는 대부분 멀티코어야. 내 컴퓨터만 봐도 8코어, 16코어 이런 식이잖아? 근데 전통적인 Unity 코드는 대부분 메인 스레드 하나에서만 돌아가. 나머지 코어들은 놀고 있는 거지. 이게 얼마나 아까운 일이야! 😱

Job System을 사용하면 작업을 여러 스레드에 분산시켜서 모든 CPU 코어를 활용할 수 있어. 8코어 CPU라면 이론상 8배 빠르게 처리할 수 있는 거지!

// Job 정의 예시
[BurstCompile]
public struct VelocityJob : IJobParallelFor
{
    public NativeArray<float3> positions;
    [ReadOnly] public NativeArray<float3> velocities;
    public float deltaTime;
    
    public void Execute(int index)
    {
        positions[index] += velocities[index] * deltaTime;
    }
}

// Job 실행
var job = new VelocityJob
{
    positions = positionArray,
    velocities = velocityArray,
    deltaTime = Time.deltaTime
};

JobHandle handle = job.Schedule(positionArray.Length, 64);
handle.Complete();

IJobParallelFor를 사용하면 배열의 각 요소를 병렬로 처리할 수 있어. Schedule 함수의 두 번째 파라미터(64)는 배치 크기인데, 이건 한 스레드가 한 번에 처리할 요소 개수야. 이 값을 조정해서 성능을 튜닝할 수 있지.

⚠️ 주의사항: Job System을 사용할 때는 NativeArray 같은 네이티브 컬렉션을 사용해야 해. 일반 C# 배열이나 List는 사용할 수 없어. 왜냐하면 Job은 메인 스레드가 아닌 다른 스레드에서 실행되는데, 일반 C# 객체는 스레드 안전하지 않거든. NativeArray는 Unity가 메모리를 직접 관리해서 스레드 안전을 보장해줘.

Job의 종류들
Job System에는 여러 종류의 Job이 있어:

• IJob: 단일 작업을 다른 스레드에서 실행
• IJobParallelFor: 배열의 각 요소를 병렬로 처리
• IJobParallelForTransform: Transform 배열을 병렬로 처리
• IJobEntityBatch: ECS 엔티티를 배치 단위로 병렬 처리

상황에 맞는 Job 타입을 선택하는 게 중요해. 대부분의 경우 IJobParallelFor나 IJobEntityBatch를 많이 사용하게 될 거야.

3. Burst Compiler 💨

Burst Compiler는 DOTS의 마지막 퍼즐 조각이자, 성능의 최종 비밀 병기야. C# 코드를 고도로 최적화된 네이티브 코드로 컴파일해주는 컴파일러지.

일반적으로 C# 코드는 IL(Intermediate Language)로 컴파일되고, 런타임에 JIT(Just-In-Time) 컴파일러가 네이티브 코드로 변환해. 근데 이 과정에서 최적화가 제한적이야. Burst는 이걸 완전히 다르게 접근해.

Burst의 마법 ✨

• LLVM 기반: 업계 최고 수준의 컴파일러 인프라 사용
• SIMD 최적화: CPU의 벡터 연산 명령어를 최대한 활용
• 자동 벡터화: 루프를 자동으로 벡터 연산으로 변환
• 인라이닝: 함수 호출 오버헤드 제거
• 레지스터 최적화: CPU 레지스터를 효율적으로 활용

사용법은 엄청 간단해. 그냥 [BurstCompile] 어트리뷰트를 붙이면 끝!

[BurstCompile]
public struct MyJob : IJob
{
    public void Execute()
    {
        // 이 코드가 초고속으로 실행됨!
    }
}

실제 성능 차이는 어마어마해. 같은 코드를 Burst 없이 실행하면 10ms 걸리는데, Burst를 켜면 0.5ms로 줄어드는 경우도 흔해. 20배 빠른 거지! 🚀

💡 Burst 최적화 팁: 1. 수학 연산에 Unity.Mathematics 사용: float3, quaternion 등 Burst에 최적화된 타입들
2. 분기문 최소화: if문이 많으면 최적화가 어려워짐
3. 함수 호출 줄이기: 인라이닝이 안 되는 함수는 성능 저하
4. 메모리 접근 패턴 최적화: 순차적 접근이 랜덤 접근보다 훨씬 빠름

Burst Inspector라는 도구를 사용하면 컴파일된 어셈블리 코드를 볼 수 있어. 어떻게 최적화됐는지 확인할 수 있지. 처음엔 어셈블리 코드가 무섭게 보이겠지만, 익숙해지면 성능 튜닝에 엄청 도움이 돼!

성능 비교: GameObject vs DOTS GameObject 1x 기준 ECS Only 3x ~ 5x ECS + Jobs 10x ~ 20x Full DOTS 50x ~ 100x 🏆

🛠️DOTS 실전 활용하기

이론은 충분히 봤으니, 이제 실제로 어떻게 사용하는지 알아보자! 간단한 예제부터 시작해서 점점 복잡한 시스템까지 만들어볼 거야.

프로젝트 설정하기

먼저 Unity 프로젝트에 DOTS 패키지를 설치해야 해. Package Manager를 열고 다음 패키지들을 설치하자:

필수 패키지: • Entities: ECS 핵심 패키지
• Hybrid Renderer: ECS 엔티티를 렌더링하기 위한 패키지
• Unity Physics 또는 Havok Physics: 물리 엔진
• Burst: Burst Compiler (보통 자동으로 설치됨)
• Collections: NativeArray 등 네이티브 컬렉션
• Mathematics: 수학 라이브러리

Unity 2022 LTS 이상을 사용하는 걸 추천해. DOTS는 계속 발전하고 있어서 최신 버전일수록 안정적이고 기능도 많거든.

첫 번째 예제: 수천 개의 큐브 움직이기

가장 기본적인 예제로 시작해보자. 10,000개의 큐브를 화면에 띄우고 각자 다른 속도로 움직이게 만들 거야. 전통적인 GameObject 방식으로는 프레임이 뚝뚝 끊기겠지만, DOTS로는 부드럽게 돌아갈 거야! 😎

Step 1: 컴포넌트 정의

using Unity.Entities;
using Unity.Mathematics;

// 위치 컴포넌트 (이미 Unity.Transforms에 있지만 예시로)
public struct PositionComponent : IComponentData
{
    public float3 Value;
}

// 속도 컴포넌트
public struct VelocityComponent : IComponentData
{
    public float3 Value;
}

// 회전 속도 컴포넌트
public struct RotationSpeedComponent : IComponentData
{
    public float Value;
}

Step 2: 엔티티 생성 시스템

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Rendering;
using UnityEngine;

public class CubeSpawner : MonoBehaviour
{
    public Mesh cubeMesh;
    public Material cubeMaterial;
    public int cubeCount = 10000;
    public float spawnRadius = 100f;
    
    void Start()
    {
        EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
        
        // Archetype 생성 (어떤 컴포넌트 조합을 가질지 정의)
        EntityArchetype cubeArchetype = entityManager.CreateArchetype(
            typeof(LocalToWorld),
            typeof(Translation),
            typeof(Rotation),
            typeof(VelocityComponent),
            typeof(RotationSpeedComponent),
            typeof(RenderMesh),
            typeof(RenderBounds)
        );
        
        // 렌더링 설정
        RenderMeshDescription renderMeshDescription = new RenderMeshDescription
        {
            FilterSettings = RenderFilterSettings.Default,
            LightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off
        };
        
        RenderMeshArray renderMeshArray = new RenderMeshArray(
            new Material[] { cubeMaterial },
            new Mesh[] { cubeMesh }
        );
        
        // 큐브 생성
        for (int i = 0; i < cubeCount; i++)
        {
            Entity cube = entityManager.CreateEntity(cubeArchetype);
            
            // 랜덤 위치
            float3 randomPos = new float3(
                UnityEngine.Random.Range(-spawnRadius, spawnRadius),
                UnityEngine.Random.Range(-spawnRadius, spawnRadius),
                UnityEngine.Random.Range(-spawnRadius, spawnRadius)
            );
            
            entityManager.SetComponentData(cube, new Translation { Value = randomPos });
            entityManager.SetComponentData(cube, new Rotation { Value = quaternion.identity });
            
            // 랜덤 속도
            float3 randomVelocity = new float3(
                UnityEngine.Random.Range(-5f, 5f),
                UnityEngine.Random.Range(-5f, 5f),
                UnityEngine.Random.Range(-5f, 5f)
            );
            
            entityManager.SetComponentData(cube, new VelocityComponent { Value = randomVelocity });
            entityManager.SetComponentData(cube, new RotationSpeedComponent 
            { 
                Value = UnityEngine.Random.Range(0.5f, 3f) 
            });
            
            // 렌더링 설정
            RenderMeshUtility.AddComponents(
                cube,
                entityManager,
                renderMeshDescription,
                renderMeshArray,
                MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0)
            );
        }
        
        Debug.Log($"{cubeCount}개의 큐브 생성 완료!");
    }
}

Step 3: 움직임 시스템

using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

[BurstCompile]
public partial struct MovementSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        float deltaTime = SystemAPI.Time.DeltaTime;
        
        // 모든 큐브의 위치 업데이트
        foreach (var (transform, velocity) in 
                 SystemAPI.Query<RefRW<Translation>, RefRO<VelocityComponent>>())
        {
            transform.ValueRW.Value += velocity.ValueRO.Value * deltaTime;
            
            // 경계 체크 (범위를 벗어나면 반대편으로)
            if (math.abs(transform.ValueRO.Value.x) > 100f)
                transform.ValueRW.Value.x *= -1;
            if (math.abs(transform.ValueRO.Value.y) > 100f)
                transform.ValueRW.Value.y *= -1;
            if (math.abs(transform.ValueRO.Value.z) > 100f)
                transform.ValueRW.Value.z *= -1;
        }
    }
}

Step 4: 회전 시스템

[BurstCompile]
public partial struct RotationSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        float deltaTime = SystemAPI.Time.DeltaTime;
        
        foreach (var (rotation, rotSpeed) in 
                 SystemAPI.Query<RefRW<Rotation>, RefRO<RotationSpeedComponent>>())
        {
            rotation.ValueRW.Value = math.mul(
                rotation.ValueRO.Value,
                quaternion.RotateY(rotSpeed.ValueRO.Value * deltaTime)
            );
        }
    }
}

이제 실행해보면 10,000개의 큐브가 각자 다른 속도로 움직이고 회전하는 걸 볼 수 있어! 그것도 60fps 이상으로 부드럽게 말이야. 전통적인 방식으로 했다면? 아마 10fps도 안 나왔을 거야. 😅

💡 성능 측정 팁: Unity Profiler를 열어서 실제 성능을 확인해봐. Window > Analysis > Profiler에서 볼 수 있어. CPU Usage를 보면 여러 스레드가 골고루 사용되는 걸 확인할 수 있을 거야. 이게 바로 Job System의 위력이지!

두 번째 예제: 보이드 알고리즘 (Boids)

이번엔 좀 더 복잡한 예제를 해보자. 보이드 알고리즘은 새 떼나 물고기 떼의 움직임을 시뮬레이션하는 거야. 각 개체가 주변 개체들과 상호작용하면서 자연스러운 집단 행동을 만들어내지. 이런 건 DOTS가 진짜 빛을 발하는 분야야! 🐦

보이드 알고리즘의 세 가지 규칙:

1. 분리(Separation): 너무 가까운 이웃과는 거리를 둠
2. 정렬(Alignment): 주변 이웃들의 평균 방향으로 움직임
3. 응집(Cohesion): 이웃들의 중심을 향해 움직임
using Unity.Entities;
using Unity.Mathematics;

// 보이드 설정 컴포넌트
public struct BoidComponent : IComponentData
{
    public float SeparationWeight;
    public float AlignmentWeight;
    public float CohesionWeight;
    public float SeparationDistance;
    public float NeighborDistance;
    public float MaxSpeed;
}

// 보이드 시스템
[BurstCompile]
public partial struct BoidSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        float deltaTime = SystemAPI.Time.DeltaTime;
        
        // 모든 보이드의 위치와 속도를 배열로 수집
        var boidQuery = SystemAPI.QueryBuilder()
            .WithAll<Translation, VelocityComponent, BoidComponent>()
            .Build();
        
        int boidCount = boidQuery.CalculateEntityCount();
        
        // Job으로 처리
        var job = new BoidJob
        {
            DeltaTime = deltaTime,
            // ... 필요한 데이터 설정
        };
        
        state.Dependency = job.ScheduleParallel(state.Dependency);
    }
}

[BurstCompile]
public partial struct BoidJob : IJobEntity
{
    public float DeltaTime;
    
    public void Execute(
        ref Translation translation,
        ref VelocityComponent velocity,
        in BoidComponent boid)
    {
        float3 separation = float3.zero;
        float3 alignment = float3.zero;
        float3 cohesion = float3.zero;
        int neighborCount = 0;
        
        // 주변 보이드들과의 상호작용 계산
        // (실제로는 공간 분할 자료구조를 사용해야 효율적)
        
        // 세 가지 힘을 합산
        float3 totalForce = 
            separation * boid.SeparationWeight +
            alignment * boid.AlignmentWeight +
            cohesion * boid.CohesionWeight;
        
        // 속도 업데이트
        velocity.Value += totalForce * DeltaTime;
        
        // 최대 속도 제한
        float speed = math.length(velocity.Value);
        if (speed > boid.MaxSpeed)
        {
            velocity.Value = math.normalize(velocity.Value) * boid.MaxSpeed;
        }
        
        // 위치 업데이트
        translation.Value += velocity.Value * DeltaTime;
    }
}

보이드 알고리즘의 핵심은 각 개체가 주변 이웃들을 찾아서 상호작용하는 거야. 문제는 모든 개체가 모든 개체를 확인하면 O(N²) 복잡도가 되어서 느려진다는 거지. 이걸 해결하려면 공간 분할(Spatial Partitioning)을 사용해야 해.

DOTS에서는 이를 위해 여러 방법을 사용할 수 있어:

• Grid-based Hashing: 공간을 그리드로 나누고 해시 테이블로 관리
• Octree/Quadtree: 계층적 공간 분할
• KD-Tree: k차원 이진 탐색 트리

실제 프로젝트에서는 Unity Physics의 충돌 감지 시스템을 활용하거나, 직접 공간 분할 자료구조를 구현해야 해. 이렇게 하면 수만 개의 보이드도 실시간으로 시뮬레이션할 수 있어!

세 번째 예제: 파티클 시스템

DOTS로 커스텀 파티클 시스템을 만들면 Unity의 기본 파티클 시스템보다 훨씬 많은 파티클을 처리할 수 있어. 100만 개 이상도 가능하지! 🎆

public struct ParticleComponent : IComponentData
{
    public float LifeTime;
    public float MaxLifeTime;
    public float3 StartPosition;
    public float4 StartColor;
    public float4 EndColor;
}

[BurstCompile]
public partial struct ParticleSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        float deltaTime = SystemAPI.Time.DeltaTime;
        var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.TempJob);
        
        foreach (var (particle, translation, entity) in 
                 SystemAPI.Query<RefRW<ParticleComponent>, RefRW<Translation>>()
                 .WithEntityAccess())
        {
            // 수명 감소
            particle.ValueRW.LifeTime -= deltaTime;
            
            // 수명이 다하면 제거
            if (particle.ValueRO.LifeTime <= 0)
            {
                ecb.DestroyEntity(entity);
                continue;
            }
            
            // 색상 보간
            float t = 1f - (particle.ValueRO.LifeTime / particle.ValueRO.MaxLifeTime);
            // 색상 업데이트 로직...
            
            // 중력 적용
            translation.ValueRW.Value.y -= 9.81f * deltaTime;
        }
        
        ecb.Playback(state.EntityManager);
        ecb.Dispose();
    }
}

파티클 시스템에서 중요한 건 EntityCommandBuffer야. 엔티티를 생성하거나 제거하는 작업은 즉시 할 수 없고, CommandBuffer에 기록했다가 나중에 일괄 처리해야 해. 이게 멀티스레드 환경에서 안전하게 작동하는 방법이거든.


🎓고급 테크닉과 최적화

기본을 넘어서 진짜 프로처럼 DOTS를 활용하는 방법들을 알아보자. 이 부분은 좀 어려울 수 있지만, 성능을 극한까지 끌어올리고 싶다면 꼭 알아야 해!

Chunk Iteration과 메모리 레이아웃

ECS는 내부적으로 엔티티들을 Chunk라는 16KB 크기의 메모리 블록에 저장해. 같은 Archetype(컴포넌트 조합)을 가진 엔티티들이 같은 Chunk에 모여 있지.

이걸 이해하면 성능을 더 끌어올릴 수 있어:

[BurstCompile]
public partial struct OptimizedSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // Chunk 단위로 반복 - 더 빠름!
        var query = SystemAPI.QueryBuilder()
            .WithAll<Translation, VelocityComponent>()
            .Build();
        
        var chunks = query.ToArchetypeChunkArray(Unity.Collections.Allocator.TempJob);
        
        var translationType = state.GetComponentTypeHandle<Translation>(false);
        var velocityType = state.GetComponentTypeHandle<VelocityComponent>(true);
        
        foreach (var chunk in chunks)
        {
            var translations = chunk.GetNativeArray(ref translationType);
            var velocities = chunk.GetNativeArray(ref velocityType);
            
            // 이제 배열 단위로 처리 - SIMD 최적화 가능!
            for (int i = 0; i < chunk.Count; i++)
            {
                translations[i] = new Translation 
                { 
                    Value = translations[i].Value + velocities[i].Value * SystemAPI.Time.DeltaTime 
                };
            }
        }
        
        chunks.Dispose();
    }
}

Chunk 단위로 처리하면 메모리 접근 패턴이 최적화되고, Burst가 SIMD 명령어를 더 잘 활용할 수 있어. 특히 대량의 엔티티를 처리할 때 차이가 크지.

Enableable Components

Unity DOTS 1.0부터 도입된 기능인데, 컴포넌트를 제거하지 않고 활성화/비활성화할 수 있어. 이게 왜 중요하냐면, 컴포넌트를 추가/제거하면 엔티티가 다른 Chunk로 이동해야 하는데, 이게 비용이 크거든.

public struct DamageableComponent : IComponentData, IEnableableComponent
{
    public int Health;
}

// 사용 예시
public partial struct DamageSystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        foreach (var (damageable, entity) in 
                 SystemAPI.Query<RefRW<DamageableComponent>>()
                 .WithEntityAccess())
        {
            if (damageable.ValueRO.Health <= 0)
            {
                // 컴포넌트 비활성화 (제거하지 않음)
                SystemAPI.SetComponentEnabled<DamageableComponent>(entity, false);
            }
        }
    }
}

이렇게 하면 "죽은" 엔티티를 효율적으로 관리할 수 있어. 나중에 다시 활성화할 수도 있고!

Aspect 패턴

Aspect는 여러 컴포넌트를 하나로 묶어서 편하게 사용할 수 있게 해주는 기능이야. 코드 재사용성이 엄청 좋아지지.

public readonly partial struct TransformAspect : IAspect
{
    public readonly RefRW<Translation> Translation;
    public readonly RefRW<Rotation> Rotation;
    
    public float3 Position
    {
        get => Translation.ValueRO.Value;
        set => Translation.ValueRW.Value = value;
    }
    
    public quaternion Orientation
    {
        get => Rotation.ValueRO.Value;
        set => Rotation.ValueRW.Value = value;
    }
    
    public void MoveForward(float distance)
    {
        Position += math.forward(Orientation) * distance;
    }
}

// 사용
public partial struct MovementSystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        foreach (var transform in SystemAPI.Query<TransformAspect>())
        {
            transform.MoveForward(5f * SystemAPI.Time.DeltaTime);
        }
    }
}

Aspect를 사용하면 코드가 훨씬 깔끔해지고, 같은 로직을 여러 시스템에서 재사용할 수 있어. 특히 복잡한 게임에서 진가를 발휘하지!

Baker와 Authoring

DOTS 엔티티를 에디터에서 편집하려면 Authoring과 Baker를 사용해야 해. 이게 GameObject와 ECS를 연결하는 다리 역할을 하지.

// Authoring Component (에디터에서 편집 가능)
public class EnemyAuthoring : MonoBehaviour
{
    public float MoveSpeed = 5f;
    public int Health = 100;
    public GameObject ProjectilePrefab;
}

// Baker (GameObject를 Entity로 변환)
public class EnemyBaker : Baker<EnemyAuthoring>
{
    public override void Bake(EnemyAuthoring authoring)
    {
        var entity = GetEntity(TransformUsageFlags.Dynamic);
        
        AddComponent(entity, new VelocityComponent 
        { 
            Value = new float3(0, 0, authoring.MoveSpeed) 
        });
        
        AddComponent(entity, new HealthComponent 
        { 
            CurrentHealth = authoring.Health,
            MaxHealth = authoring.Health
        });
        
        // Prefab 참조도 변환 가능
        AddComponent(entity, new ProjectilePrefabComponent 
        { 
            Prefab = GetEntity(authoring.ProjectilePrefab, TransformUsageFlags.Dynamic) 
        });
    }
}

Baker를 사용하면 에디터에서 GameObject로 작업하다가, 런타임에는 자동으로 ECS 엔티티로 변환돼. 양쪽의 장점을 다 가져갈 수 있는 거지! 🎨

성능 프로파일링과 디버깅

DOTS 코드를 최적화하려면 제대로 측정하는 게 중요해. Unity Profiler에서 확인할 수 있는 것들:

주요 지표들: • System Update Time: 각 시스템이 얼마나 시간을 쓰는지
• Job Scheduling: Job이 제대로 병렬화되고 있는지
• Structural Changes: 엔티티 생성/삭제가 얼마나 자주 일어나는지
• Memory Allocations: 메모리 할당이 과도하게 일어나는지

특히 Structural Changes는 조심해야 해. 엔티티를 생성하거나 컴포넌트를 추가/제거하면 전체 시스템이 동기화되어야 해서 성능이 떨어지거든. 가능하면 미리 엔티티를 만들어두고 재사용하는 게 좋아 (Object Pooling 패턴).

💡 디버깅 팁: 1. Entity Debugger: Window > Entities > Hierarchy에서 런타임 엔티티 확인
2. System Inspector: 각 시스템의 실행 순서와 의존성 확인
3. Burst Inspector: 컴파일된 어셈블리 코드 확인
4. Memory Profiler: 메모리 사용량 상세 분석

🎮실전 프로젝트 사례

이론과 간단한 예제는 충분히 봤으니, 이제 실제 게임에서 DOTS를 어떻게 활용하는지 알아보자. 여러 장르별로 DOTS가 빛을 발하는 상황들을 살펴볼게!

RTS 게임: 대규모 유닛 관리

RTS(Real-Time Strategy) 게임은 DOTS의 완벽한 활용 사례야. 수백, 수천 개의 유닛이 동시에 움직이고 전투하는 상황에서 DOTS는 필수적이지.

핵심 시스템들:

• Path Finding System: A* 알고리즘을 Job으로 병렬 처리
• Formation System: 유닛들의 대형 유지
• Combat System: 전투 계산 및 데미지 처리
• Resource Gathering System: 자원 채집 로직
• Building System: 건물 건설 및 관리
// RTS 유닛 컴포넌트
public struct RTSUnitComponent : IComponentData
{
    public int TeamID;
    public float AttackRange;
    public float AttackDamage;
    public float AttackCooldown;
    public float CurrentCooldown;
}

public struct TargetComponent : IComponentData
{
    public Entity Target;
    public float3 TargetPosition;
}

// 전투 시스템
[BurstCompile]
public partial struct CombatSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        float deltaTime = SystemAPI.Time.DeltaTime;
        var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.TempJob);
        
        foreach (var (unit, target, translation, entity) in 
                 SystemAPI.Query<RefRW<RTSUnitComponent>, 
                                RefRO<TargetComponent>, 
                                RefRO<Translation>>()
                 .WithEntityAccess())
        {
            // 쿨다운 감소
            unit.ValueRW.CurrentCooldown -= deltaTime;
            
            if (unit.ValueRO.CurrentCooldown > 0)
                continue;
            
            // 타겟과의 거리 확인
            float distance = math.distance(
                translation.ValueRO.Value,
                target.ValueRO.TargetPosition
            );
            
            if (distance <= unit.ValueRO.AttackRange)
            {
                // 공격!
                ecb.AddComponent(target.ValueRO.Target, new DamageEvent 
                { 
                    Damage = unit.ValueRO.AttackDamage 
                });
                
                unit.ValueRW.CurrentCooldown = unit.ValueRO.AttackCooldown;
            }
        }
        
        ecb.Playback(state.EntityManager);
        ecb.Dispose();
    }
}

RTS 게임에서 특히 중요한 건 공간 쿼리야. "이 유닛 주변 10미터 안에 적이 있나?" 같은 질문을 초당 수천 번 해야 하거든. 이를 위해 Unity Physics나 커스텀 공간 분할 자료구조를 사용해야 해.

슈팅 게임: 총알과 파티클

슈팅 게임에서는 엄청난 수의 총알, 폭발 이펙트, 파편 등을 처리해야 해. DOTS 없이는 불가능한 수준이지.

public struct BulletComponent : IComponentData
{
    public float Speed;
    public float Damage;
    public float LifeTime;
    public int TeamID;
}

[BurstCompile]
public partial struct BulletSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        float deltaTime = SystemAPI.Time.DeltaTime;
        var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.TempJob);
        
        // Physics World 가져오기 (충돌 감지용)
        var physicsWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>().PhysicsWorld;
        
        foreach (var (bullet, translation, rotation, entity) in 
                 SystemAPI.Query<RefRW<BulletComponent>, 
                                RefRW<Translation>, 
                                RefRO<Rotation>>()
                 .WithEntityAccess())
        {
            // 총알 이동
            float3 forward = math.forward(rotation.ValueRO.Value);
            float3 newPos = translation.ValueRO.Value + forward * bullet.ValueRO.Speed * deltaTime;
            
            // Raycast로 충돌 체크
            var raycastInput = new RaycastInput
            {
                Start = translation.ValueRO.Value,
                End = newPos,
                Filter = CollisionFilter.Default
            };
            
            if (physicsWorld.CastRay(raycastInput, out var hit))
            {
                // 충돌 처리
                ecb.AddComponent(hit.Entity, new DamageEvent 
                { 
                    Damage = bullet.ValueRO.Damage,
                    HitPoint = hit.Position,
                    HitNormal = hit.SurfaceNormal
                });
                
                // 총알 제거
                ecb.DestroyEntity(entity);
            }
            else
            {
                translation.ValueRW.Value = newPos;
                
                // 수명 감소
                bullet.ValueRW.LifeTime -= deltaTime;
                if (bullet.ValueRO.LifeTime <= 0)
                {
                    ecb.DestroyEntity(entity);
                }
            }
        }
        
        ecb.Playback(state.EntityManager);
        ecb.Dispose();
    }
}

슈팅 게임에서는 Object Pooling이 정말 중요해. 총알을 매번 생성/제거하면 Structural Change 때문에 성능이 떨어지거든. 미리 총알 엔티티를 만들어두고 활성화/비활성화하는 방식이 훨씬 효율적이야.

시뮬레이션 게임: 복잡한 시스템 상호작용

도시 건설, 경영 시뮬레이션 같은 게임에서는 수많은 시스템이 서로 영향을 주고받아. DOTS의 시스템 기반 아키텍처가 딱 맞아떨어지지.

시뮬레이션 게임의 시스템 예시: • Population System: 인구 증가/감소
• Economy System: 자원 생산/소비
• Happiness System: 시민 만족도
• Traffic System: 교통 흐름
• Pollution System: 환경 오염
• Weather System: 날씨 변화

이런 시스템들이 서로 데이터를 주고받으면서 복잡한 시뮬레이션을 만들어내. DOTS의 데이터 중심 설계가 이런 상호작용을 명확하고 효율적으로 만들어줘.

오픈 월드 게임: 스트리밍과 LOD

오픈 월드 게임에서는 넓은 맵을 효율적으로 관리해야 해. DOTS는 이를 위한 강력한 도구들을 제공하지.

// LOD (Level of Detail) 컴포넌트
public struct LODComponent : IComponentData
{
    public int CurrentLOD;
    public float LOD0Distance;
    public float LOD1Distance;
    public float LOD2Distance;
}

[BurstCompile]
public partial struct LODSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // 카메라 위치 가져오기
        float3 cameraPos = SystemAPI.GetSingleton<CameraPositionComponent>().Value;
        
        foreach (var (lod, translation) in 
                 SystemAPI.Query<RefRW<LODComponent>, RefRO<Translation>>())
        {
            float distance = math.distance(cameraPos, translation.ValueRO.Value);
            
            int newLOD;
            if (distance < lod.ValueRO.LOD0Distance)
                newLOD = 0;
            else if (distance < lod.ValueRO.LOD1Distance)
                newLOD = 1;
            else if (distance < lod.ValueRO.LOD2Distance)
                newLOD = 2;
            else
                newLOD = 3;
            
            if (newLOD != lod.ValueRO.CurrentLOD)
            {
                lod.ValueRW.CurrentLOD = newLOD;
                // LOD 변경 이벤트 발생
            }
        }
    }
}

오픈 월드에서는 Scene Streaming도 중요해. 플레이어가 이동하면서 필요한 부분만 로드하고 언로드하는 거지. DOTS의 SubScene 시스템이 이를 지원해줘.


⚠️DOTS의 한계와 주의사항

DOTS가 완벽한 건 아니야. 모든 상황에 적합한 것도 아니고, 사용하면서 주의해야 할 점들이 있어. 솔직하게 얘기해보자!

학습 곡선이 가파르다

DOTS는 전통적인 Unity 개발과 완전히 다른 사고방식을 요구해. 객체 지향에서 데이터 지향으로 패러다임을 바꿔야 하거든. 처음에는 정말 어렵게 느껴질 수 있어.

초보자들이 자주 하는 실수: • GameObject 방식으로 생각하면서 DOTS 사용하기
• 모든 걸 DOTS로 만들려고 하기
• 성능 측정 없이 최적화에 집착하기
• 문서를 제대로 읽지 않고 시작하기

내 조언은, 작은 프로젝트부터 시작해서 점진적으로 배워나가는 거야. 처음부터 큰 프로젝트를 DOTS로 만들려고 하면 좌절하기 쉬워. 😅

에디터 워크플로우가 불편할 수 있다

GameObject는 에디터에서 직접 보고 편집할 수 있지만, ECS 엔티티는 그렇지 않아. Baker와 Authoring을 사용하면 되긴 하지만, 여전히 GameObject만큼 직관적이지는 않지.

특히 디자이너나 아티스트와 협업할 때 문제가 될 수 있어. 그들은 DOTS를 몰라도 되지만, 작업 방식은 이해해야 하거든.

모든 Unity 기능이 지원되는 건 아니다

DOTS는 아직 발전 중이야. 일부 Unity 기능들은 DOTS와 호환되지 않거나, 제한적으로만 작동해:

제한사항들: • 일부 렌더링 기능 (특히 커스텀 셰이더)
• 애니메이션 시스템 (Mecanim은 제한적)
• UI 시스템 (UGUI는 GameObject 기반)
• 일부 물리 기능
• 써드파티 에셋 호환성

그래서 실제 프로젝트에서는 하이브리드 접근을 많이 사용해. 성능이 중요한 부분은 DOTS로, UI나 복잡한 로직은 GameObject로 만드는 거지.

디버깅이 어렵다

DOTS 코드는 디버깅하기가 까다로워. 특히 Burst로 컴파일된 코드는 일반적인 디버거로 추적하기 어렵거든.

💡 디버깅 전략: 1. Burst 끄고 테스트: 개발 중에는 Burst를 끄고 디버깅
2. 로그 활용: Debug.Log 대신 Unity.Debug.Log 사용
3. Entity Debugger: 런타임 엔티티 상태 확인
4. 단위 테스트: 시스템별로 테스트 코드 작성
5. 시각화: Gizmos로 데이터 시각화

언제 DOTS를 사용해야 할까?

모든 프로젝트에 DOTS가 필요한 건 아니야. 다음 상황에서 DOTS를 고려해봐:

DOTS가 적합한 경우:
  • ✅ 대량의 오브젝트를 처리해야 할 때 (수천 개 이상)
  • ✅ 성능이 정말 중요한 프로젝트
  • ✅ 복잡한 시뮬레이션이나 AI
  • ✅ 멀티플레이어 게임의 서버
  • ✅ 모바일 게임의 성능 최적화
DOTS가 과할 수 있는 경우:
  • ❌ 작은 규모의 프로젝트
  • ❌ 프로토타입이나 빠른 개발이 필요할 때
  • ❌ 팀원들이 DOTS 경험이 없을 때
  • ❌ UI 중심의 게임
  • ❌ 스토리 중심의 싱글플레이어 게임

결국 프로젝트의 요구사항과 팀의 역량을 고려해서 결정해야 해. DOTS는 강력한 도구지만, 만능은 아니야!


🚀DOTS의 미래와 발전 방향

DOTS는 계속 발전하고 있어. Unity Technologies가 엄청난 리소스를 투입하고 있고, 커뮤니티도 활발하지. 앞으로 어떻게 발전할지 살펴보자!

Unity 6와 DOTS 1.0+

Unity 6 (구 Unity 2023 LTS)에서 DOTS 1.0이 정식 출시됐어. 이전 버전들은 프리뷰나 실험적 기능이었는데, 이제는 프로덕션에서 사용할 수 있을 만큼 안정화됐지.

DOTS 1.0의 주요 개선사항: • API 안정화 및 표준화
• 에디터 워크플로우 개선
• 성능 최적화
• 더 나은 문서와 샘플
• NetCode for Entities 통합

멀티플레이어와 NetCode

DOTS의 가장 흥미로운 활용 분야 중 하나가 멀티플레이어야. NetCode for Entities는 DOTS 기반의 네트워킹 솔루션인데, 엄청난 수의 플레이어와 엔티티를 동기화할 수 있어.

전통적인 네트워킹은 GameObject 기반이라 수백 명의 플레이어를 처리하기 어려웠어. 하지만 NetCode는 수천 명도 가능하게 만들어줘. MMO나 배틀로얄 같은 대규모 멀티플레이어 게임의 게임 체인저가 될 수 있지! 🎮

모바일 플랫폼 최적화

DOTS는 모바일 게임에서도 엄청난 가능성을 보여줘. 모바일 기기의 제한된 성능을 최대한 활용할 수 있거든.

특히 최근 모바일 CPU들이 멀티코어를 많이 탑재하면서, Job System의 효과가 더 커지고 있어. 예전에는 불가능했던 수준의 그래픽과 게임플레이를 모바일에서 구현할 수 있게 됐지.

AI와 머신러닝 통합

DOTS의 고성능 특성은 AI와 머신러닝에도 완벽해. 수천 개의 AI 에이전트를 실시간으로 시뮬레이션하거나, 복잡한 의사결정 트리를 병렬로 처리할 수 있어.

Unity ML-Agents와 DOTS를 결합하면, 대규모 강화학습 환경을 만들 수 있지. 이건 게임 AI뿐만 아니라 연구 목적으로도 엄청 유용해!


📚학습 리소스와 커뮤니티

DOTS를 제대로 배우고 싶다면 좋은 리소스를 활용하는 게 중요해. 내가 추천하는 학습 경로와 리소스들을 소개할게!

공식 문서와 샘플

Unity 공식 문서가 많이 개선됐어. 특히 DOTS 1.0부터는 문서가 체계적으로 정리되어 있지.

필수 공식 리소스: • Unity DOTS Documentation: 공식 문서
• Entities Samples: GitHub의 공식 샘플 프로젝트
• Unity Learn: 공식 튜토리얼
• Unity Blog: 최신 업데이트와 기술 아티클

커뮤니티와 포럼

DOTS 커뮤니티는 정말 활발해. 막히는 부분이 있으면 물어보면 대부분 답을 얻을 수 있어.

• Unity Forums - DOTS: 공식 포럼
• Discord - Unity DOTS: 실시간 질문/답변
• Reddit - r/Unity3D: 커뮤니티 토론
• Stack Overflow: 기술적 질문

특히 Discord는 정말 활발해. 전 세계의 DOTS 개발자들이 실시간으로 소통하고 있어. 막히는 부분이 있으면 물어보면 금방 답을 얻을 수 있지!

추천 학습 순서

DOTS를 처음 배운다면 이런 순서로 학습하는 걸 추천해:

1단계: 기초 다지기 • ECS 개념 이해하기
• 간단한 Entity와 Component 만들어보기
• 기본 System 작성해보기

2단계: Job System 익히기
• IJob, IJobParallelFor 사용해보기
• NativeArray와 네이티브 컬렉션 이해하기
• Job 의존성 관리 배우기

3단계: Burst 최적화
• Burst Compiler 활성화하기
• Unity.Mathematics 사용하기
• 성능 프로파일링하기

4단계: 실전 프로젝트
• 작은 게임 프로토타입 만들기
• 성능 측정 및 최적화
• 하이브리드 접근 방식 시도하기

재능넷에서 DOTS 배우기

혼자 배우기 어렵다면 재능넷(https://www.jaenung.net)에서 전문가의 도움을 받는 것도 좋은 방법이야. DOTS는 학습 곡선이 가파르기 때문에, 경험 있는 개발자에게 멘토링을 받으면 훨씬 빠르게 배울 수 있거든.

재능넷에는 Unity 전문가들이 많이 있어서, 1:1 코칭이나 코드 리뷰를 받을 수 있어. 특히 실무 프로젝트에 DOTS를 적용하려고 할 때, 전문가의 조언이 정말 큰 도움이 되지. 시행착오를 줄이고 올바른 방향으로 갈 수 있거든! 💡

DOTS 학습 로드맵 1단계 ECS 기초 2단계 Job System 3단계 Burst 4단계 실전 🏆 Entity Component System 병렬 처리 NativeArray 의존성 관리 최적화 프로파일링 SIMD 프로젝트 최적화 디버깅 DOTS 마스터!

💭마치며

와, 정말 긴 여정이었지? DOTS에 대해 거의 모든 걸 다뤄봤어. 기초 개념부터 실전 활용, 최적화 테크닉, 그리고 미래 전망까지 말이야.

DOTS는 분명 배우기 어려운 기술이야. 전통적인 Unity 개발과는 완전히 다른 사고방식을 요구하거든. 하지만 한번 익숙해지면, 그 파워는 정말 대단해. 이전에는 불가능했던 수준의 성능과 규모를 달성할 수 있지.

내가 강조하고 싶은 건, DOTS가 만능은 아니라는 거야. 모든 프로젝트에 DOTS를 사용할 필요는 없어. 프로젝트의 요구사항, 팀의 역량, 개발 일정 등을 종합적으로 고려해서 결정해야 해.

하지만 대규모 시뮬레이션, 멀티플레이어 게임, 모바일 최적화 등이 필요한 프로젝트라면? DOTS는 정말 강력한 무기가 될 거야. 특히 앞으로 게임이 점점 더 복잡해지고 규모가 커질수록, DOTS 같은 기술의 중요성은 더 커질 거라고 생각해.

지금 당장 DOTS를 배우기 시작하면, 미래의 게임 개발 트렌드를 선도하는 개발자가 될 수 있어. 어렵더라도 포기하지 말고 꾸준히 학습해봐. 작은 프로젝트부터 시작해서 점점 규모를 키워가면, 어느새 DOTS 마스터가 되어 있을 거야! 🚀

그리고 혼자 배우기 힘들다면, 커뮤니티를 적극 활용하고, 필요하다면 재능넷 같은 플랫폼에서 전문가의 도움을 받는 것도 좋은 방법이야. 시행착오를 줄이고 올바른 방향으로 갈 수 있거든.

자, 이제 당신 차례야! 이 글을 읽은 걸로 끝내지 말고, 직접 Unity를 열어서 첫 번째 DOTS 프로젝트를 시작해봐. 10,000개의 큐브를 화면에 띄우고, 부드럽게 움직이는 걸 보면서 DOTS의 파워를 직접 느껴보라고! 😊

행운을 빌어! 그리고 멋진 게임 만들어봐! 🎮✨

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.