2026-07-18 19:03:37 +08:00
|
|
|
|
# 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
|
2026-07-18 19:03:37 +08:00
|
|
|
|
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;
|
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;
|
2026-07-18 19:03:37 +08:00
|
|
|
|
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;
|
2026-07-18 19:03:37 +08:00
|
|
|
|
public bool HasSingleton<T>() where T : struct;
|
|
|
|
|
|
public void RemoveSingleton<T>() where T : struct;
|
|
|
|
|
|
|
|
|
|
|
|
// --- Queries ---
|
2026-07-21 00:24:10 +08:00
|
|
|
|
// See QueryExtensions for Select / FindEntity / FindEntities.
|
2026-07-18 19:03:37 +08:00
|
|
|
|
|
|
|
|
|
|
// --- 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>()
|
2026-07-18 19:03:37 +08:00
|
|
|
|
where T : struct;
|
2026-07-21 00:24:10 +08:00
|
|
|
|
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;
|
2026-07-18 19:03:37 +08:00
|
|
|
|
|
|
|
|
|
|
// --- Relationships ---
|
|
|
|
|
|
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
|
|
|
|
|
|
where T : struct, IRelationship;
|
|
|
|
|
|
|
|
|
|
|
|
// --- Cleanup ---
|
|
|
|
|
|
public void Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
## WorldQueryExtensions
|
2026-07-18 21:59:09 +08:00
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
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.
|
2026-07-18 21:59:09 +08:00
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
namespace OECS;
|
|
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
public static class WorldQueryExtensions
|
2026-07-18 21:59:09 +08:00
|
|
|
|
{
|
2026-07-21 00:24:10 +08:00
|
|
|
|
// Select — returns a ref struct enumerator for foreach.
|
2026-07-20 17:44:25 +08:00
|
|
|
|
public static Select1<T1> Select<T1>(
|
2026-07-21 00:24:10 +08:00
|
|
|
|
this World world, Query<T1> query = default) where T1 : struct;
|
|
|
|
|
|
// Overloads for 2–6 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;
|
2026-07-18 21:59:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
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).
|
2026-07-18 21:59:09 +08:00
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
Usage:
|
2026-07-18 21:59:09 +08:00
|
|
|
|
```csharp
|
2026-07-21 00:24:10 +08:00
|
|
|
|
// Simple scan: all entities with Position.
|
|
|
|
|
|
foreach (var it in world.Select<Position>())
|
2026-07-20 17:44:25 +08:00
|
|
|
|
{
|
2026-07-21 00:24:10 +08:00
|
|
|
|
it.Item1.X += 1; // ref mutates component in-place
|
2026-07-20 17:44:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
// With Without filter:
|
|
|
|
|
|
var query = new Query<Position>().Without<Frozen>();
|
|
|
|
|
|
foreach (var it in world.Select(query))
|
2026-07-18 21:59:09 +08:00
|
|
|
|
{
|
2026-07-21 00:24:10 +08:00
|
|
|
|
// it.Entity, it.Item1
|
2026-07-18 21:59:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
// Find first entity:
|
|
|
|
|
|
var hand = world.FindEntity<PlayerHand>();
|
2026-07-18 19:03:37 +08:00
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
// Multi-component:
|
|
|
|
|
|
foreach (var it in world.Select<Position, Velocity>())
|
2026-07-18 19:03:37 +08:00
|
|
|
|
{
|
2026-07-21 00:24:10 +08:00
|
|
|
|
it.Item1.X += it.Item2.X * dt;
|
2026-07-18 19:03:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
## 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.
|
2026-07-18 19:03:37 +08:00
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
namespace OECS;
|
|
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
public struct Query<T1> where T1 : struct
|
2026-07-18 19:03:37 +08:00
|
|
|
|
{
|
2026-07-21 00:24:10 +08:00
|
|
|
|
public Query<T1> Without<W>() where W : struct;
|
2026-07-18 19:03:37 +08:00
|
|
|
|
}
|
2026-07-21 00:24:10 +08:00
|
|
|
|
|
|
|
|
|
|
// Overloads for 2–6 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>()) { ... }
|
2026-07-18 19:03:37 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ISystem
|
|
|
|
|
|
|
2026-07-20 17:44:25 +08:00
|
|
|
|
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.
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
```csharp
|
|
|
|
|
|
namespace OECS;
|
|
|
|
|
|
|
|
|
|
|
|
public interface ISystem
|
|
|
|
|
|
{
|
|
|
|
|
|
void Run(World world);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-20 17:44:25 +08:00
|
|
|
|
Systems do **not** declare a query on the interface. Instead, they either
|
2026-07-21 00:24:10 +08:00
|
|
|
|
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.
|
2026-07-20 17:44:25 +08:00
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ITickedSystem
|
|
|
|
|
|
|
2026-07-20 17:44:25 +08:00
|
|
|
|
An optional extension for systems that need tick metadata (delta time or
|
|
|
|
|
|
logical tick marker).
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
```csharp
|
|
|
|
|
|
namespace OECS;
|
|
|
|
|
|
|
|
|
|
|
|
public interface ITickedSystem : ISystem
|
|
|
|
|
|
{
|
|
|
|
|
|
void Run(World world, Tick tick);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-20 17:44:25 +08:00
|
|
|
|
`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.
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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);
|
2026-07-18 19:03:37 +08:00
|
|
|
|
public void Add(ISystem system);
|
|
|
|
|
|
public void Remove(ISystem system);
|
|
|
|
|
|
public void RunTimed(float deltaTime);
|
|
|
|
|
|
public void RunLogical();
|
|
|
|
|
|
public int Count { get; }
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-20 17:44:25 +08:00
|
|
|
|
Systems execute in registration order. `SystemGroup` automatically drains
|
|
|
|
|
|
commands and posts changes after each system and after the full tick.
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ICommand
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
namespace OECS;
|
|
|
|
|
|
|
|
|
|
|
|
public interface ICommand
|
|
|
|
|
|
{
|
|
|
|
|
|
void Execute(World world);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-20 17:44:25 +08:00
|
|
|
|
Implementations should be `[MessagePackObject]` structs so they can be
|
|
|
|
|
|
serialized and replayed.
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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();
|
2026-07-18 19:03:37 +08:00
|
|
|
|
public int Count { get; }
|
|
|
|
|
|
public IReadOnlyList<Exception> Errors { get; }
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-20 17:44:25 +08:00
|
|
|
|
`Enqueue<T>` uses a constrained generic to avoid boxing at the call site.
|
|
|
|
|
|
Commands enqueued during `ExecuteAll` are processed in the same drain cycle.
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-07-20 17:44:25 +08:00
|
|
|
|
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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
## EntityChange
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
namespace OECS;
|
|
|
|
|
|
|
2026-07-18 21:59:09 +08:00
|
|
|
|
public readonly struct EntityChange : IEquatable<EntityChange>
|
2026-07-18 19:03:37 +08:00
|
|
|
|
{
|
|
|
|
|
|
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
|
2026-07-20 17:44:25 +08:00
|
|
|
|
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; }
|
2026-07-20 17:44:25 +08:00
|
|
|
|
[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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
## Usage Example
|
|
|
|
|
|
|
2026-07-20 18:01:46 +08:00
|
|
|
|
Components and commands should be `public record struct` types with explicit
|
|
|
|
|
|
fields/properties (not positional syntax). See the `writing-games` skill for
|
|
|
|
|
|
the rationale.
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
```csharp
|
|
|
|
|
|
using OECS;
|
|
|
|
|
|
|
|
|
|
|
|
// Define components
|
|
|
|
|
|
[MessagePackObject]
|
2026-07-20 18:01:46 +08:00
|
|
|
|
public record struct Position
|
2026-07-18 19:03:37 +08:00
|
|
|
|
{
|
|
|
|
|
|
[Key(0)] public float X;
|
|
|
|
|
|
[Key(1)] public float Y;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[MessagePackObject]
|
2026-07-20 18:01:46 +08:00
|
|
|
|
public record struct Velocity
|
2026-07-18 19:03:37 +08:00
|
|
|
|
{
|
|
|
|
|
|
[Key(0)] public float X;
|
|
|
|
|
|
[Key(1)] public float Y;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Define a system
|
|
|
|
|
|
public class MovementSystem : ITickedSystem
|
|
|
|
|
|
{
|
2026-07-18 21:59:09 +08:00
|
|
|
|
public void Run(World world) => Run(world, Tick.Logical());
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
public void Run(World world, Tick tick)
|
|
|
|
|
|
{
|
|
|
|
|
|
float dt = tick.DeltaTime;
|
|
|
|
|
|
|
2026-07-21 00:24:10 +08:00
|
|
|
|
foreach (var it in world.Select<Position, Velocity>())
|
2026-07-18 19:03:37 +08:00
|
|
|
|
{
|
2026-07-21 00:24:10 +08:00
|
|
|
|
it.Item1.X += it.Item2.X * dt;
|
|
|
|
|
|
it.Item1.Y += it.Item2.Y * dt;
|
|
|
|
|
|
}
|
2026-07-18 19:03:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Wire it up
|
|
|
|
|
|
var world = new World();
|
2026-07-18 21:59:09 +08:00
|
|
|
|
var group = new SystemGroup(world);
|
2026-07-21 00:24:10 +08:00
|
|
|
|
group.Add(new MovementSystem());
|
2026-07-18 19:03:37 +08:00
|
|
|
|
|
|
|
|
|
|
// Create entities
|
|
|
|
|
|
var player = world.CreateEntity();
|
|
|
|
|
|
world.AddComponent(player, new Position { X = 0, Y = 0 });
|
|
|
|
|
|
world.AddComponent(player, new Velocity { X = 1, Y = 0 });
|
|
|
|
|
|
|
2026-07-20 17:44:25 +08:00
|
|
|
|
// Observe changes via R3
|
|
|
|
|
|
world.ObserveComponentChanges<Position>()
|
|
|
|
|
|
.Subscribe(change => Console.WriteLine($"{change.Entity} moved"));
|
|
|
|
|
|
|
2026-07-18 19:03:37 +08:00
|
|
|
|
// 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. |
|
2026-07-18 19:03:37 +08:00
|
|
|
|
| `RelationshipIndex` | Reverse lookup from target entity to source entities. |
|
|
|
|
|
|
| `EntityAllocator` | Free-list + bump allocator for entity IDs. |
|
2026-07-21 00:24:10 +08:00
|
|
|
|
| `WorldQueryExtensions` | Extension methods with ref struct iterators and entity lookup. |
|
2026-07-20 17:44:25 +08:00
|
|
|
|
| `ComponentRegistry` | Source-generated registry of all component types for serialization. |
|
|
|
|
|
|
| `ComponentDescriptor` | Source-generated per-type descriptor with serialize/deserialize callbacks. |
|