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