Unity DOTS ECS EntityQuery 심화 가이드
Unity DOTS(Data-Oriented Technology Stack)의 ECS(Entity Component System)에서 EntityQuery는 원하는 컴포넌트 조합을 가진 엔티티를 효율적으로 찾는 메커니즘입니다. 캐시 친화적인 데이터 레이아웃으로 CPU 성능을 극대화합니다.
패키지: com.unity.entities
1. 기본 EntityQuery
섹션 제목: “1. 기본 EntityQuery”using Unity.Entities;using Unity.Mathematics;using Unity.Transforms;
public partial class MovementSystem : SystemBase{ protected override void OnUpdate() { float deltaTime = SystemAPI.Time.DeltaTime;
// Foreach로 간단한 쿼리 (내부적으로 EntityQuery 생성) foreach (var (transform, velocity) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<Velocity>>()) { transform.ValueRW = transform.ValueRO.Translate( velocity.ValueRO.Value * deltaTime); } }}
// 컴포넌트 정의public struct Velocity : IComponentData{ public float3 Value;}2. EntityQuery 명시적 구성
섹션 제목: “2. EntityQuery 명시적 구성”public partial class FilteredSystem : SystemBase{ private EntityQuery _query;
protected override void OnCreate() { // 명시적 쿼리 빌드 _query = new EntityQueryBuilder(Allocator.Temp) .WithAll<Position, Velocity>() // 반드시 포함 .WithAny<Enemy, Player>() // 하나 이상 포함 .WithNone<Disabled, Dead>() // 반드시 미포함 .WithChangeFilter<Velocity>() // 변경된 것만 .Build(this); }
protected override void OnUpdate() { // 쿼리로 엔티티 수 확인 int count = _query.CalculateEntityCount();
// 쿼리로 배열 가져오기 var entities = _query.ToEntityArray(Allocator.Temp); var positions = _query.ToComponentDataArray<Position>(Allocator.Temp); }}3. IJobEntity — 병렬 처리
섹션 제목: “3. IJobEntity — 병렬 처리”// 잡(Job) 구조체 정의[BurstCompile]public partial struct MoveJob : IJobEntity{ public float DeltaTime;
// 엔티티가 처리될 때 자동 호출 void Execute(ref LocalTransform transform, in Velocity velocity) { transform = transform.Translate(velocity.Value * DeltaTime); }}
// 시스템에서 잡 스케줄링[BurstCompile]public partial struct MovementSystem : ISystem{ [BurstCompile] public void OnUpdate(ref SystemState state) { var job = new MoveJob { DeltaTime = SystemAPI.Time.DeltaTime };
// 메인 스레드에서 실행 // job.Run();
// 단일 스레드 잡으로 스케줄 // state.Dependency = job.Schedule(state.Dependency);
// 병렬 잡으로 스케줄 (권장) state.Dependency = job.ScheduleParallel(state.Dependency); }}4. 변경 감지 (Change Filtering)
섹션 제목: “4. 변경 감지 (Change Filtering)”public partial class HealthBarSystem : SystemBase{ protected override void OnUpdate() { // Health 컴포넌트가 변경된 엔티티만 처리 foreach (var (health, healthBar) in SystemAPI.Query<RefRO<Health>, RefRW<HealthBarUI>>() .WithChangeFilter<Health>()) { healthBar.ValueRW.CurrentHP = health.ValueRO.Current; healthBar.ValueRW.MaxHP = health.ValueRO.Max; healthBar.ValueRW.NeedsUpdate = true; } }}
public struct Health : IComponentData{ public float Current; public float Max;}5. 청크 반복 (Chunk Iteration)
섹션 제목: “5. 청크 반복 (Chunk Iteration)”아키타입 청크에 직접 접근하여 더 세밀한 제어가 가능합니다.
[BurstCompile]public partial struct ChunkIterationJob : IJobChunk{ public ComponentTypeHandle<LocalTransform> TransformHandle; [ReadOnly] public ComponentTypeHandle<Velocity> VelocityHandle; public float DeltaTime;
[BurstCompile] public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) { // 청크에서 컴포넌트 배열 가져오기 var transforms = chunk.GetNativeArray(ref TransformHandle); var velocities = chunk.GetNativeArray(ref VelocityHandle);
for (int i = 0; i < chunk.Count; i++) { transforms[i] = transforms[i].Translate(velocities[i].Value * DeltaTime); } }}6. Enableable Components
섹션 제목: “6. Enableable Components”컴포넌트를 제거/추가하지 않고 활성화/비활성화합니다.
// IEnableableComponent 인터페이스로 활성화 가능한 컴포넌트public struct Frozen : IComponentData, IEnableableComponent { }
// 비활성화된 경우 쿼리에서 제외foreach (var transform in SystemAPI.Query<RefRW<LocalTransform>>() .WithDisabled<Frozen>()) // Frozen이 비활성화된 것만{ // 이동 처리}
// 활성화/비활성화SystemAPI.SetComponentEnabled<Frozen>(entity, true); // 동결SystemAPI.SetComponentEnabled<Frozen>(entity, false); // 해제7. 엔티티 생성과 커맨드 버퍼
섹션 제목: “7. 엔티티 생성과 커맨드 버퍼”public partial class SpawnSystem : SystemBase{ private BeginSimulationEntityCommandBufferSystem _ecbSystem;
protected override void OnCreate() { _ecbSystem = World.GetOrCreateSystemManaged <BeginSimulationEntityCommandBufferSystem>(); }
protected override void OnUpdate() { var ecb = _ecbSystem.CreateCommandBuffer().AsParallelWriter();
Entities .WithAll<SpawnRequest>() .ForEach((Entity entity, int entityInQueryIndex, in SpawnRequest request) => { // 새 엔티티 생성 명령 var newEntity = ecb.CreateEntity(entityInQueryIndex); ecb.AddComponent(entityInQueryIndex, newEntity, new LocalTransform { Position = request.Position });
// 스폰 요청 엔티티 삭제 ecb.DestroyEntity(entityInQueryIndex, entity); }) .ScheduleParallel();
_ecbSystem.AddJobHandleForProducer(Dependency); }}8. SystemAPI.Query vs Entities.ForEach
섹션 제목: “8. SystemAPI.Query vs Entities.ForEach”| 방식 | 장점 | 단점 |
|---|---|---|
SystemAPI.Query | 직관적, 타입 안전 | 제한적 캡처 |
Entities.ForEach | 강력, 람다 캡처 | Deprecated 예정 |
IJobEntity | 재사용 가능, Burst 최적 | 별도 구조체 필요 |
IJobChunk | 최대 제어 | 복잡한 API |
SystemAPI.Query<>()— 가장 간단한 반복IJobEntity+[BurstCompile]— 병렬 처리 최적WithChangeFilter<T>()— 변경된 컴포넌트만 처리IEnableableComponent— 컴포넌트 토글 (구조 변경 없음)EntityCommandBuffer— 구조 변경 명령 지연 실행