From 52e1e8fdc90ca7f7f03ff8b8e250bc25996fcf27 Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 21 Jul 2026 11:03:01 +0800 Subject: [PATCH] docs: update skill guides for auto-tracking and batching changes --- .agents/skills/developing-oecs/SKILL.md | 30 +++++++------ .agents/skills/writing-games/SKILL.md | 56 +++++++++++++++++-------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/.agents/skills/developing-oecs/SKILL.md b/.agents/skills/developing-oecs/SKILL.md index 27ab080..805df00 100644 --- a/.agents/skills/developing-oecs/SKILL.md +++ b/.agents/skills/developing-oecs/SKILL.md @@ -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 | | 003 | Explicit query iteration | Visible cost model, no code-gen needed | | 004 | Registration order for systems | Simplest model that works at this scale | -| 005 | Manual `MarkModified` for value changes | No per-component overhead | -| 006 | Deferred change posting | Prevents mid-system reentrancy | +| 005 | Auto-tracking in batching scopes | Eliminates manual `MarkModified` in systems | +| 006 | Deferred mutation batching | Prevents mid-system reentrancy, safe iteration | | 007 | R3 for reactivity | Zero-allocation, UI lifecycle-friendly | | 008 | Commands as serializable structs in a queue | Not ECS state | | 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()`. 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. +`GetEnumerator()` returning `this`. Expose `Entity`, `Ref1`..`RefN` (mutable, +tracked for auto-dirty-marking), and `Val1`..`ValN` (read-only, untracked). +Constructor/dispose manage `BeginBatching()`/`EndBatching()` for pending +mutation flushing. Singleton entities are excluded via `IsSingletonEntity()`. -Also provides `FindEntity()` (first match) and `FindEntities()` (all matches). +Also provides `FindEntity()` (first match). ### `Query` — 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()`: 1. Drain commands (pre-tick). -2. For each system: run it → drain commands → post changes. -3. Drain commands + post changes (post-tick). +2. `BeginBatching()` — outer batching scope for the 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 @@ -189,11 +192,12 @@ when there are no fields to compare. ## Common Pitfalls -- **Forgetting `MarkModified` after mutating a `ref` component.** Changes won't - propagate to R3 subscribers. Debug builds have a warning for this. +- **Using `ValN` when you meant to mutate.** `ValN` returns a `ref readonly` — + 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 - the end of iteration (via `BeginIteration`/`EndIteration`). Adding/removing - components mid-iteration is safe — they're deferred. + the end of the batching scope. Adding/removing 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. - **Singletons each have their own entity allocated automatically by diff --git a/.agents/skills/writing-games/SKILL.md b/.agents/skills/writing-games/SKILL.md index 258730f..e9c17de 100644 --- a/.agents/skills/writing-games/SKILL.md +++ b/.agents/skills/writing-games/SKILL.md @@ -97,12 +97,13 @@ target) are cleaned up automatically. ## 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 public class DealSystem : ISystem { - public void Run(World world) + public void RunImpl(World world) { var state = world.ReadSingleton(); 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 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 Use `world.Select()` 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 foreach (var it in world.Select()) { - 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` with `.Without()`: var query = new Query().Without(); 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()` or `FindEntities()`: +To find a single entity, use `FindEntity()`: ```csharp var handEntity = world.FindEntity(); -var allCards = world.FindEntities(); ``` The `Select` ref struct enumerators expose: - `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. @@ -165,8 +180,8 @@ group.Add(new PlayerBustCheckSystem()); group.Add(new DealerSystem()); ``` -`SystemGroup` automatically drains commands and posts changes after each system -and after the full tick. +`SystemGroup` automatically drains commands, flushes pending mutations, and +posts changes after each system and after the full tick. ## Defining Commands @@ -191,7 +206,9 @@ public record struct PlaceBetCommand : ICommand ``` 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 @@ -206,21 +223,24 @@ world.SetSingleton(new GameState { Phase = GamePhase.Betting, Chips = 100 }); // Read-only inspection: var state = world.ReadSingleton(); -// Mutation: +// Mutation (auto-tracked during batching scopes): ref var mutable = ref world.GetSingleton(); mutable.Phase = GamePhase.RoundOver; ``` -`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. +`GetSingleton` returns a `ref`. During system execution, command drains, and +`foreach` iterations, mutations via `GetSingleton` are auto-marked for change +tracking. Outside a batching scope, call `MarkModified` after mutating. `ReadSingleton` returns a copy and never auto-marks. ## Change Tracking -- Structural changes (entity create/destroy, component add/remove) are auto-marked. -- Value mutations (modifying a `ref T` component) must be manually marked via - `world.MarkModified(entity)`. +- Structural changes (entity create/destroy, component add/remove) are always + auto-marked. +- Value mutations via `GetComponent`, `GetSingleton`, and `RefN` iterator + properties are auto-tracked during batching scopes (system runs, command + drains, `foreach` iterations). +- Outside batching scopes, call `world.MarkModified(entity)` explicitly. - Changes are posted after each system runs (automatic via `SystemGroup`). ## Serialization