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
+35 -24
View File
@@ -117,30 +117,40 @@ The `ISystem` interface has no `Query` property. Systems read singletons,
build queries, and iterate on their own — this keeps the interface minimal
and gives systems full flexibility.
### Iteration Styles
### Iteration
Two options:
Use `world.Select<T1..T6>()` with `foreach` for zero-allocation iteration with
`ref` access to components:
**ForEach callbacks** (16 components):
```csharp
var query = world.Query().With<Position>().With<Velocity>().Build();
world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel) =>
foreach (var it in world.Select<Position, Velocity>())
{
pos.X += vel.X * dt;
world.MarkModified<Position>(e);
});
```
**Ref struct iterators** via `EntityIterator.Select<T>()` (13 components):
```csharp
using var iter = world.Select<PlayerHand>();
while (iter.MoveNext())
{
// iter.CurrentEntity, iter.Current1 (ref)
it.Item1.X += it.Item2.X * dt; // ref Position, ref Velocity
}
```
The singleton entity (ID 1) is automatically skipped by all iterators.
For queries with exclusion filters, create a `Query<T>` with `.Without<W>()`:
```csharp
var query = new Query<Cell>().Without<Mark>();
foreach (var it in world.Select(query))
{
if (it.Item1.Row == row && it.Item1.Col == col) { ... }
}
```
To find entities, use `FindEntity<T>()` or `FindEntities<T>()`:
```csharp
var handEntity = world.FindEntity<PlayerHand>();
var allCards = world.FindEntities<Card>();
```
The `Select` ref struct enumerators expose:
- `it.Entity` — the current entity handle.
- `it.Item1`..`it.ItemN``ref` references to the matched components.
Singleton entities are automatically excluded from query results.
### System Registration
@@ -176,7 +186,6 @@ public record struct PlaceBetCommand : ICommand
state.CurrentBet = Amount;
state.Chips -= Amount;
state.Phase = GamePhase.Dealing;
world.MarkModified<GameState>(World.SingletonEntity);
}
}
```
@@ -186,10 +195,12 @@ when the queue is drained (automatically by `SystemGroup`).
## Singletons
Global state lives on the singleton entity (ID 1). Use `SetSingleton<T>`,
`GetSingleton<T>` (ref), and `ReadSingleton<T>` (copy):
Each singleton component type gets its own dedicated entity, allocated
automatically by `SetSingleton<T>`. Use `SetSingleton<T>`, `GetSingleton<T>`
(ref), and `ReadSingleton<T>` (copy):
```csharp
// Setup:
world.SetSingleton(new GameState { Phase = GamePhase.Betting, Chips = 100 });
// Read-only inspection:
@@ -198,12 +209,12 @@ var state = world.ReadSingleton<GameState>();
// Mutation:
ref var mutable = ref world.GetSingleton<GameState>();
mutable.Phase = GamePhase.RoundOver;
world.MarkModified<GameState>(World.SingletonEntity);
```
`GetSingleton` returns a `ref` — always call `MarkModified` after mutating
so reactivity subscribers see the change. `ReadSingleton` returns a copy and
never auto-marks.
`GetSingleton` returns a `ref`. During system execution (inside `SystemGroup`),
mutations via `GetSingleton` are auto-marked. Outside of iteration, call
`MarkModified` after mutating so reactivity subscribers see the change.
`ReadSingleton` returns a copy and never auto-marks.
## Change Tracking