EntityQuery 정리
ECS(Entity Component System)에서 특정 조건을 만족하는 Entity들을 효율적으로 검색하는 도구
Entity가 가진 컴포넌트 조합을 기준으로 검색하며, 이를 통해 메모리 캐시 효율을 극대화
1. EntityQuery의 기본 개념
- Entity 필터링: 특정한 컴포넌트를 포함하거나 제외하는 기준을 정할 수 있다.
- Chunk 단위 검색:
EntityQuery
는 Entity 개별 조회가 아닌 Chunk 단위(Archetype Chunk)로 데이터를 가져옴 - 즉시 검색 or
Job
시스템과 병렬 처리 가능
– 특정 컴포넌트를 가진 Entity 찾기
public partial struct FindEntitiesSystem : ISystem { public void OnUpdate(ref SystemState state) { // PositionComponent와 VelocityComponent를 가진 Entity 찾기 EntityQuery query = SystemAPI.QueryBuilder() .WithAll<PositionComponent, VelocityComponent>() .Build(); foreach (var (position, velocity) in SystemAPI.Query<RefRW<PositionComponent>, RefRO<VelocityComponent>>()) { position.ValueRW.Value += velocity.ValueRO.Value * SystemAPI.Time.DeltaTime; } } }
- WithAll() → 모든 엔티티 중 PositionComponent와 VelocityComponent를 가진 것만 검색
- RefRW → 읽기/쓰기 가능
- RefRO → 읽기 전용
2. EntityQuery의 주요 조건
조건 | 설명 |
---|---|
WithAll<T>() | 해당 컴포넌트를 모두 가진 Entity만 검색 |
WithAny<T>() | 지정한 컴포넌트 중 하나라도 가진 Entity 검색 |
WithNone<T>() | 특정한 컴포넌트를 가지지 않은 Entity 검색 |
WithDisabled<T>() | 비활성화된 컴포넌트가 있는 Entity 검색 |
WithAbsent<T>() | 특정한 컴포넌트가 없는 Entity 검색 |
– 특정 조건의 엔티티 찾기
EntityQuery query = SystemAPI.QueryBuilder() .WithAll<PositionComponent>() // PositionComponent가 있어야 함 .WithAny<VelocityComponent, AccelerationComponent>() // 둘 중 하나만 있으면 됨 .WithNone<DestroyedTag>() // DestroyedTag가 있으면 제외 .Build();
- WithAll() → 반드시 포함
- WithAny() → 하나라도 포함
- WithNone() → 포함하면 안 됨
3. EntityQuery를 사용한 데이터 접근 방법
(1) SystemAPI.Query() 사용
EntityQuery
를 생성하고foreach
루프에서 엔티티 데이터를 순회 가능
foreach (var (position, velocity) in SystemAPI.Query<RefRW<PositionComponent>, RefRO<VelocityComponent>>()) { position.ValueRW.Value += velocity.ValueRO.Value * SystemAPI.Time.DeltaTime; }
(2) ToEntityArray()로 Entity 리스트 가져오기
ToEntityArray()
를 사용하면 해당EntityQuery
에 해당하는 Entity 리스트를 가져옴
var entities = query.ToEntityArray(Allocator.Temp); foreach (var entity in entities) { Debug.Log($"찾은 Entity: {entity}"); } entities.Dispose();
ToEntityArray(Allocator.Temp)
→ NativeArray로 Entity를 가져옴Dispose()
필수! 메모리 해제 안 하면 메모리 누수 발생
(3) ToComponentDataArray()로 특정 컴포넌트 리스트 가져오기
- 특정 컴포넌트의 값만 가져올 수도 있음.
var positions = query.ToComponentDataArray<PositionComponent>(Allocator.Temp); foreach (var position in positions) { Debug.Log($"Position: {position.Value}"); } positions.Dispose();
- 데이터만 필요한 경우 성능 최적화 가능
4. IJobChunk과 함께 사용하기
IJobChunk
를 사용하면EntityQuery
를 멀티스레드로 병렬 처리 가능-
ArchetypeChunk
을 사용하여 Chunk 단위 데이터 접근 가능
[BurstCompile] public struct MoveJob : IJobChunk { public ComponentTypeHandle<PositionComponent> PositionHandle; [ReadOnly] public ComponentTypeHandle<VelocityComponent> VelocityHandle; public float DeltaTime; public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { var positions = chunk.GetNativeArray(PositionHandle); var velocities = chunk.GetNativeArray(VelocityHandle); for (int i = 0; i < chunk.Count; i++) { positions[i] = new PositionComponent { Value = positions[i].Value + velocities[i].Value * DeltaTime }; } } } public partial struct MoveSystem : ISystem { public void OnUpdate(ref SystemState state) { var query = SystemAPI.QueryBuilder() .WithAll<PositionComponent, VelocityComponent>() .Build(); var positionHandle = SystemAPI.GetComponentTypeHandle<PositionComponent>(); var velocityHandle = SystemAPI.GetComponentTypeHandle<VelocityComponent>(isReadOnly: true); var job = new MoveJob { PositionHandle = positionHandle, VelocityHandle = velocityHandle, DeltaTime = SystemAPI.Time.DeltaTime }; // query를 기반으로 Schedule()을 호출 state.Dependency = job.Schedule(query, state.Dependency); } }
GetComponentTypeHandle<T>()
사용 → 컴포넌트 데이터를 가져올 때 캐시 최적화GetComponentTypeHandle<T>()
은 ECS(Worlds)의 청크 단위 데이터 접근을 최적화하기 위한 APIIJobChunk
으로 멀티스레드 병렬 처리 가능
job.Schedule(query, state.Dependency)
의 실행 과정
query
를 사용하여 IJobChunk
가 처리할 청크들을 찾음
query
는PositionComponent
와VelocityComponent
를 포함하는 엔티티들의 청크 목록을 가져옴.IJobChunk
는 이 청크 목록을 기반으로 실행됨.
job.Schedule()
을 호출하여 MoveJob
을 실행
query
에서 반환된 청크를MoveJob
에서 병렬적으로 처리.- 내부적으로 각 청크별로
MoveJob.Execute()
가 실행됨.
state.Dependency
를 업데이트하여 잡 실행을 관리
state.Dependency
는 현재 시스템의 의존성 체인을 관리.- 이전 프레임에서 실행된 다른 잡들과의 의존 관계를 고려하여 동기화함.
- 병렬 처리를 안전하게 실행하기 위해 필요함.
매개변수 | 타입 | 설명 |
---|---|---|
chunk | ArchetypeChunk | 현재 처리 중인 Chunk 객체. 해당 Chunk 내의 Entity 및 컴포넌트에 접근할 수 있음. |
chunkIndex | int | 현재 Chunk 의 인덱스 (0부터 시작). 병렬 작업을 할 때 Chunk 가 몇 번째인지 확인 가능. |
firstEntityIndex | int | 현재 Chunk 에서 첫 번째 Entity 의 전역 인덱스. (전체 Entity 중 몇 번째인지 나타냄) |
(Q) ComponentTypeHandle을 사용하여 NativeArray를 생성하는 것과 미리 NativeArray를 넘겨주는 것의 차이점
(1) ComponentTypeHandle<T>
을 사용하는 이유
GetComponentTypeHandle<T>()
을 사용하여 IJobChunk
내부에서 NativeArray
를 가져오는 이유는 Chunk 단위 접근 최적화 때문이다.
ComponentTypeHandle<T>
을 사용하면 각 Chunk에 맞는NativeArray
를 개별적으로 가져올 수 있음- 만약
NativeArray
를 미리 만들어서 넘겨준다면, Chunk별로 나눠진 데이터가 하나의 배열에 병합되어야 하므로 메모리 효율이 떨어지고 캐시 활용도가 낮아짐
(2) ComponentTypeHandle<T>
과 NativeArray<T>
차이점 비교
방식 | 설명 | 장점 | 단점 |
---|---|---|---|
ComponentTypeHandle<T> 사용 ( IJobChunk 내부에서 GetNativeArray() ) | 각 청크에서 해당 컴포넌트의 데이터 배열을 가져옴 | – 청크별로 최적화된 데이터 접근 가능 – 캐시 친화적이며 성능 최적화 가능 – Burst 및 멀티스레딩에 적합 | – IJobChunk 을 사용해야 함 |
NativeArray<T> 를 미리 만들어 Job에 넘김 | 전체 데이터를 하나의 NativeArray 로 만들고 Job에서 사용 | – 간단한 구조, 직관적 – 작은 데이터셋에서는 부담이 적음 | – 전체 데이터를 하나의 배열로 만들기 때문에 캐시 미스 발생 가능성 증가 – Chunk별 최적화가 불가능하여 성능 저하 가능성 있음 |
(3) ComponentTypeHandle<T>
방식
1> SystemAPI.GetComponentTypeHandle<T>()
실행
var positionHandle = SystemAPI.GetComponentTypeHandle<PositionComponent>(); var velocityHandle = SystemAPI.GetComponentTypeHandle<VelocityComponent>(isReadOnly: true);
ComponentTypeHandle<T>
을 가져옴- Chunk별로 데이터가 분산되어 있기 때문에
NativeArray<T>
로 직접 변환하지 않음
2> MoveJob.Schedule()
실행
var job = new MoveJob { PositionHandle = positionHandle, VelocityHandle = velocityHandle, DeltaTime = SystemAPI.Time.DeltaTime }; state.Dependency = job.Schedule(query, state.Dependency);
MoveJob
을 스케줄할 때ComponentTypeHandle<T>
을 넘김
3> MoveJob.Execute()
내부에서 GetNativeArray()
실행
var positions = chunk.GetNativeArray(PositionHandle); var velocities = chunk.GetNativeArray(VelocityHandle);
- 각 Chunk에서
NativeArray<T>
를 가져와서 사용함. - 이렇게 하면 각 Chunk가 독립적으로 자신의 데이터에 접근할 수 있으며, 캐시 최적화 및 멀티스레딩 효율 증가
(4) NativeArray
Job 방식
만약 NativeArray<T>
를 미리 만들어서 Job에 넘기면, 멀티스레딩 최적화가 어렵고 성능 저하 가능성 증가.
전체 데이터를 하나의 NativeArray
로 병합하는 과정이 필요, Chunk별 데이터 레이아웃이 깨질 수 있음 → CPU 캐시 효율 저하.
NativeArray<PositionComponent> positions = new NativeArray<PositionComponent>(entityCount, Allocator.TempJob); NativeArray<VelocityComponent> velocities = new NativeArray<VelocityComponent>(entityCount, Allocator.TempJob); // Job에 NativeArray를 직접 넘김 var job = new MoveJob { Positions = positions, Velocities = velocities, DeltaTime = SystemAPI.Time.DeltaTime }; state.Dependency = job.Schedule(state.Dependency);
이 경우, positions
와 velocities
를 Chunk별로 나누지 않고 전체 데이터를 하나의 배열로 만듦.
Chunk별 데이터가 섞일 가능성이 높아져 캐시 미스(Cache Miss)가 증가할 수 있음
5. EntityQuery의 추가 기능
(1) CalculateEntityCount()로 개수 확인
int entityCount = query.CalculateEntityCount(); Debug.Log($"현재 쿼리 결과 개수: {entityCount}");
(2) SetChangedVersionFilter()로 변경된 컴포넌트만 검색
query.SetChangedVersionFilter<PositionComponent>();
최근 변경된 PositionComponent
를 가진 엔티티만 검색
(3) Enable/Disable로 쿼리 활성화/비활성화
query.SetEnabled(false); // 쿼리 비활성화 query.SetEnabled(true); // 다시 활성화
쿼리를 특정 조건에서만 사용하고 싶을 때 활용