docs: update skill guides for auto-tracking and batching changes
This commit is contained in:
parent
4b94f411bf
commit
52e1e8fdc9
|
|
@ -38,8 +38,8 @@ Key ADRs to keep in mind when changing the library:
|
||||||
| 002 | 32-bit Entity (24 ID + 8 version) | Fits in register, enough headroom |
|
| 002 | 32-bit Entity (24 ID + 8 version) | Fits in register, enough headroom |
|
||||||
| 003 | Explicit query iteration | Visible cost model, no code-gen needed |
|
| 003 | Explicit query iteration | Visible cost model, no code-gen needed |
|
||||||
| 004 | Registration order for systems | Simplest model that works at this scale |
|
| 004 | Registration order for systems | Simplest model that works at this scale |
|
||||||
| 005 | Manual `MarkModified` for value changes | No per-component overhead |
|
| 005 | Auto-tracking in batching scopes | Eliminates manual `MarkModified` in systems |
|
||||||
| 006 | Deferred change posting | Prevents mid-system reentrancy |
|
| 006 | Deferred mutation batching | Prevents mid-system reentrancy, safe iteration |
|
||||||
| 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 per component type | Each singleton gets its own entity |
|
| 009 | Singleton per component type | Each singleton gets its own entity |
|
||||||
|
|
@ -73,12 +73,12 @@ Uses swap-remove for O(1) deletion.
|
||||||
|
|
||||||
Zero-allocation `ref struct` enumerators returned by `world.Select<T1..T6>()`.
|
Zero-allocation `ref struct` enumerators returned by `world.Select<T1..T6>()`.
|
||||||
Drive from the smallest sparse set to minimize probes. Support `foreach` via
|
Drive from the smallest sparse set to minimize probes. Support `foreach` via
|
||||||
`GetEnumerator()` returning `this`. Expose `Entity` and `ref Item1`..`ref ItemN`
|
`GetEnumerator()` returning `this`. Expose `Entity`, `Ref1`..`RefN` (mutable,
|
||||||
properties. Constructor/dispose manage `BeginIteration()`/`EndIteration()` for
|
tracked for auto-dirty-marking), and `Val1`..`ValN` (read-only, untracked).
|
||||||
pending mutation flushing. Singleton entities are excluded via
|
Constructor/dispose manage `BeginBatching()`/`EndBatching()` for pending
|
||||||
`IsSingletonEntity()` check.
|
mutation flushing. Singleton entities are excluded via `IsSingletonEntity()`.
|
||||||
|
|
||||||
Also provides `FindEntity<T>()` (first match) and `FindEntities<T>()` (all matches).
|
Also provides `FindEntity<T>()` (first match).
|
||||||
|
|
||||||
### `Query<T1..T6>` — generic query descriptors
|
### `Query<T1..T6>` — generic query descriptors
|
||||||
|
|
||||||
|
|
@ -90,8 +90,11 @@ method adds exclusion filters without changing the arity. Replaces the old
|
||||||
|
|
||||||
Manages an ordered list of `ISystem`. `RunAll()`:
|
Manages an ordered list of `ISystem`. `RunAll()`:
|
||||||
1. Drain commands (pre-tick).
|
1. Drain commands (pre-tick).
|
||||||
2. For each system: run it → drain commands → post changes.
|
2. `BeginBatching()` — outer batching scope for the tick.
|
||||||
3. Drain commands + post changes (post-tick).
|
3. For each system: run via `Run` extension (nested batch) → drain commands →
|
||||||
|
flush pending mutations → post changes.
|
||||||
|
4. `EndBatching()` — auto-marks + flushes remaining.
|
||||||
|
5. Drain commands + post changes (post-tick).
|
||||||
|
|
||||||
### `WorldSerializer` — save/load
|
### `WorldSerializer` — save/load
|
||||||
|
|
||||||
|
|
@ -189,11 +192,12 @@ when there are no fields to compare.
|
||||||
|
|
||||||
## Common Pitfalls
|
## Common Pitfalls
|
||||||
|
|
||||||
- **Forgetting `MarkModified` after mutating a `ref` component.** Changes won't
|
- **Using `ValN` when you meant to mutate.** `ValN` returns a `ref readonly` —
|
||||||
propagate to R3 subscribers. Debug builds have a warning for this.
|
writes through it still work but won't be tracked for auto-dirty-marking.
|
||||||
|
Use `RefN` for any component you intend to mutate.
|
||||||
- **Modifying components during iteration.** Pending mutations are flushed at
|
- **Modifying components during iteration.** Pending mutations are flushed at
|
||||||
the end of iteration (via `BeginIteration`/`EndIteration`). Adding/removing
|
the end of the batching scope. Adding/removing components mid-iteration is
|
||||||
components mid-iteration is safe — they're deferred.
|
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.
|
||||||
- **Singletons each have their own entity allocated automatically by
|
- **Singletons each have their own entity allocated automatically by
|
||||||
|
|
|
||||||
|
|
@ -97,12 +97,13 @@ target) are cleaned up automatically.
|
||||||
|
|
||||||
## Defining Systems
|
## Defining Systems
|
||||||
|
|
||||||
Systems implement `ISystem` (or `ITickedSystem` if they need delta time):
|
Systems implement `ISystem` (or `ITickedSystem` if they need delta time).
|
||||||
|
Implement `RunImpl` with the system logic:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public class DealSystem : ISystem
|
public class DealSystem : ISystem
|
||||||
{
|
{
|
||||||
public void Run(World world)
|
public void RunImpl(World world)
|
||||||
{
|
{
|
||||||
var state = world.ReadSingleton<GameState>();
|
var state = world.ReadSingleton<GameState>();
|
||||||
if (state.Phase != GamePhase.Dealing)
|
if (state.Phase != GamePhase.Dealing)
|
||||||
|
|
@ -117,15 +118,29 @@ 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.
|
||||||
|
|
||||||
|
### Running Systems
|
||||||
|
|
||||||
|
Use the `Run` extension method to execute a system with automatic batching:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var system = new MySystem();
|
||||||
|
system.Run(world); // wraps RunImpl in BeginBatching/EndBatching
|
||||||
|
```
|
||||||
|
|
||||||
|
`SystemGroup` handles this automatically — you just register systems and call
|
||||||
|
`RunLogical()` or `RunTimed()`.
|
||||||
|
|
||||||
### Iteration
|
### Iteration
|
||||||
|
|
||||||
Use `world.Select<T1..T6>()` with `foreach` for zero-allocation iteration with
|
Use `world.Select<T1..T6>()` with `foreach` for zero-allocation iteration with
|
||||||
`ref` access to components:
|
`ref` access to components. Use `RefN` for components you intend to mutate
|
||||||
|
(tracked for auto-dirty-marking) and `ValN` for read-only access (untracked):
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
foreach (var it in world.Select<Position, Velocity>())
|
foreach (var it in world.Select<Position, Velocity>())
|
||||||
{
|
{
|
||||||
it.Item1.X += it.Item2.X * dt; // ref Position, ref Velocity
|
it.Ref1.X += it.Val2.X * dt; // Position tracked, Velocity not
|
||||||
|
it.Ref1.Y += it.Val2.Y * dt;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -135,20 +150,20 @@ For queries with exclusion filters, create a `Query<T>` with `.Without<W>()`:
|
||||||
var query = new Query<Cell>().Without<Mark>();
|
var query = new Query<Cell>().Without<Mark>();
|
||||||
foreach (var it in world.Select(query))
|
foreach (var it in world.Select(query))
|
||||||
{
|
{
|
||||||
if (it.Item1.Row == row && it.Item1.Col == col) { ... }
|
if (it.Val1.Row == row && it.Val1.Col == col) { ... }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To find entities, use `FindEntity<T>()` or `FindEntities<T>()`:
|
To find a single entity, use `FindEntity<T>()`:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var handEntity = world.FindEntity<PlayerHand>();
|
var handEntity = world.FindEntity<PlayerHand>();
|
||||||
var allCards = world.FindEntities<Card>();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `Select` ref struct enumerators expose:
|
The `Select` ref struct enumerators expose:
|
||||||
- `it.Entity` — the current entity handle.
|
- `it.Entity` — the current entity handle.
|
||||||
- `it.Item1`..`it.ItemN` — `ref` references to the matched components.
|
- `it.Ref1`..`it.RefN` — `ref` references to components (tracked for auto-dirty).
|
||||||
|
- `it.Val1`..`it.ValN` — `ref readonly` references to components (untracked).
|
||||||
|
|
||||||
Singleton entities are automatically excluded from query results.
|
Singleton entities are automatically excluded from query results.
|
||||||
|
|
||||||
|
|
@ -165,8 +180,8 @@ group.Add(new PlayerBustCheckSystem());
|
||||||
group.Add(new DealerSystem());
|
group.Add(new DealerSystem());
|
||||||
```
|
```
|
||||||
|
|
||||||
`SystemGroup` automatically drains commands and posts changes after each system
|
`SystemGroup` automatically drains commands, flushes pending mutations, and
|
||||||
and after the full tick.
|
posts changes after each system and after the full tick.
|
||||||
|
|
||||||
## Defining Commands
|
## Defining Commands
|
||||||
|
|
||||||
|
|
@ -191,7 +206,9 @@ public record struct PlaceBetCommand : ICommand
|
||||||
```
|
```
|
||||||
|
|
||||||
Enqueue commands via `world.Commands.Enqueue(...)`. They execute deferred
|
Enqueue commands via `world.Commands.Enqueue(...)`. They execute deferred
|
||||||
when the queue is drained (automatically by `SystemGroup`).
|
when the queue is drained (automatically by `SystemGroup`). Commands run
|
||||||
|
inside a batching scope, so `GetSingleton` and `GetComponent` calls are
|
||||||
|
auto-tracked for modification.
|
||||||
|
|
||||||
## Singletons
|
## Singletons
|
||||||
|
|
||||||
|
|
@ -206,21 +223,24 @@ world.SetSingleton(new GameState { Phase = GamePhase.Betting, Chips = 100 });
|
||||||
// Read-only inspection:
|
// Read-only inspection:
|
||||||
var state = world.ReadSingleton<GameState>();
|
var state = world.ReadSingleton<GameState>();
|
||||||
|
|
||||||
// Mutation:
|
// Mutation (auto-tracked during batching scopes):
|
||||||
ref var mutable = ref world.GetSingleton<GameState>();
|
ref var mutable = ref world.GetSingleton<GameState>();
|
||||||
mutable.Phase = GamePhase.RoundOver;
|
mutable.Phase = GamePhase.RoundOver;
|
||||||
```
|
```
|
||||||
|
|
||||||
`GetSingleton` returns a `ref`. During system execution (inside `SystemGroup`),
|
`GetSingleton` returns a `ref`. During system execution, command drains, and
|
||||||
mutations via `GetSingleton` are auto-marked. Outside of iteration, call
|
`foreach` iterations, mutations via `GetSingleton` are auto-marked for change
|
||||||
`MarkModified` after mutating so reactivity subscribers see the change.
|
tracking. Outside a batching scope, call `MarkModified` after mutating.
|
||||||
`ReadSingleton` returns a copy and never auto-marks.
|
`ReadSingleton` returns a copy and never auto-marks.
|
||||||
|
|
||||||
## Change Tracking
|
## Change Tracking
|
||||||
|
|
||||||
- Structural changes (entity create/destroy, component add/remove) are auto-marked.
|
- Structural changes (entity create/destroy, component add/remove) are always
|
||||||
- Value mutations (modifying a `ref T` component) must be manually marked via
|
auto-marked.
|
||||||
`world.MarkModified<T>(entity)`.
|
- Value mutations via `GetComponent<T>`, `GetSingleton<T>`, and `RefN` iterator
|
||||||
|
properties are auto-tracked during batching scopes (system runs, command
|
||||||
|
drains, `foreach` iterations).
|
||||||
|
- Outside batching scopes, call `world.MarkModified<T>(entity)` explicitly.
|
||||||
- Changes are posted after each system runs (automatic via `SystemGroup`).
|
- Changes are posted after each system runs (automatic via `SystemGroup`).
|
||||||
|
|
||||||
## Serialization
|
## Serialization
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue