oecs-sharp/docs/api-surface.md

549 lines
14 KiB
Markdown
Raw Normal View History

# 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
2026-07-18 21:59:09 +08:00
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
{
2026-07-18 21:59:09 +08:00
// --- Singleton entity ---
public static Entity SingletonEntity { get; } // ID=1, Version=1
// --- 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;
2026-07-18 21:59:09 +08:00
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;
2026-07-18 21:59:09 +08:00
public T ReadSingleton<T>() where T : struct;
public bool HasSingleton<T>() where T : struct;
public void RemoveSingleton<T>() where T : struct;
// --- Queries ---
public QueryBuilder Query();
// --- Query Execution ---
public void ForEach<T1>(
QueryDescriptor query,
2026-07-18 21:59:09 +08:00
ForEachAction<T1> action) where T1 : struct;
// Overloads for 26 component types:
2026-07-18 21:59:09 +08:00
public void ForEach<T1, T2>(QueryDescriptor, ForEachAction<T1, T2>)
where T1 : struct where T2 : struct;
2026-07-18 21:59:09 +08:00
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 ---
public CommandQueue Commands { get; }
public void ExecuteCommands();
// --- Reactivity ---
public void MarkModified<T>(Entity entity) where T : struct;
public void PostChanges();
2026-07-18 21:59:09 +08:00
public Observable<EntityChange> ObserveEntityChanges();
public Observable<EntityChange> ObserveComponentChanges<T>()
where T : struct;
2026-07-18 21:59:09 +08:00
public Observable<EntityChange> ObserveQuery(QueryDescriptor query);
// --- Relationships ---
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
where T : struct, IRelationship;
// --- Cleanup ---
public void Dispose();
}
```
---
2026-07-18 21:59:09 +08:00
## ForEachAction Delegates
Custom delegate types for query iteration with `ref` parameters. The built-in
`Action<...>` delegates do not support `ref` parameters.
```csharp
namespace OECS;
public delegate void ForEachAction<T1>(Entity entity, ref T1 c1) where T1 : struct;
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.
2026-07-18 21:59:09 +08:00
```csharp
namespace OECS;
public static class EntityIterator
{
// With explicit QueryDescriptor (supports Without<T> filters).
2026-07-18 21:59:09 +08:00
public static Select1<T1> Select<T1>(
this World world, QueryDescriptor query) where T1 : struct;
public static Select2<T1, T2> Select<T1, T2>(
this World world, QueryDescriptor query)
where T1 : struct where T2 : struct;
public static Select3<T1, T2, T3> Select<T1, T2, T3>(
this World world, QueryDescriptor query)
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;
2026-07-18 21:59:09 +08:00
}
```
The singletons `CurrentEntity`, `Current1`, `Current2`, `Current3` are
exposed on the ref struct iterators directly. Usage:
2026-07-18 21:59:09 +08:00
```csharp
// Simple scan: all entities with PlayerHand.
using var iter = world.Select<PlayerHand>();
while (iter.MoveNext())
{
Console.WriteLine(iter.CurrentEntity);
}
// With query filter:
var query = world.Query().With<Position>().Without<Frozen>().Build();
using var iter2 = world.Select<Position>(query);
while (iter2.MoveNext())
2026-07-18 21:59:09 +08:00
{
iter2.Current1.X += 1;
2026-07-18 21:59:09 +08:00
}
```
---
## QueryBuilder
```csharp
namespace OECS;
public class QueryBuilder
{
public QueryBuilder With<T>() where T : struct;
public QueryBuilder Without<T>() where T : struct;
public QueryDescriptor Build();
}
```
---
## QueryDescriptor
```csharp
namespace OECS;
public class QueryDescriptor
{
public IReadOnlySet<Type> With { get; }
public IReadOnlySet<Type> Without { get; }
}
```
---
## ISystem
A system is a unit of logic that runs during a tick. It receives the `World`
and decides what to do — typically reading singletons, iterating queries,
or enqueuing commands.
```csharp
namespace OECS;
public interface ISystem
{
void Run(World world);
}
```
Systems do **not** declare a query on the interface. Instead, they either
read singletons directly (`world.ReadSingleton<T>()`) or use the iterator /
`ForEach` API with a `QueryDescriptor` they build internally. This keeps
the interface minimal and gives systems full flexibility over what they
inspect at runtime.
---
## ITickedSystem
An optional extension for systems that need tick metadata (delta time or
logical tick marker).
```csharp
namespace OECS;
public interface ITickedSystem : ISystem
{
void Run(World world, Tick tick);
}
```
`SystemGroup` checks each system at runtime: if it implements
`ITickedSystem`, the `Run(World, Tick)` overload is called; otherwise
`Run(World)` is called. Both overloads must be implemented.
---
## 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
{
2026-07-18 21:59:09 +08:00
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` automatically drains
commands and posts changes 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);
2026-07-18 21:59:09 +08:00
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.
Commands enqueued during `ExecuteAll` are processed in the same drain cycle.
---
## IRelationship
```csharp
namespace OECS;
public interface IRelationship
{
Entity Source { get; }
Entity Target { get; }
}
```
---
2026-07-18 21:59:09 +08:00
## 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
2026-07-18 21:59:09 +08:00
{
[Key(0)] public Entity Source { get; set; }
[Key(1)] public Entity Target { get; set; }
}
```
---
## EntityChange
```csharp
namespace OECS;
2026-07-18 21:59:09 +08:00
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
}
```
---
2026-07-18 21:59:09 +08:00
## WorldSerializer
Saves and loads a `World` to/from a MessagePack stream. Components are
serialized by their runtime type using the source-generated `ComponentRegistry`.
2026-07-18 21:59:09 +08:00
```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]
2026-07-18 21:59:09 +08:00
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
{
private readonly QueryDescriptor _query;
public MovementSystem(World world)
{
_query = world.Query()
.With<Position>()
.With<Velocity>()
.Build();
}
2026-07-18 21:59:09 +08:00
public void Run(World world) => Run(world, Tick.Logical());
public void Run(World world, Tick tick)
{
float dt = tick.DeltaTime;
world.ForEach(_query, (Entity entity, ref Position pos, ref Velocity vel) =>
{
pos.X += vel.X * dt;
pos.Y += vel.Y * dt;
world.MarkModified<Position>(entity);
});
}
}
// Wire it up
var world = new World();
2026-07-18 21:59:09 +08:00
var group = new SystemGroup(world);
group.Add(new MovementSystem(world));
// 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. |
2026-07-18 21:59:09 +08:00
| `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. |
| `QueryExecutor` | Query iteration logic with smallest-set driver optimization. |
| `ComponentRegistry` | Source-generated registry of all component types for serialization. |
| `ComponentDescriptor` | Source-generated per-type descriptor with serialize/deserialize callbacks. |