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:
2026-07-21 00:24:10 +08:00
parent 737136e2ef
commit b98e8d66af
4 changed files with 127 additions and 158 deletions
+73 -125
View File
@@ -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 26 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 26 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 26 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. |