refactor: replace QueryBuilder with generic Query and Select iterators
Replace the old `QueryBuilder` and `ForEach` callback pattern with a more performant, zero-allocation `Select` API using `ref struct` iterators. This change also updates the singleton implementation to use dedicated entities per component type rather than a single reserved entity. - Replace `QueryBuilder`/`QueryDescriptor` with generic `Query<T1..T6>` - Replace `ForEach` callbacks with `world.Select<T1..T6>()` iterators - Replace `EntityIterator` with `WorldQueryExtensions` - Update singleton logic to allocate one entity per component type - Add `FindEntity<T>` and `FindEntities<T>` lookup methods
This commit is contained in:
+73
-125
@@ -37,9 +37,6 @@ namespace OECS;
|
||||
|
||||
public class World : IDisposable
|
||||
{
|
||||
// --- Singleton entity ---
|
||||
public static Entity SingletonEntity { get; } // ID=1, Version=1
|
||||
|
||||
// --- Lifecycle ---
|
||||
public World();
|
||||
|
||||
@@ -64,24 +61,7 @@ public class World : IDisposable
|
||||
public void RemoveSingleton<T>() where T : struct;
|
||||
|
||||
// --- Queries ---
|
||||
public QueryBuilder Query();
|
||||
|
||||
// --- Query Execution ---
|
||||
public void ForEach<T1>(
|
||||
QueryDescriptor query,
|
||||
ForEachAction<T1> action) where T1 : struct;
|
||||
|
||||
// Overloads for 2–6 component types:
|
||||
public void ForEach<T1, T2>(QueryDescriptor, ForEachAction<T1, T2>)
|
||||
where T1 : struct where T2 : struct;
|
||||
public void ForEach<T1, T2, T3>(QueryDescriptor, ForEachAction<T1, T2, T3>)
|
||||
where T1 : struct where T2 : struct where T3 : struct;
|
||||
public void ForEach<T1, T2, T3, T4>(QueryDescriptor, ForEachAction<T1, T2, T3, T4>)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct;
|
||||
public void ForEach<T1, T2, T3, T4, T5>(QueryDescriptor, ForEachAction<T1, T2, T3, T4, T5>)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct;
|
||||
public void ForEach<T1, T2, T3, T4, T5, T6>(QueryDescriptor, ForEachAction<T1, T2, T3, T4, T5, T6>)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct;
|
||||
// See QueryExtensions for Select / FindEntity / FindEntities.
|
||||
|
||||
// --- Commands ---
|
||||
public CommandQueue Commands { get; }
|
||||
@@ -94,7 +74,10 @@ public class World : IDisposable
|
||||
public Observable<EntityChange> ObserveEntityChanges();
|
||||
public Observable<EntityChange> ObserveComponentChanges<T>()
|
||||
where T : struct;
|
||||
public Observable<EntityChange> ObserveQuery(QueryDescriptor query);
|
||||
public Observable<EntityChange> ObserveQuery<T1>(Query<T1> query = default)
|
||||
where T1 : struct;
|
||||
public Observable<EntityChange> ObserveQuery<T1, T2>(Query<T1, T2> query = default)
|
||||
where T1 : struct where T2 : struct;
|
||||
|
||||
// --- Relationships ---
|
||||
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
|
||||
@@ -107,121 +90,97 @@ public class World : IDisposable
|
||||
|
||||
---
|
||||
|
||||
## ForEachAction Delegates
|
||||
## WorldQueryExtensions
|
||||
|
||||
Custom delegate types for query iteration with `ref` parameters. The built-in
|
||||
`Action<...>` delegates do not support `ref` parameters.
|
||||
Extension methods on `World` providing `foreach`-compatible iteration and
|
||||
entity lookup. Select returns `ref struct` enumerators for zero-allocation
|
||||
iteration with `ref` access to components.
|
||||
|
||||
```csharp
|
||||
namespace OECS;
|
||||
|
||||
public delegate void ForEachAction<T1>(Entity entity, ref T1 c1) where T1 : struct;
|
||||
|
||||
public delegate void ForEachAction<T1, T2>(Entity entity, ref T1 c1, ref T2 c2)
|
||||
where T1 : struct where T2 : struct;
|
||||
|
||||
public delegate void ForEachAction<T1, T2, T3>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3)
|
||||
where T1 : struct where T2 : struct where T3 : struct;
|
||||
|
||||
public delegate void ForEachAction<T1, T2, T3, T4>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct;
|
||||
|
||||
public delegate void ForEachAction<T1, T2, T3, T4, T5>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4, ref T5 c5)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct;
|
||||
|
||||
public delegate void ForEachAction<T1, T2, T3, T4, T5, T6>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4, ref T5 c5, ref T6 c6)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EntityIterator
|
||||
|
||||
Extension methods on `World` for `foreach`-style iteration over queries.
|
||||
Returns `ref struct` iterators that support zero-allocation iteration with
|
||||
`ref` access to components.
|
||||
|
||||
Two sets of overloads: one accepting an explicit `QueryDescriptor` for
|
||||
filtering with `Without<T>`, and one without for simple "has component" scans.
|
||||
|
||||
```csharp
|
||||
namespace OECS;
|
||||
|
||||
public static class EntityIterator
|
||||
public static class WorldQueryExtensions
|
||||
{
|
||||
// With explicit QueryDescriptor (supports Without<T> filters).
|
||||
// Select — returns a ref struct enumerator for foreach.
|
||||
public static Select1<T1> Select<T1>(
|
||||
this World world, QueryDescriptor query) where T1 : struct;
|
||||
this World world, Query<T1> query = default) where T1 : struct;
|
||||
// Overloads for 2–6 component types:
|
||||
public static Select2<T1, T2> Select<T1, T2>(...) where T1 : struct where T2 : struct;
|
||||
// ... up to Select6<T1..T6>
|
||||
|
||||
public static Select2<T1, T2> Select<T1, T2>(
|
||||
this World world, QueryDescriptor query)
|
||||
where T1 : struct where T2 : struct;
|
||||
// FindEntity — returns the first matching entity or Entity.Null.
|
||||
public static Entity FindEntity<T1>(
|
||||
this World world, Query<T1> query = default) where T1 : struct;
|
||||
public static Entity FindEntity<T1, T2>(...) where T1 : struct where T2 : struct;
|
||||
public static Entity FindEntity<T1, T2, T3>(...) where T1 : struct where T2 : struct where T3 : struct;
|
||||
|
||||
public static Select3<T1, T2, T3> Select<T1, T2, T3>(
|
||||
this World world, QueryDescriptor query)
|
||||
where T1 : struct where T2 : struct where T3 : struct;
|
||||
|
||||
// Without QueryDescriptor — iterates all entities with the given component(s).
|
||||
public static Select1<T1> Select<T1>(
|
||||
this World world) where T1 : struct;
|
||||
|
||||
public static Select2<T1, T2> Select<T1, T2>(
|
||||
this World world)
|
||||
where T1 : struct where T2 : struct;
|
||||
|
||||
public static Select3<T1, T2, T3> Select<T1, T2, T3>(
|
||||
this World world)
|
||||
where T1 : struct where T2 : struct where T3 : struct;
|
||||
// FindEntities — returns all matching entities as List<Entity>.
|
||||
public static List<Entity> FindEntities<T1>(...) where T1 : struct;
|
||||
public static List<Entity> FindEntities<T1, T2>(...) where T1 : struct where T2 : struct;
|
||||
public static List<Entity> FindEntities<T1, T2, T3>(...) where T1 : struct where T2 : struct where T3 : struct;
|
||||
}
|
||||
```
|
||||
|
||||
The singletons `CurrentEntity`, `Current1`, `Current2`, `Current3` are
|
||||
exposed on the ref struct iterators directly. Usage:
|
||||
Each ref struct (Select1 through Select6) exposes:
|
||||
- `Entity Entity` — the current entity handle.
|
||||
- `ref T1 Item1`, `ref T2 Item2`, ... — read-write references to components.
|
||||
- `bool MoveNext()` — advances and returns true while items remain.
|
||||
- `GetEnumerator()` — returns `this` for `foreach` compatibility.
|
||||
- `Dispose()` — ends the iteration scope (flushes deferred mutations).
|
||||
|
||||
Usage:
|
||||
```csharp
|
||||
// Simple scan: all entities with PlayerHand.
|
||||
using var iter = world.Select<PlayerHand>();
|
||||
while (iter.MoveNext())
|
||||
// Simple scan: all entities with Position.
|
||||
foreach (var it in world.Select<Position>())
|
||||
{
|
||||
Console.WriteLine(iter.CurrentEntity);
|
||||
it.Item1.X += 1; // ref mutates component in-place
|
||||
}
|
||||
|
||||
// With query filter:
|
||||
var query = world.Query().With<Position>().Without<Frozen>().Build();
|
||||
using var iter2 = world.Select<Position>(query);
|
||||
while (iter2.MoveNext())
|
||||
// With Without filter:
|
||||
var query = new Query<Position>().Without<Frozen>();
|
||||
foreach (var it in world.Select(query))
|
||||
{
|
||||
iter2.Current1.X += 1;
|
||||
// it.Entity, it.Item1
|
||||
}
|
||||
|
||||
// Find first entity:
|
||||
var hand = world.FindEntity<PlayerHand>();
|
||||
|
||||
// Multi-component:
|
||||
foreach (var it in world.Select<Position, Velocity>())
|
||||
{
|
||||
it.Item1.X += it.Item2.X * dt;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## QueryBuilder
|
||||
## Query
|
||||
|
||||
Describes a query over the ECS world. The `With` types are encoded as generic
|
||||
parameters. Optional `Without` filters are added via the fluent API.
|
||||
|
||||
```csharp
|
||||
namespace OECS;
|
||||
|
||||
public class QueryBuilder
|
||||
public struct Query<T1> where T1 : struct
|
||||
{
|
||||
public QueryBuilder With<T>() where T : struct;
|
||||
public QueryBuilder Without<T>() where T : struct;
|
||||
public QueryDescriptor Build();
|
||||
public Query<T1> Without<W>() where W : struct;
|
||||
}
|
||||
|
||||
// Overloads for 2–6 With types:
|
||||
public struct Query<T1, T2> where T1 : struct where T2 : struct { ... }
|
||||
// ... up to Query<T1, T2, T3, T4, T5, T6>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## QueryDescriptor
|
||||
|
||||
Usage:
|
||||
```csharp
|
||||
namespace OECS;
|
||||
// Two required types, one excluded:
|
||||
var query = new Query<Position, Velocity>().Without<Frozen>();
|
||||
foreach (var it in world.Select(query)) { ... }
|
||||
|
||||
public class QueryDescriptor
|
||||
{
|
||||
public IReadOnlySet<Type> With { get; }
|
||||
public IReadOnlySet<Type> Without { get; }
|
||||
}
|
||||
// Simple scan — no Without filter needed:
|
||||
foreach (var it in world.Select<Card>()) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
@@ -242,10 +201,10 @@ public interface ISystem
|
||||
```
|
||||
|
||||
Systems do **not** declare a query on the interface. Instead, they either
|
||||
read singletons directly (`world.ReadSingleton<T>()`) or use the iterator /
|
||||
`ForEach` API with a `QueryDescriptor` they build internally. This keeps
|
||||
the interface minimal and gives systems full flexibility over what they
|
||||
inspect at runtime.
|
||||
read singletons directly (`world.ReadSingleton<T>()`) or use the
|
||||
`WorldQueryExtensions` (`Select`, `FindEntity`, `FindEntities`) with a
|
||||
`Query<T1..T6>` they build inline. This keeps the interface minimal and
|
||||
gives systems full flexibility over what they inspect at runtime.
|
||||
|
||||
---
|
||||
|
||||
@@ -486,35 +445,24 @@ public record struct Velocity
|
||||
// Define a system
|
||||
public class MovementSystem : ITickedSystem
|
||||
{
|
||||
private readonly QueryDescriptor _query;
|
||||
|
||||
public MovementSystem(World world)
|
||||
{
|
||||
_query = world.Query()
|
||||
.With<Position>()
|
||||
.With<Velocity>()
|
||||
.Build();
|
||||
}
|
||||
|
||||
public void Run(World world) => Run(world, Tick.Logical());
|
||||
|
||||
public void Run(World world, Tick tick)
|
||||
{
|
||||
float dt = tick.DeltaTime;
|
||||
|
||||
world.ForEach(_query, (Entity entity, ref Position pos, ref Velocity vel) =>
|
||||
foreach (var it in world.Select<Position, Velocity>())
|
||||
{
|
||||
pos.X += vel.X * dt;
|
||||
pos.Y += vel.Y * dt;
|
||||
world.MarkModified<Position>(entity);
|
||||
});
|
||||
it.Item1.X += it.Item2.X * dt;
|
||||
it.Item1.Y += it.Item2.Y * dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wire it up
|
||||
var world = new World();
|
||||
var group = new SystemGroup(world);
|
||||
group.Add(new MovementSystem(world));
|
||||
group.Add(new MovementSystem());
|
||||
|
||||
// Create entities
|
||||
var player = world.CreateEntity();
|
||||
@@ -543,6 +491,6 @@ These types are implementation details and may change without notice:
|
||||
| `ChangeSet` | Deduplicated change accumulator within a single batch. |
|
||||
| `RelationshipIndex` | Reverse lookup from target entity to source entities. |
|
||||
| `EntityAllocator` | Free-list + bump allocator for entity IDs. |
|
||||
| `QueryExecutor` | Query iteration logic with smallest-set driver optimization. |
|
||||
| `WorldQueryExtensions` | Extension methods with ref struct iterators and entity lookup. |
|
||||
| `ComponentRegistry` | Source-generated registry of all component types for serialization. |
|
||||
| `ComponentDescriptor` | Source-generated per-type descriptor with serialize/deserialize callbacks. |
|
||||
|
||||
Reference in New Issue
Block a user