---
title: "캡슐 하나가 엔티티가 되기까지"
description: "SubScene 에 캡슐을 놓으면 커스텀 코드 없이 엔티티가 되어 GPU 인스턴싱으로 그려집니다. 그리고 Baker 는 자기 엔티티만 건드릴 수 있습니다."
author: "유니얼"
pubDate: 2026-09-09
category: "game-dev/unity/dots"
tags: ["Unity", "DOTS"]
canonical: https://blog.unialgames.com/posts/dots-baking-and-entity/
---
ECS 로 만들려면 씬에 무엇을 어떻게 놓아야 하는지부터 막힙니다.
엔티티는 인스펙터에 안 보이고, 프리팹을 그냥 `Instantiate` 하면 GameObject 가 나옵니다.

답은 **베이킹** 입니다. GameObject 로 배치하고, 그것을 엔티티로 굽습니다.

## SubScene 에 놓으면 구워진다

Main 씬 안에 SubScene 을 만들고 그 안에 캡슐 하나를 놓습니다. 그게 전부입니다.

베이킹이 자동으로 `LocalTransform`, `LocalToWorld`, `RenderMeshArray`,
`MaterialMeshInfo` 를 붙인 엔티티를 만들고, Entities Graphics 가 GPU 인스턴싱으로 그립니다.
**커스텀 코드가 한 줄도 없습니다.**

Entities Hierarchy 창을 열면 "Capsule (Live converted)" 로 보입니다.
씬 하이어라키가 아니라 이쪽을 봐야 합니다. 이게 처음에 제일 헷갈리는 지점입니다.

## 컴포넌트는 struct 다

내가 붙일 데이터는 `IComponentData` 로 만듭니다.

```csharp
public struct Enemy : IComponentData { }          // 태그. 필드가 없어도 됩니다

public struct Velocity : IComponentData
{
    public float3 Value;                           // public 이어야 시스템이 접근합니다
}
```

**`class` 로 쓰면 컴파일은 되는데 managed 컴포넌트가 됩니다.**
GC 대상이 되고, Burst 가 못 쓰고, 청크에 값으로 안 들어가서 캐시 이점이 사라집니다.
컴파일러가 안 막아 주니 습관으로 막아야 합니다. **항상 `struct` 입니다.**

필드를 `private` 로 두는 것도 같은 부류입니다. 컴파일은 되고 시스템에서 못 씁니다.

> 어셈블리 정의를 만들 때 `Unity.Transforms` 와 `Unity.Rendering` 이 참조 목록에 없어서 헤맸는데,
> **이 둘은 어셈블리가 아니라 네임스페이스**입니다.
> 각각 `Unity.Entities` 와 `Unity.Entities.Graphics` 가 제공합니다.
> `Unity.Entities`, `Unity.Entities.Graphics`, `Unity.Burst`, `Unity.Collections`,
> `Unity.Mathematics` 다섯이면 됩니다.

## Authoring 과 Baker

인스펙터에서 값을 조절하려면 MonoBehaviour 가 하나 필요합니다.
그게 authoring 이고, 그 값을 엔티티 컴포넌트로 옮기는 것이 Baker 입니다.

```csharp
public class MonsterAuthoring : MonoBehaviour { }

class MonsterBaker : Baker<MonsterAuthoring>
{
    public override void Bake(MonsterAuthoring authoring)
    {
        var e = GetEntity(TransformUsageFlags.Dynamic);
        AddComponent<Enemy>(e);
    }
}
```

**클래스 이름을 `Baker` 로 지으면 `Unity.Entities.Baker` 와 충돌합니다.**
`MonsterBaker` 처럼 앞에 뭘 붙이세요.

## Baker 는 자기 엔티티만 건드린다

여기서 한 번 크게 막혔습니다.

스포너가 몬스터 프리팹을 참조하고 있으니, 스포너의 Baker 에서
프리팹 엔티티에 `Enemy` 태그를 붙이면 될 것 같았습니다.

```csharp
var prefabEntity = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic);
AddComponent<Enemy>(prefabEntity);   // 여기서 터집니다
```

```text
InvalidOperationException: Entity doesn't belong to the current authoring component
```

**Baker 는 자기 GameObject 의 엔티티만 수정할 수 있습니다.**
참조해서 가져온 프리팹의 엔티티는 남의 것입니다.

이유가 있습니다. 베이킹은 증분으로 돕니다.
프리팹이 바뀌면 그 프리팹만 다시 굽는데, 남이 그 엔티티에 뭘 붙여 놨으면
누가 언제 다시 붙여 줘야 하는지가 안 정해집니다.
**자기 것만 건드리게 해 두면 다시 구울 범위가 명확해집니다.**

그래서 해법은 간단합니다. **프리팹이 자기 컴포넌트를 자기가 authoring 합니다.**
Monster 프리팹에 `MonsterAuthoring` 을 붙여 두면, 프리팹이 이미 `Enemy` 를 가진 채로 구워지고
`GetEntity` 로 받아 온 핸들도 그걸 그대로 들고 있습니다. 복제본이 상속받습니다.

## 목표 수를 유지하는 스폰

스포너 쪽은 이렇게 굽습니다.

```csharp
public struct SpawnConfig : IComponentData
{
    public Entity Prefab;   // 엔티티 프리팹 핸들
    public int Count;       // 목표 수. 슬라이더가 런타임에 바꿉니다
    public float Radius;
}
```

시스템은 "한 번 스폰하고 끝" 이 아니라 **매 프레임 목표 수에 맞춥니다.**

- 지금이 목표보다 적으면 부족분만 `Instantiate`
- 많으면 초과분만 `DestroyEntity`

이렇게 두면 슬라이더로 적 수를 실시간으로 조절할 수 있고,
벤치마크에서 적 수를 자동으로 스윕하는 것도 이 위에 그냥 얹힙니다.
**나중에 측정을 편하게 만든 것이 이 구조였습니다.**

`RequireForUpdate<SpawnConfig>()` 는 빼먹으면 안 됩니다.
SubScene 이 비동기로 로드되기 때문에, 없으면 로드 전에 `GetSingleton` 이 터집니다.

## 여기까지의 숫자

| 적 수 | 프레임 |
| --- | --- |
| 100 | 약 128 FPS |
| 5,000 | 약 121 FPS |
| **10,000** | **약 120에서 156 FPS** |

아직 이동도 없고 전투도 없습니다. 그냥 1만 개가 서 있고 그려지기만 합니다.
그런데 이 숫자가 뒤에 나올 모든 비교의 출발점이 됩니다.

## 컴파일이 통과했다고 되는 게 아니다

이 단계에서 컴파일을 통과하고도 안 돌았던 것들입니다.

- 어셈블리에 코드가 0개라 참조가 맞는지 확인이 안 된 상태로 초록불
- 컴포넌트를 `class` 로 만듦
- 필드를 `private` 로 둠
- Baker 코드는 썼는데 **Spawner GameObject 를 SubScene 에 안 놓음.**
  Baker 는 authoring 이 붙은 GameObject 가 SubScene 에 있어야 실행됩니다
- `RequireForUpdate` 누락

전부 에러 0개입니다. 실행하고, 쿼리로 개수를 세 보고, 화면을 봐야 알 수 있습니다.
**이 부류가 이 프로젝트 내내 반복됩니다.**