--- name: writing-games description: Create game logic using the OECS entity component system (C#). Use this when building a new game or game feature with OECS — defining components, systems, commands, relationships, and singletons. --- # Writing Games with OECS OECS is a single-threaded, observable-first ECS for C#. It targets `net8.0` (C# 12) and depends on `MessagePack` (serialization) and `R3` (reactivity). Before writing any code, read `docs/api-surface.md` for the full type reference and `docs/architecture.md` for the design rationale behind the key decisions. ## Project Setup A game is a class library referencing `OECS`: ```xml Library Game.YourGameName ``` The game DLL must also reference `OECS.SourceGen` as an analyzer so the component registry is generated for serialization. See `Blackjack.csproj` for the exact MSBuild incantation. ## Defining Components Components are `public record struct` types annotated with `[MessagePackObject]` and `[Key]` attributes. Prefer `record struct` by default — it gives you value equality and a generated `ToString()` for free. Use **explicit properties or fields** — not positional syntax. OECS mutates components in-place via `ref T`, which requires settable fields/properties. Positional record structs produce `init`-only properties that can't be mutated through a `ref`. ```csharp [MessagePackObject] public record struct Card { [Key(0)] public Suit Suit; [Key(1)] public Rank Rank; } ``` - Types must be `public` — MessagePack requires public accessibility. - Use sequential integer keys starting from 0. - Tag components (no data) are just empty structs: ```csharp [MessagePackObject] public struct PlayerHand { } ``` ## Relationships Relationships are components that implement `IRelationship`. They model a directed edge between a source entity and a target entity. You can either use the generic `Relationship` base struct or implement `IRelationship` directly: ```csharp // Using the base struct: world.AddComponent(child, new Relationship { Source = child, Target = parent }); // Direct implementation (preferred for domain-specific names): [MessagePackObject] public record struct Holds : IRelationship { [Key(0)] public Entity Source { get; set; } [Key(1)] public Entity Target { get; set; } } ``` Reverse lookup is automatic. The `World` maintains a reverse index so you can query all sources pointing to a target: ```csharp var cards = world.GetSources(handEntity); ``` When an entity is destroyed, all relationships it participates in (as source or target) are cleaned up automatically. ## Defining Systems Systems implement `ISystem` (or `ITickedSystem` if they need delta time). Implement `RunImpl` with the system logic: ```csharp public class DealSystem : ISystem { public void RunImpl(World world) { var state = world.ReadSingleton(); if (state.Phase != GamePhase.Dealing) return; // Do work... } } ``` 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. 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.Ref1.X += it.Val2.X * dt; // Position tracked, Velocity not it.Ref1.Y += it.Val2.Y * dt; } ``` For queries with exclusion filters, create a `Query` with `.Without()`: ```csharp var query = new Query().Without(); foreach (var it in world.Select(query)) { if (it.Val1.Row == row && it.Val1.Col == col) { ... } } ``` To find a single entity, use `FindEntity()`: ```csharp var handEntity = world.FindEntity(); ``` The `Select` ref struct enumerators expose: - `it.Entity` — the current entity handle. - `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. ### System Registration Systems run in registration order via `SystemGroup`: ```csharp var world = new World(); var group = new SystemGroup(world); group.Add(new DeckSetupSystem()); group.Add(new DealSystem()); group.Add(new PlayerBustCheckSystem()); group.Add(new DealerSystem()); ``` `SystemGroup` automatically drains commands, flushes pending mutations, and posts changes after each system and after the full tick. ## Defining Commands Commands are `public record struct` types annotated with `[MessagePackObject]` and implementing `ICommand`: ```csharp [MessagePackObject] public record struct PlaceBetCommand : ICommand { [Key(0)] public int Amount; public void Execute(World world) { ref var state = ref world.GetSingleton(); if (state.Phase != GamePhase.Betting) return; state.CurrentBet = Amount; state.Chips -= Amount; state.Phase = GamePhase.Dealing; } } ``` Enqueue commands via `world.Commands.Enqueue(...)`. They execute deferred 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 Each singleton component type gets its own dedicated entity, allocated automatically by `SetSingleton`. Use `SetSingleton`, `GetSingleton` (ref), and `ReadSingleton` (copy): ```csharp // Setup: world.SetSingleton(new GameState { Phase = GamePhase.Betting, Chips = 100 }); // Read-only inspection: var state = world.ReadSingleton(); // Mutation (auto-tracked during batching scopes): ref var mutable = ref world.GetSingleton(); mutable.Phase = GamePhase.RoundOver; ``` `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 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 `WorldSerializer.Save/Load` uses the source-generated `ComponentRegistry`. All component types used with `World` generic methods are automatically discovered. Serialization round-trips must be tested — see `testing-games` skill.