oecs-sharp/docs/api-surface.md

564 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# API Surface: OECS
This document describes the public API surface of the OECS library. It serves
as a contract for implementation and a reference for consumers.
All types reside in the `OECS` namespace unless otherwise noted.
---
## Entity
```csharp
namespace OECS;
public readonly struct Entity : IEquatable<Entity>
{
public static Entity Null { get; } // ID=0, Version=0
public uint Id { get; } // lower 24-bit identifier
public uint Version { get; } // upper 8-bit generation
public bool IsNull { get; } // true for Entity.Null
public bool Equals(Entity other);
public override bool Equals(object? obj);
public override int GetHashCode();
public override string ToString(); // "Entity(42:v3)"
public static bool operator ==(Entity left, Entity right);
public static bool operator !=(Entity left, Entity right);
}
```
---
## World
```csharp
namespace OECS;
public class World : IDisposable
{
// --- Lifecycle ---
public World();
// --- Entity Management ---
public Entity CreateEntity();
public void DestroyEntity(Entity entity);
public bool IsAlive(Entity entity);
// --- Component Management ---
public void AddComponent<T>(Entity entity, T component) where T : struct;
public void RemoveComponent<T>(Entity entity) where T : struct;
public ref T GetComponent<T>(Entity entity) where T : struct;
public T ReadComponent<T>(Entity entity) where T : struct;
public bool TryGetComponent<T>(Entity entity, out T value) where T : struct;
public bool HasComponent<T>(Entity entity) where T : struct;
// --- Singleton ---
public void SetSingleton<T>(T component) where T : struct;
public ref T GetSingleton<T>() where T : struct;
public T ReadSingleton<T>() where T : struct;
public bool HasSingleton<T>() where T : struct;
public void RemoveSingleton<T>() where T : struct;
// --- Queries ---
// See WorldQueryExtensions for Select / FindEntity.
// --- Commands ---
public CommandQueue Commands { get; }
public void ExecuteCommands();
// --- Interrupts ---
public void Interrupt<T>(T interrupt) where T : struct, IInterrupt;
public bool HasInterrupt<T>() where T : struct, IInterrupt;
// --- Reactivity ---
public void MarkModified<T>(Entity entity) where T : struct;
public void PostChanges();
public Observable<EntityChange> ObserveEntityChanges();
public Observable<EntityChange> ObserveComponentChanges<T>()
where T : struct;
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 ---
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
where T : struct, IRelationship;
public void ReorderSources<T>(Entity target, IReadOnlyList<Entity> ordered)
where T : struct, IRelationship;
// --- Cleanup ---
public void Dispose();
}
```
---
## WorldQueryExtensions
Extension methods on `World` providing `foreach`-compatible iteration and
entity lookup. `Select` returns `ref struct` enumerators for zero-allocation
iteration with `ref` access to components.
```csharp
namespace OECS;
public static class WorldQueryExtensions
{
// Select — returns a ref struct enumerator for foreach.
public static Select1<T1> Select<T1>(
this World world, Query<T1> query = default) where T1 : struct;
// Overloads for 26 component types:
public static Select2<T1, T2> Select<T1, T2>(...) where T1 : struct where T2 : struct;
// ... up to Select6<T1..T6>
// FindEntity — returns the first matching entity or Entity.Null.
public static Entity FindEntity<T1>(
this World world, Query<T1> query = default) where T1 : struct;
public static Entity FindEntity<T1, T2>(
this World world, Query<T1, T2> query = default)
where T1 : struct where T2 : struct;
public static Entity FindEntity<T1, T2, T3>(
this World world, Query<T1, T2, T3> query = default)
where T1 : struct where T2 : struct where T3 : struct;
}
```
Each ref struct (Select1 through Select6) exposes:
- `Entity Entity` — the current entity handle.
- `ref T1 Ref1`, `ref T2 Ref2`, ... — mutable references to components.
Access is tracked for auto-dirty-marking during a batching scope.
- `ref readonly T1 Val1`, `ref readonly T2 Val2`, ... — read-only references.
Access is NOT tracked — use these when you only read the component.
- `bool MoveNext()` — advances and returns true while items remain.
- `GetEnumerator()` — returns `this` for `foreach` compatibility.
- `Dispose()` — ends the batching scope (flushes deferred mutations).
Usage:
```csharp
// Mutating: use RefN to auto-mark as modified.
foreach (var it in world.Select<Position, Velocity>())
{
it.Ref1.X += it.Val2.X * dt; // Position tracked, Velocity not
}
// Read-only: use ValN everywhere.
foreach (var it in world.Select<Cell, Mark>())
{
grid[it.Val1.Row, it.Val1.Col] = it.Val2.Player;
}
// With Without filter:
var query = new Query<Position>().Without<Frozen>();
foreach (var it in world.Select(query))
{
// it.Ref1, it.Val1
}
// Find first entity:
var hand = world.FindEntity<PlayerHand>();
```
---
## 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
namespace OECS;
public struct Query<T1> where T1 : struct
{
public Query<T1> Without<W>() where W : struct;
}
// Overloads for 26 With types:
public struct Query<T1, T2> where T1 : struct where T2 : struct { ... }
// ... up to Query<T1, T2, T3, T4, T5, T6>
```
Usage:
```csharp
// Two required types, one excluded:
var query = new Query<Position, Velocity>().Without<Frozen>();
foreach (var it in world.Select(query)) { ... }
// Simple scan — no Without filter needed:
foreach (var it in world.Select<Card>()) { ... }
```
---
## ISystem
A system is a unit of logic that runs during a tick. Implement `RunImpl` with
the system logic. Use the `Run` extension method to execute with automatic
batching — or register with `SystemGroup` which handles this automatically.
```csharp
namespace OECS;
public interface ISystem
{
void RunImpl(World world);
}
```
Systems do **not** declare a query on the interface. Instead, they either
read singletons directly (`world.ReadSingleton<T>()`) or use the
`WorldQueryExtensions` (`Select`, `FindEntity`) with a
`Query<T1..T6>` they build inline. This keeps the interface minimal and
gives systems full flexibility over what they inspect at runtime.
The `Run` extension method wraps `RunImpl` in a batching scope so component
accesses via `GetComponent<T>` and `RefN` are auto-tracked for modification:
```csharp
public static class SystemExtensions
{
public static void Run(this ISystem system, World world);
public static void Run(this ITickedSystem system, World world, Tick tick);
}
```
---
## ITickedSystem
An optional extension for systems that need tick metadata (delta time or
logical tick marker).
```csharp
namespace OECS;
public interface ITickedSystem : ISystem
{
void RunImpl(World world, Tick tick);
}
```
`ITickedSystem` must also implement `ISystem.RunImpl(World)` (typically
delegating to the ticked overload with a default tick). `SystemGroup` checks
each system at runtime and calls the appropriate `Run` extension.
---
## Tick
```csharp
namespace OECS;
public readonly struct Tick
{
public TickType Type { get; }
public float DeltaTime { get; }
public static Tick Timed(float deltaTime);
public static Tick Logical();
}
public enum TickType
{
Timed,
Logical
}
```
---
## SystemGroup
```csharp
namespace OECS;
public class SystemGroup
{
public SystemGroup(World world);
public void Add(ISystem system);
public void Remove(ISystem system);
public void RunTimed(float deltaTime);
public void RunLogical();
public int Count { get; }
}
```
Systems execute in registration order. `SystemGroup` wraps the entire tick in
a batching scope, and each system's `Run` extension adds a nested scope.
Commands are drained, pending mutations are flushed, and changes are posted
after each system and after the full tick.
---
## ICommand
```csharp
namespace OECS;
public interface ICommand
{
void Execute(World world);
}
```
Implementations should be `[MessagePackObject]` structs so they can be
serialized and replayed.
---
## CommandQueue
```csharp
namespace OECS;
public class CommandQueue
{
public void Enqueue<T>(T command) where T : struct, ICommand;
public void ExecuteAll(World world);
public void Clear();
public void ClearErrors();
public int Count { get; }
public IReadOnlyList<Exception> Errors { get; }
}
```
`Enqueue<T>` uses a constrained generic to avoid boxing at the call site.
`ExecuteAll` wraps the drain in a batching scope — mutations are deferred and
component accesses are auto-tracked. Pending mutations are flushed after each
command so chained commands see each other's changes within the same drain cycle.
---
## IInterrupt
```csharp
namespace OECS;
public interface IInterrupt { }
```
Marker interface for interrupt types. Interrupts are issued by systems to
block the next tick until a matching handler command resolves them.
---
## IInterruptHandlerCommand\<TInterrupt\>
```csharp
namespace OECS;
public interface IInterruptHandlerCommand<TInterrupt> : ICommand
where TInterrupt : struct, IInterrupt
{
bool TryResolve(TInterrupt interrupt);
}
```
A command that resolves a pending interrupt. The default `ICommand.Execute`
looks up the pending interrupt, calls `TryResolve`, and resolves the
interrupt if `TryResolve` returns `true`.
Usage:
```csharp
[MessagePackObject]
public struct ConfirmHandler : IInterruptHandlerCommand<ConfirmInterrupt>
{
[Key(0)] public bool Confirmed;
public bool TryResolve(ConfirmInterrupt interrupt)
{
// Do work here. Return false to reject.
return true;
}
}
```
---
## IRelationship
```csharp
namespace OECS;
public interface IRelationship
{
Entity Target { get; }
}
```
---
## Relationship\<TSelf, TTarget\>
A convenience base struct for relationship components. The type parameters are
phantom types that differentiate relationship kinds at the type level, enabling
type-safe queries and reverse lookups.
```csharp
namespace OECS;
[MessagePackObject]
public struct Relationship<TSelf, TTarget> : IRelationship
where TSelf : struct where TTarget : struct
{
[Key(0)] public Entity Target { get; set; }
}
```
---
## EntityChange
```csharp
namespace OECS;
public readonly struct EntityChange : IEquatable<EntityChange>
{
public Entity Entity { get; }
public ChangeKind Kind { get; }
public Type? ComponentType { get; } // null for entity-level changes
}
public enum ChangeKind
{
EntityAdded,
EntityRemoved,
ComponentAdded,
ComponentRemoved,
ComponentModified,
RelationshipReordered
}
```
---
## WorldSerializer
Saves and loads a `World` to/from a MessagePack stream. Components are
serialized by their runtime type using the source-generated `ComponentRegistry`.
```csharp
namespace OECS;
public static class WorldSerializer
{
public static void Save(World world, Stream stream);
public static void Load(World world, Stream stream);
}
```
---
## WorldSnapshot / EntitySnapshot / ComponentEntry
Serialization types used by `WorldSerializer`. These are public but intended
primarily for serialization infrastructure.
```csharp
namespace OECS;
[MessagePackObject]
public class WorldSnapshot
{
[Key(0)] public EntitySnapshot[] Entities { get; set; }
}
[MessagePackObject]
public class EntitySnapshot
{
[Key(0)] public uint Id { get; set; }
[Key(1)] public uint Version { get; set; }
[Key(2)] public ComponentEntry[] Components { get; set; }
[IgnoreMember]
public Entity Entity { get; }
}
[MessagePackObject]
public class ComponentEntry
{
[Key(0)] public string TypeName { get; set; }
[Key(1)] public byte[] Data { get; set; }
}
```
---
## Usage Example
Components and commands should be `public record struct` types with explicit
fields/properties (not positional syntax). See the `writing-games` skill for
the rationale.
```csharp
using OECS;
// Define components
[MessagePackObject]
public record struct Position
{
[Key(0)] public float X;
[Key(1)] public float Y;
}
[MessagePackObject]
public record struct Velocity
{
[Key(0)] public float X;
[Key(1)] public float Y;
}
// Define a system
public class MovementSystem : ITickedSystem
{
public void RunImpl(World world) => RunImpl(world, Tick.Logical());
public void RunImpl(World world, Tick tick)
{
float dt = tick.DeltaTime;
foreach (var it in world.Select<Position, Velocity>())
{
it.Ref1.X += it.Val2.X * dt;
it.Ref1.Y += it.Val2.Y * dt;
}
}
}
// Wire it up
var world = new World();
var group = new SystemGroup(world);
group.Add(new MovementSystem());
// Create entities
var player = world.CreateEntity();
world.AddComponent(player, new Position { X = 0, Y = 0 });
world.AddComponent(player, new Velocity { X = 1, Y = 0 });
// Observe changes via R3
world.ObserveComponentChanges<Position>()
.Subscribe(change => Console.WriteLine($"{change.Entity} moved"));
// Run a tick
group.RunTimed(0.016f); // ~60 FPS
```
---
## Internal Types (not part of public API)
These types are implementation details and may change without notice:
| Type | Purpose |
|---|---|
| `SparseSet<T>` | Dense/sparse array pair for component storage. |
| `ComponentStore` | Registry of `SparseSet<T>` instances by type. |
| `ChangeBuffer` | Accumulates `EntityChange` during system run, posts to R3 subjects. |
| `ChangeSet` | Deduplicated change accumulator within a single batch. |
| `RelationshipIndex` | Reverse lookup from target entity to source entities. |
| `EntityAllocator` | Free-list + bump allocator for entity IDs. |
| `WorldQueryExtensions` | Extension methods with ref struct iterators and entity lookup. |
| `ComponentRegistry` | Source-generated registry of all component types for serialization. |
| `ComponentDescriptor` | Source-generated per-type descriptor with serialize/deserialize callbacks. |