캡슐 하나가 엔티티가 되기까지
Esc

    캡슐 하나가 엔티티가 되기까지

    –UnityDOTS

    ECS 로 만들려면 씬에 무엇을 어떻게 놓아야 하는지부터 막힙니다. 엔티티는 인스펙터에 안 보이고, 프리팹을 그냥 Instantiate 하면 GameObject 가 나옵니다.

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

    SubScene 에 놓으면 구워진다

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

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

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

    컴포넌트는 struct 다

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

    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 입니다.

    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 태그를 붙이면 될 것 같았습니다.

    var prefabEntity = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic);
    AddComponent<Enemy>(prefabEntity);   // 여기서 터집니다
    InvalidOperationException: Entity doesn't belong to the current authoring component

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

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

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

    목표 수를 유지하는 스폰

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

    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개입니다. 실행하고, 쿼리로 개수를 세 보고, 화면을 봐야 알 수 있습니다. 이 부류가 이 프로젝트 내내 반복됩니다.