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:
parent
737136e2ef
commit
b98e8d66af
|
|
@ -42,7 +42,7 @@ Key ADRs to keep in mind when changing the library:
|
||||||
| 006 | Deferred change posting | Prevents mid-system reentrancy |
|
| 006 | Deferred change posting | Prevents mid-system reentrancy |
|
||||||
| 007 | R3 for reactivity | Zero-allocation, UI lifecycle-friendly |
|
| 007 | R3 for reactivity | Zero-allocation, UI lifecycle-friendly |
|
||||||
| 008 | Commands as serializable structs in a queue | Not ECS state |
|
| 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 |
|
| 010 | Single-threaded by default | Simpler, no locks needed |
|
||||||
| 011 | .NET 8 target | LTS through Nov 2026 |
|
| 011 | .NET 8 target | LTS through Nov 2026 |
|
||||||
| 012 | Public types required for MessagePack | Build-time validation via analyzer |
|
| 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).
|
parallel entity IDs, `sparse[]` maps entity ID → dense index (-1 = absent).
|
||||||
Uses swap-remove for O(1) deletion.
|
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
|
Zero-allocation `ref struct` enumerators returned by `world.Select<T1..T6>()`.
|
||||||
probes. Support `foreach` via `GetEnumerator()` returning `this`. Must call
|
Drive from the smallest sparse set to minimize probes. Support `foreach` via
|
||||||
`world.BeginIteration()` / `EndIteration()` for pending mutation flushing.
|
`GetEnumerator()` returning `this`. Expose `Entity` and `ref Item1`..`ref ItemN`
|
||||||
Always skip singleton entity (ID 1).
|
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
|
### `SystemGroup` — system orchestration
|
||||||
|
|
||||||
|
|
@ -186,5 +196,5 @@ when there are no fields to compare.
|
||||||
components mid-iteration is safe — they're deferred.
|
components mid-iteration is safe — they're deferred.
|
||||||
- **Type not public or missing `[MessagePackObject]`.** Serialization will fail.
|
- **Type not public or missing `[MessagePackObject]`.** Serialization will fail.
|
||||||
The `MessagePackAnalyzer` catches most issues at compile time.
|
The `MessagePackAnalyzer` catches most issues at compile time.
|
||||||
- **Entity ID 1 is the singleton.** Don't destroy it. Iterators skip it
|
- **Singletons each have their own entity allocated automatically by
|
||||||
automatically.
|
`SetSingleton<T>`.** Iterators skip singleton entities automatically.
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ public void PlaceBet_AdvancesToDealing()
|
||||||
|
|
||||||
Common test helpers:
|
Common test helpers:
|
||||||
- `SetupGame(seed?)` — create world, register systems, set initial singletons.
|
- `SetupGame(seed?)` — create world, register systems, set initial singletons.
|
||||||
- `FindEntity<T>(world)` — find first non-singleton entity with component T.
|
- `FindEntity<T>(world)` — find first entity with component T (singletons are automatically excluded by query results).
|
||||||
- `CountCardsInHand(world, handEntity)` or `GetHandCards(world, handEntity)`.
|
- `CountCardsInHand(world, handEntity)` or `GetHandCards(world, handEntity)`.
|
||||||
|
|
||||||
Always keep helpers in the test class (or a shared base) rather than in the
|
Always keep helpers in the test class (or a shared base) rather than in the
|
||||||
|
|
|
||||||
|
|
@ -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
|
build queries, and iterate on their own — this keeps the interface minimal
|
||||||
and gives systems full flexibility.
|
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** (1–6 components):
|
|
||||||
```csharp
|
```csharp
|
||||||
var query = world.Query().With<Position>().With<Velocity>().Build();
|
foreach (var it in world.Select<Position, Velocity>())
|
||||||
world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel) =>
|
|
||||||
{
|
{
|
||||||
pos.X += vel.X * dt;
|
it.Item1.X += it.Item2.X * dt; // ref Position, ref Velocity
|
||||||
world.MarkModified<Position>(e);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ref struct iterators** via `EntityIterator.Select<T>()` (1–3 components):
|
|
||||||
```csharp
|
|
||||||
using var iter = world.Select<PlayerHand>();
|
|
||||||
while (iter.MoveNext())
|
|
||||||
{
|
|
||||||
// iter.CurrentEntity, iter.Current1 (ref)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
### System Registration
|
||||||
|
|
||||||
|
|
@ -176,7 +186,6 @@ public record struct PlaceBetCommand : ICommand
|
||||||
state.CurrentBet = Amount;
|
state.CurrentBet = Amount;
|
||||||
state.Chips -= Amount;
|
state.Chips -= Amount;
|
||||||
state.Phase = GamePhase.Dealing;
|
state.Phase = GamePhase.Dealing;
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -186,10 +195,12 @@ when the queue is drained (automatically by `SystemGroup`).
|
||||||
|
|
||||||
## Singletons
|
## Singletons
|
||||||
|
|
||||||
Global state lives on the singleton entity (ID 1). Use `SetSingleton<T>`,
|
Each singleton component type gets its own dedicated entity, allocated
|
||||||
`GetSingleton<T>` (ref), and `ReadSingleton<T>` (copy):
|
automatically by `SetSingleton<T>`. Use `SetSingleton<T>`, `GetSingleton<T>`
|
||||||
|
(ref), and `ReadSingleton<T>` (copy):
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
// Setup:
|
||||||
world.SetSingleton(new GameState { Phase = GamePhase.Betting, Chips = 100 });
|
world.SetSingleton(new GameState { Phase = GamePhase.Betting, Chips = 100 });
|
||||||
|
|
||||||
// Read-only inspection:
|
// Read-only inspection:
|
||||||
|
|
@ -198,12 +209,12 @@ var state = world.ReadSingleton<GameState>();
|
||||||
// Mutation:
|
// Mutation:
|
||||||
ref var mutable = ref world.GetSingleton<GameState>();
|
ref var mutable = ref world.GetSingleton<GameState>();
|
||||||
mutable.Phase = GamePhase.RoundOver;
|
mutable.Phase = GamePhase.RoundOver;
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`GetSingleton` returns a `ref` — always call `MarkModified` after mutating
|
`GetSingleton` returns a `ref`. During system execution (inside `SystemGroup`),
|
||||||
so reactivity subscribers see the change. `ReadSingleton` returns a copy and
|
mutations via `GetSingleton` are auto-marked. Outside of iteration, call
|
||||||
never auto-marks.
|
`MarkModified` after mutating so reactivity subscribers see the change.
|
||||||
|
`ReadSingleton` returns a copy and never auto-marks.
|
||||||
|
|
||||||
## Change Tracking
|
## Change Tracking
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,6 @@ namespace OECS;
|
||||||
|
|
||||||
public class World : IDisposable
|
public class World : IDisposable
|
||||||
{
|
{
|
||||||
// --- Singleton entity ---
|
|
||||||
public static Entity SingletonEntity { get; } // ID=1, Version=1
|
|
||||||
|
|
||||||
// --- Lifecycle ---
|
// --- Lifecycle ---
|
||||||
public World();
|
public World();
|
||||||
|
|
||||||
|
|
@ -64,24 +61,7 @@ public class World : IDisposable
|
||||||
public void RemoveSingleton<T>() where T : struct;
|
public void RemoveSingleton<T>() where T : struct;
|
||||||
|
|
||||||
// --- Queries ---
|
// --- Queries ---
|
||||||
public QueryBuilder Query();
|
// See QueryExtensions for Select / FindEntity / FindEntities.
|
||||||
|
|
||||||
// --- 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;
|
|
||||||
|
|
||||||
// --- Commands ---
|
// --- Commands ---
|
||||||
public CommandQueue Commands { get; }
|
public CommandQueue Commands { get; }
|
||||||
|
|
@ -94,7 +74,10 @@ public class World : IDisposable
|
||||||
public Observable<EntityChange> ObserveEntityChanges();
|
public Observable<EntityChange> ObserveEntityChanges();
|
||||||
public Observable<EntityChange> ObserveComponentChanges<T>()
|
public Observable<EntityChange> ObserveComponentChanges<T>()
|
||||||
where T : struct;
|
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 ---
|
// --- Relationships ---
|
||||||
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
|
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
|
Extension methods on `World` providing `foreach`-compatible iteration and
|
||||||
`Action<...>` delegates do not support `ref` parameters.
|
entity lookup. Select returns `ref struct` enumerators for zero-allocation
|
||||||
|
iteration with `ref` access to components.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
namespace OECS;
|
namespace OECS;
|
||||||
|
|
||||||
public delegate void ForEachAction<T1>(Entity entity, ref T1 c1) where T1 : struct;
|
public static class WorldQueryExtensions
|
||||||
|
|
||||||
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
|
|
||||||
{
|
{
|
||||||
// With explicit QueryDescriptor (supports Without<T> filters).
|
// Select — returns a ref struct enumerator for foreach.
|
||||||
public static Select1<T1> Select<T1>(
|
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>(
|
// FindEntity — returns the first matching entity or Entity.Null.
|
||||||
this World world, QueryDescriptor query)
|
public static Entity FindEntity<T1>(
|
||||||
where T1 : struct where T2 : struct;
|
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>(
|
// FindEntities — returns all matching entities as List<Entity>.
|
||||||
this World world, QueryDescriptor query)
|
public static List<Entity> FindEntities<T1>(...) where T1 : struct;
|
||||||
where T1 : struct where T2 : struct where T3 : 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;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The singletons `CurrentEntity`, `Current1`, `Current2`, `Current3` are
|
Each ref struct (Select1 through Select6) exposes:
|
||||||
exposed on the ref struct iterators directly. Usage:
|
- `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
|
```csharp
|
||||||
// Simple scan: all entities with PlayerHand.
|
// Simple scan: all entities with Position.
|
||||||
using var iter = world.Select<PlayerHand>();
|
foreach (var it in world.Select<Position>())
|
||||||
while (iter.MoveNext())
|
|
||||||
{
|
{
|
||||||
Console.WriteLine(iter.CurrentEntity);
|
it.Item1.X += 1; // ref mutates component in-place
|
||||||
}
|
}
|
||||||
|
|
||||||
// With query filter:
|
// With Without filter:
|
||||||
var query = world.Query().With<Position>().Without<Frozen>().Build();
|
var query = new Query<Position>().Without<Frozen>();
|
||||||
using var iter2 = world.Select<Position>(query);
|
foreach (var it in world.Select(query))
|
||||||
while (iter2.MoveNext())
|
|
||||||
{
|
{
|
||||||
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
|
```csharp
|
||||||
namespace OECS;
|
namespace OECS;
|
||||||
|
|
||||||
public class QueryBuilder
|
public struct Query<T1> where T1 : struct
|
||||||
{
|
{
|
||||||
public QueryBuilder With<T>() where T : struct;
|
public Query<T1> Without<W>() where W : struct;
|
||||||
public QueryBuilder Without<T>() where T : struct;
|
|
||||||
public QueryDescriptor Build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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>
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Usage:
|
||||||
|
|
||||||
## QueryDescriptor
|
|
||||||
|
|
||||||
```csharp
|
```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
|
// Simple scan — no Without filter needed:
|
||||||
{
|
foreach (var it in world.Select<Card>()) { ... }
|
||||||
public IReadOnlySet<Type> With { get; }
|
|
||||||
public IReadOnlySet<Type> Without { get; }
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -242,10 +201,10 @@ public interface ISystem
|
||||||
```
|
```
|
||||||
|
|
||||||
Systems do **not** declare a query on the interface. Instead, they either
|
Systems do **not** declare a query on the interface. Instead, they either
|
||||||
read singletons directly (`world.ReadSingleton<T>()`) or use the iterator /
|
read singletons directly (`world.ReadSingleton<T>()`) or use the
|
||||||
`ForEach` API with a `QueryDescriptor` they build internally. This keeps
|
`WorldQueryExtensions` (`Select`, `FindEntity`, `FindEntities`) with a
|
||||||
the interface minimal and gives systems full flexibility over what they
|
`Query<T1..T6>` they build inline. This keeps the interface minimal and
|
||||||
inspect at runtime.
|
gives systems full flexibility over what they inspect at runtime.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -486,35 +445,24 @@ public record struct Velocity
|
||||||
// Define a system
|
// Define a system
|
||||||
public class MovementSystem : ITickedSystem
|
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) => Run(world, Tick.Logical());
|
||||||
|
|
||||||
public void Run(World world, Tick tick)
|
public void Run(World world, Tick tick)
|
||||||
{
|
{
|
||||||
float dt = tick.DeltaTime;
|
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;
|
it.Item1.X += it.Item2.X * dt;
|
||||||
pos.Y += vel.Y * dt;
|
it.Item1.Y += it.Item2.Y * dt;
|
||||||
world.MarkModified<Position>(entity);
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire it up
|
// Wire it up
|
||||||
var world = new World();
|
var world = new World();
|
||||||
var group = new SystemGroup(world);
|
var group = new SystemGroup(world);
|
||||||
group.Add(new MovementSystem(world));
|
group.Add(new MovementSystem());
|
||||||
|
|
||||||
// Create entities
|
// Create entities
|
||||||
var player = world.CreateEntity();
|
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. |
|
| `ChangeSet` | Deduplicated change accumulator within a single batch. |
|
||||||
| `RelationshipIndex` | Reverse lookup from target entity to source entities. |
|
| `RelationshipIndex` | Reverse lookup from target entity to source entities. |
|
||||||
| `EntityAllocator` | Free-list + bump allocator for entity IDs. |
|
| `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. |
|
| `ComponentRegistry` | Source-generated registry of all component types for serialization. |
|
||||||
| `ComponentDescriptor` | Source-generated per-type descriptor with serialize/deserialize callbacks. |
|
| `ComponentDescriptor` | Source-generated per-type descriptor with serialize/deserialize callbacks. |
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue