oecs-sharp/docs/api-surface.md

12 KiB
Raw Blame 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

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

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 QueryExtensions for Select / FindEntity / FindEntities.

    // --- 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<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;

    // --- 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.

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>(...) where T1 : struct where T2 : struct;
    public static Entity FindEntity<T1, T2, T3>(...) where T1 : struct where T2 : struct where T3 : struct;

    // FindEntities — returns all matching entities as List<Entity>.
    public static List<Entity> FindEntities<T1>(...) where T1 : struct;
    public static List<Entity> FindEntities<T1, T2>(...) where T1 : struct where T2 : struct;
    public static List<Entity> FindEntities<T1, T2, T3>(...) where T1 : struct where T2 : struct where T3 : struct;
}

Each ref struct (Select1 through Select6) exposes:

  • Entity Entity — the current entity handle.
  • ref T1 Item1, ref T2 Item2, ... — read-write references to components.
  • bool MoveNext() — advances and returns true while items remain.
  • GetEnumerator() — returns this for foreach compatibility.
  • Dispose() — ends the iteration scope (flushes deferred mutations).

Usage:

// Simple scan: all entities with Position.
foreach (var it in world.Select<Position>())
{
    it.Item1.X += 1; // ref mutates component in-place
}

// With Without filter:
var query = new Query<Position>().Without<Frozen>();
foreach (var it in world.Select(query))
{
    // it.Entity, it.Item1
}

// Find first entity:
var hand = world.FindEntity<PlayerHand>();

// Multi-component:
foreach (var it in world.Select<Position, Velocity>())
{
    it.Item1.X += it.Item2.X * dt;
}

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.

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:

// 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. It receives the World and decides what to do — typically reading singletons, iterating queries, or enqueuing commands.

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 WorldQueryExtensions (Select, FindEntity, FindEntities) with a Query<T1..T6> they build inline. 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).

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

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

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

namespace OECS;

public interface ICommand
{
    void Execute(World world);
}

Implementations should be [MessagePackObject] structs so they can be serialized and replayed.


CommandQueue

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

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.

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

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.

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.

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.

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 Run(World world) => Run(world, Tick.Logical());

    public void Run(World world, Tick tick)
    {
        float dt = tick.DeltaTime;

        foreach (var it in world.Select<Position, Velocity>())
        {
            it.Item1.X += it.Item2.X * dt;
            it.Item1.Y += it.Item2.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.