Giriş: Neden EntityQuery?
Unity’nin Veri Odaklı Teknoloji Yığını (DOTS) ile oyun geliştirirken, bileşen tabanlı varlıklarınız (entities) arasında belirli kriterlere uyanları bulmak ve onlarla etkileşim kurmak temel bir görevdir. İşte tam da bu noktada EntityQuery kullanımı devreye girer. `EntityQuery`, DOTS ekosisteminde binlerce, hatta milyonlarca varlık arasından belirli bileşen kombinasyonlarına sahip olanları verimli bir şekilde sorgulamanızı sağlayan güçlü bir araçtır. Geleneksel GameObject tabanlı yaklaşımlarda tüm oyun objelerini döngüye alıp bileşenlerini kontrol etmek performans sorunlarına yol açabilirken, `EntityQuery` bu işlemi optimize edilmiş ve paralel çalışmaya uygun bir biçimde gerçekleştirir.
Bu makalede, `EntityQuery`’nin ne olduğunu, nasıl oluşturulup kullanılacağını, ileri seviye sorgulama tekniklerini, yaygın hataları ve performans ipuçlarını detaylı bir şekilde inceleyeceğiz. Amacımız, Unity DOTS projelerinizde `EntityQuery`’yi en etkili şekilde kullanarak daha hızlı ve ölçeklenebilir oyunlar geliştirmenize yardımcı olmaktır.
EntityQuery Nedir? Temel Kavramlar
Bir `EntityQuery`, belirli bir bileşen kümesine (component set) sahip tüm varlıkları tanımlayan bir filtredir. Örneğin, ‘konumu olan ve hareket edebilen tüm varlıkları bul’ gibi bir sorgu oluşturabilirsiniz. Bu sorgular, `SystemBase` sınıfından türetilmiş sistemlerinizde (systems) varlıkları işlemeye başlamadan önce hangi varlıkların üzerinde çalışılacağını belirlemek için kullanılır.
Sorgu Oluşturma: WithAll, WithAny, WithNone
Bir `EntityQuery` oluşturmanın temel yolu, `EntityManager` veya `SystemBase` içindeki `GetEntityQuery` metodunu kullanmaktır. Bu metod, sorgunuzun hangi bileşenleri içermesi, hangilerini içermemesi gerektiğini belirten `ComponentType` listeleri alır:
- `WithAll`: Sorguya uyan varlıkların bu bileşenlerin tümüne sahip olması gerekir.
- `WithAny`: Sorguya uyan varlıkların bu bileşenlerden herhangi birine sahip olması gerekir.
- `WithNone`: Sorguya uyan varlıkların bu bileşenlerin hiçbirine sahip olmaması gerekir.
Ayrıca, bileşen tiplerini belirtirken `ComponentType.ReadOnly` veya `ComponentType.ReadWrite` kullanmak önemlidir. `ReadOnly` performans avantajı sağlar ve veri yarışlarını önlemeye yardımcı olur, çünkü sistemin bu bileşeni sadece okuyacağını, değiştirmeyeceğini garanti eder.
using Unity.Entities;
using Unity.Mathematics;
public struct Position : IComponentData { public float3 Value; }
public struct Rotation : IComponentData { public quaternion Value; }
public struct Speed : IComponentData { public float Value; }
public partial class MovementSystem : SystemBase
{
private EntityQuery _movementQuery;
protected override void OnCreate()
{
// Konum, Dönüş ve Hız bileşenlerine sahip tüm varlıkları sorgula
_movementQuery = GetEntityQuery(
ComponentType.ReadWrite(),
ComponentType.ReadOnly(),
ComponentType.ReadOnly()
);
// Alternatif olarak EntityQueryDesc kullanılarak:
// var queryDescription = new EntityQueryDesc
// {
// All = new ComponentType[] { typeof(Position), typeof(Rotation), typeof(Speed) }
// };
// _movementQuery = GetEntityQuery(queryDescription);
}
protected override void OnUpdate()
{
// Daha sonra bu sorguyu kullanarak varlıkları işleyeceğiz.
}
}
İleri Seviye EntityQuery Kullanımı
Temel sorguların ötesinde, `EntityQuery` daha karmaşık senaryolar için esneklik sunar.
EntityQueryDesc ve EntityQueryBuilder
`EntityQueryDesc`, sorgu tanımınızı daha yapılandırılmış bir şekilde ifade etmenizi sağlar. Özellikle `All`, `Any`, `None` ve `Options` gibi birden fazla filtreleme kuralını aynı anda uygulamak istediğinizde kullanışlıdır. `EntityQueryBuilder` ise daha akıcı (fluent) bir API sunarak zincirleme metot çağrılarıyla sorgular oluşturmanıza olanak tanır. Bu, özellikle karmaşık sorguları daha okunaklı hale getirir.
// EntityQueryBuilder örneği
_movementQuery = new EntityQueryBuilder(Allocator.Temp)
.WithAll<position, rotation,="" speed="">()
.WithNone() // Disabled bileşeni olmayanları seç
.Build(this);
</position,>
Sorgu Sonuçlarını İşleme: ForEach ve IJobChunk
Bir `EntityQuery` oluşturulduktan sonra, sorguya uyan varlıkları işlemek için iki ana yöntem vardır: `ForEach` ve `IJobChunk`.
ForEach ile Sorgu İşleme
`ForEach`, küçük ve orta ölçekli veri kümeleri için okunabilirliği yüksek ve kullanımı kolay bir yöntemdir. Sistem içindeki varlıkların bileşenlerine doğrudan erişim sağlar.
protected override void OnUpdate()
{
float deltaTime = SystemAPI.Time.DeltaTime;
// EntityQuery kullanımı ile varlıkları işleme
Entities.WithStoreEntityQueryInField(ref _movementQuery)
.ForEach((ref Position position, in Rotation rotation, in Speed speed) =>
{
position.Value += math.forward(rotation.Value) * speed.Value * deltaTime;
}).ScheduleParallel(); // Paralel çalıştırma
}
IJobChunk ile Sorgu İşleme
`IJobChunk`, büyük veri kümeleri ve yüksek performans gerektiren durumlar için idealdir. Varlıkları tek tek değil, veri blokları (chunks) halinde işler. Bu, bellek önbelleği (cache locality) açısından daha verimli olabilir ve daha karmaşık paralel işleme senaryolarına olanak tanır.
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
[BurstCompile]
public partial struct MovementJob : IJobChunk
{
public float DeltaTime;
public ComponentTypeHandle PositionHandle;
[ReadOnly] public ComponentTypeHandle RotationHandle;
[ReadOnly] public ComponentTypeHandle SpeedHandle;
public void Execute(in ArchetypeChunk chunk, int chunkIndex, bool use = false, int entityOffset = 0)
{
NativeArray positions = chunk.Get NativeArray(ref PositionHandle);
NativeArray rotations = chunk.Get NativeArray(ref RotationHandle);
NativeArray speeds = chunk.Get NativeArray(ref SpeedHandle);
for (int i = 0; i < chunk.Count;
i++)
{
Position position = positions[i];
Rotation rotation = rotations[i];
Speed speed = speeds[i];
position.Value += math.forward(rotation.Value) * speed.Value * DeltaTime;
positions[i] = position;
}
}
}
public partial class MovementSystemWithJob : SystemBase
{
private EntityQuery _movementQuery;
protected override void OnCreate()
{
_movementQuery = GetEntityQuery(
ComponentType.ReadWrite(),
ComponentType.ReadOnly(),
ComponentType.ReadOnly()
);
}
protected override void OnUpdate()
{
var job = new MovementJob
{
DeltaTime = SystemAPI.Time.DeltaTime,
PositionHandle = GetComponentTypeHandle(),
RotationHandle = GetComponentTypeHandle(true),
SpeedHandle = GetComponentTypeHandle(true)
};
Dependency = job.Schedule(_movementQuery, Dependency);
}
}
EntityQuery İçin Pratik İpuçları
İpucu 1: Sorguları Önbelleğe Alın (Caching)
`EntityQuery` oluşturmak nispeten maliyetli bir işlemdir. Bu nedenle, sorgularınızı `OnCreate` metodunda oluşturup bir alanda (field) önbelleğe almak ve `OnUpdate` içinde tekrar tekrar oluşturmaktan kaçınmak performansı önemli ölçüde artırır. Yukarıdaki örneklerde bu yaklaşım gösterilmiştir.
İpucu 2: `ComponentType.ReadOnly` Kullanımının Önemi
Bir bileşeni sadece okuyorsanız, mutlaka `ComponentType.ReadOnly` kullanın. Bu, DOTS’un veri bağımlılıklarını daha iyi yönetmesini ve işleri (jobs) daha güvenli ve paralel bir şekilde planlamasını sağlar. Gereksiz yere `ReadWrite` kullanmak, paralelleştirme fırsatlarını azaltabilir ve performans düşüşüne neden olabilir. EntityQuery kullanımı sırasında bu detaya dikkat etmek çok önemlidir.
İpucu 3: Sorgularınızı Detaylandırın
Sorgularınızı mümkün olduğunca spesifik tutmaya çalışın. Gereksiz yere çok sayıda varlığı sorgulamak yerine, `WithNone` veya `WithAll` gibi filtreleri kullanarak sadece ihtiyacınız olan varlıkları hedefleyin. Bu, sorgu sonuçlarının daha küçük olmasını ve dolayısıyla işleme süresinin kısalmasını sağlar.
İpucu 4: Singleton Bileşenleri Yönetme
Eğer tekil (singleton) bir bileşene ihtiyacınız varsa (örneğin, oyun ayarları veya zamanlayıcı), `RequireSingletonForUpdate()` metodunu `OnUpdate` içinde kullanabilirsiniz. Bu, eğer belirtilen bileşenden sahnede hiç yoksa veya birden fazla varsa sistemin çalışmamasını sağlar, böylece hatalı durumları önlersiniz. `EntityQuery`’nizin bu tür bileşenleri de doğru şekilde içermesi önemlidir.
Yaygın Hatalar ve Çözümleri
Hata 1: Sorguları Her Kare Yeniden Oluşturmak
Hata: `OnUpdate` metodunda her kare `GetEntityQuery` çağırmak.
Çözüm: Sorguyu `OnCreate` içinde oluşturup bir alanda saklayın ve `OnUpdate` içinde bu önbelleğe alınmış sorguyu kullanın.
Hata 2: Çok Geniş Sorgular Yazmak
Hata: Sadece `WithAll` kullanarak çok sayıda bileşeni sorguya dahil etmek, ancak aslında bazı varlıkların bu bileşenlerin hepsine sahip olmaması gerektiğini fark etmemek.
Çözüm: `WithNone` veya `WithAny` gibi filtreleri kullanarak sorgunuzu daraltın. Örneğin, `Disabled` bileşeni olmayan varlıkları sorgulamak için `WithNone` kullanın.
Hata 3: `ReadOnly` Bileşenleri Yazmaya Çalışmak
Hata: Bir `EntityQuery`’de `ComponentType.ReadOnly` olarak belirtilen bir bileşeni işlerken değiştirmeye çalışmak. Bu, çalışma zamanı hatalarına veya beklenmedik davranışlara yol açabilir.
Çözüm: Eğer bir bileşeni değiştirecekseniz, sorgunuzda onu `ComponentType.ReadWrite` olarak belirtin. Eğer mümkünse, değiştireceğiniz bileşenleri ayrı bir sorguda veya ayrı bir `ForEach` bloğunda işleyin.
Performans ve Optimizasyon Notları
IJobChunk vs ForEach
Genel olarak, çok sayıda varlığı işlerken `IJobChunk` kullanımı, `ForEach`’e göre daha iyi performans gösterir. Bunun nedeni `IJobChunk`’un veri blokları üzerinde çalışarak bellek önbellek (cache) isabet oranını artırması ve daha karmaşık paralelleştirme tekniklerine izin vermesidir. Ancak, küçük varlık setleri veya çok basit işlemler için `ForEach`’in okunabilirliği ve kullanım kolaylığı tercih edilebilir. Performans kritik senaryolarda EntityQuery kullanımı ile `IJobChunk` tercih edilmelidir.
Burst Compiler ve Cache Locality
DOTS sistemleri ve işleri `Burst` derleyicisi ile derlendiğinde inanılmaz hızlara ulaşabilir. `EntityQuery`’nin `IJobChunk` ile birlikte kullanılması, verilerin bellek içinde ardışık (contiguous) düzenlenmesini sağlayarak `Burst`’ün CPU önbelleğini daha verimli kullanmasına olanak tanır. Bu da performansın katlanarak artmasına yol açar.
EntityManager İşlemlerinden Kaçınma
İşler (jobs) veya `ForEach` döngüleri içinde `EntityManager` üzerinden varlık oluşturma, silme veya bileşen ekleme/çıkarma gibi pahalı işlemlerden mümkün olduğunca kaçının. Bu tür işlemler genellikle ana iş parçacığında (main thread) yapılmalı veya `EntityCommandBuffer` kullanılarak ertelenmelidir. `EntityCommandBuffer`, işler içinde güvenli bir şekilde bu tür değişiklikleri planlamanıza ve daha sonra ana iş parçacığında tek bir seferde uygulamanıza olanak tanır.
Sonuç
Unity DOTS ekosisteminde `EntityQuery`, varlıkları verimli bir şekilde sorgulamak ve işlemek için vazgeçilmez bir araçtır. Doğru EntityQuery kullanımı, oyunlarınızın performansını ve ölçeklenebilirliğini önemli ölçüde artırabilir. Temel sorgu oluşturmadan `IJobChunk` ile ileri seviye işlemeye kadar, bu makalede öğrendiğiniz teknikleri uygulayarak DOTS projelerinizde daha sağlam ve hızlı sistemler geliştirebilirsiniz. Unutmayın, sorgularınızı önbelleğe almak, `ReadOnly` kullanmak ve spesifik filtreler uygulamak, performans kazançlarının anahtarıdır.




