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
+18 -8
View File
@@ -42,7 +42,7 @@ Key ADRs to keep in mind when changing the library:
| 006 | Deferred change posting | Prevents mid-system reentrancy |
| 007 | R3 for reactivity | Zero-allocation, UI lifecycle-friendly |
| 008 | Commands as serializable structs in a queue | Not ECS state |
| 009 | Singleton as reserved entity (ID 1) | Reuses component storage |
| 009 | Singleton per component type | Each singleton gets its own entity |
| 010 | Single-threaded by default | Simpler, no locks needed |
| 011 | .NET 8 target | LTS through Nov 2026 |
| 012 | Public types required for MessagePack | Build-time validation via analyzer |
@@ -69,12 +69,22 @@ Dense/sparse array pair. `dense[]` is packed values, `denseEntities[]` is
parallel entity IDs, `sparse[]` maps entity ID → dense index (-1 = absent).
Uses swap-remove for O(1) deletion.
### `EntityIterator.Select1/2/3<T...>` — ref struct iterators
### `WorldQueryExtensions.Select1..Select6<T...>` — ref struct iterators
Zero-allocation iterators. Drive from the smallest sparse set to minimize
probes. Support `foreach` via `GetEnumerator()` returning `this`. Must call
`world.BeginIteration()` / `EndIteration()` for pending mutation flushing.
Always skip singleton entity (ID 1).
Zero-allocation `ref struct` enumerators returned by `world.Select<T1..T6>()`.
Drive from the smallest sparse set to minimize probes. Support `foreach` via
`GetEnumerator()` returning `this`. Expose `Entity` and `ref Item1`..`ref ItemN`
properties. Constructor/dispose manage `BeginIteration()`/`EndIteration()` for
pending mutation flushing. Singleton entities are excluded via
`IsSingletonEntity()` check.
Also provides `FindEntity<T>()` (first match) and `FindEntities<T>()` (all matches).
### `Query<T1..T6>` — generic query descriptors
Structs that encode `With` types as generic parameters. `Without<W>()` fluent
method adds exclusion filters without changing the arity. Replaces the old
`QueryBuilder` / `QueryDescriptor` / `ForEachAction` pattern.
### `SystemGroup` — system orchestration
@@ -186,5 +196,5 @@ when there are no fields to compare.
components mid-iteration is safe — they're deferred.
- **Type not public or missing `[MessagePackObject]`.** Serialization will fail.
The `MessagePackAnalyzer` catches most issues at compile time.
- **Entity ID 1 is the singleton.** Don't destroy it. Iterators skip it
automatically.
- **Singletons each have their own entity allocated automatically by
`SetSingleton<T>`.** Iterators skip singleton entities automatically.