콘텐츠로 이동

Unity DOTS ECS EntityQuery 심화 가이드

Unity DOTS(Data-Oriented Technology Stack)의 ECS(Entity Component System)에서 EntityQuery는 원하는 컴포넌트 조합을 가진 엔티티를 효율적으로 찾는 메커니즘입니다. 캐시 친화적인 데이터 레이아웃으로 CPU 성능을 극대화합니다.

패키지: com.unity.entities


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;
}

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);
}
}

// 잡(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);
}
}

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;
}

아키타입 청크에 직접 접근하여 더 세밀한 제어가 가능합니다.

[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);
}
}
}

컴포넌트를 제거/추가하지 않고 활성화/비활성화합니다.

// 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); // 해제

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);
}
}

방식장점단점
SystemAPI.Query직관적, 타입 안전제한적 캡처
Entities.ForEach강력, 람다 캡처Deprecated 예정
IJobEntity재사용 가능, Burst 최적별도 구조체 필요
IJobChunk최대 제어복잡한 API

  • SystemAPI.Query<>() — 가장 간단한 반복
  • IJobEntity + [BurstCompile] — 병렬 처리 최적
  • WithChangeFilter<T>() — 변경된 컴포넌트만 처리
  • IEnableableComponent — 컴포넌트 토글 (구조 변경 없음)
  • EntityCommandBuffer — 구조 변경 명령 지연 실행