oecs-sharp/docs/api-surface.md

545 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
{
// --- 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;
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 ---
public QueryBuilder Query();
// --- Query Execution ---
public void ForEach<T1>(
QueryDescriptor query,
ForEachAction<T1> action) where T1 : struct;
// Overloads for 26 component types:
public void ForEach<T1, T2>(QueryDescriptor, ForEachAction<T1, T2>)
where T1 : struct where T2 : struct;
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();
public Observable<EntityChange> ObserveEntityChanges();
public Observable<EntityChange> ObserveComponentChanges<T>()
where T : struct;
public Observable<EntityChange> ObserveQuery(QueryDescriptor query);
// --- Relationships ---
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
where T : struct, IRelationship;
// --- Cleanup ---
public void Dispose();
}
```
---
## 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.
```csharp
namespace OECS;
public static class EntityIterator
{
// With explicit QueryDescriptor (supports Without<T> filters).
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;
}
```
The singletons `CurrentEntity`, `Current1`, `Current2`, `Current3` are
exposed on the ref struct iterators directly. Usage:
```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())
{
iter2.Current1.X += 1;
}
```
---
## 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
{
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);
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; }
}
```
---
## 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 Source { get; set; }
[Key(1)] 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
}
```
---
## 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
```csharp
using OECS;
// Define components
[MessagePackObject]
public struct Position
{
[Key(0)] public float X;
[Key(1)] public float Y;
}
[MessagePackObject]
public 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();
}
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();
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. |
| `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. |