feat: add query system and system execution logic

Introduces a complete query system for iterating over entities with
specific component combinations, including support for "with" and
"without" filters.

Also adds:
- `ISystem` and `ITickedSystem` interfaces for defining logic.
- `SystemGroup` for managing and running groups of systems.
- `QueryBuilder` for a fluent API to construct queries.
- `QueryExecutor` for high-performance iteration using ref parameters.
- `Tick` and `TickType` to support timed and logical updates.
This commit is contained in:
hypercross 2026-07-18 19:33:05 +08:00
parent 6739cf4ac9
commit e4afaafd02
11 changed files with 890 additions and 0 deletions

39
src/OECS/ForEachAction.cs Normal file
View File

@ -0,0 +1,39 @@
namespace OECS;
// Custom delegate types for query iteration with ref parameters.
// The built-in Action<...> delegates do not support ref parameters.
/// <summary>
/// Callback for iterating entities with one component type.
/// </summary>
public delegate void ForEachAction<T1>(Entity entity, ref T1 c1) where T1 : struct;
/// <summary>
/// Callback for iterating entities with two component types.
/// </summary>
public delegate void ForEachAction<T1, T2>(Entity entity, ref T1 c1, ref T2 c2)
where T1 : struct where T2 : struct;
/// <summary>
/// Callback for iterating entities with three component types.
/// </summary>
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;
/// <summary>
/// Callback for iterating entities with four component types.
/// </summary>
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;
/// <summary>
/// Callback for iterating entities with five component types.
/// </summary>
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;
/// <summary>
/// Callback for iterating entities with six component types.
/// </summary>
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;

18
src/OECS/ISystem.cs Normal file
View File

@ -0,0 +1,18 @@
namespace OECS;
/// <summary>
/// A system that runs on every tick.
/// Systems declare a query and receive the world in their <see cref="Run"/> method.
/// </summary>
public interface ISystem
{
/// <summary>
/// The query that defines which entities this system operates on.
/// </summary>
QueryDescriptor Query { get; }
/// <summary>
/// Executes the system logic for this tick.
/// </summary>
void Run(World world);
}

13
src/OECS/ITickedSystem.cs Normal file
View File

@ -0,0 +1,13 @@
namespace OECS;
/// <summary>
/// A system that receives tick data (delta time or logical tick) in addition
/// to the world reference.
/// </summary>
public interface ITickedSystem : ISystem
{
/// <summary>
/// Executes the system logic with tick information.
/// </summary>
void Run(World world, Tick tick);
}

39
src/OECS/QueryBuilder.cs Normal file
View File

@ -0,0 +1,39 @@
namespace OECS;
/// <summary>
/// Fluent builder for constructing <see cref="QueryDescriptor"/> instances.
/// Returned by <see cref="World.Query"/>.
/// </summary>
public class QueryBuilder
{
private readonly HashSet<Type> _with = new();
private readonly HashSet<Type> _without = new();
/// <summary>
/// Requires entities to have a component of type <typeparamref name="T"/>.
/// </summary>
public QueryBuilder With<T>() where T : struct
{
_with.Add(typeof(T));
return this;
}
/// <summary>
/// Excludes entities that have a component of type <typeparamref name="T"/>.
/// </summary>
public QueryBuilder Without<T>() where T : struct
{
_without.Add(typeof(T));
return this;
}
/// <summary>
/// Builds the query descriptor.
/// </summary>
public QueryDescriptor Build()
{
return new QueryDescriptor(
new HashSet<Type>(_with),
new HashSet<Type>(_without));
}
}

View File

@ -0,0 +1,30 @@
namespace OECS;
/// <summary>
/// Describes a query over the ECS world.
/// Composed of a set of required component types ("with") and
/// a set of excluded component types ("without").
/// </summary>
public class QueryDescriptor
{
/// <summary>
/// Component types that must be present on matching entities.
/// </summary>
public IReadOnlySet<Type> With { get; }
/// <summary>
/// Component types that must NOT be present on matching entities.
/// </summary>
public IReadOnlySet<Type> Without { get; }
internal QueryDescriptor(HashSet<Type> with, HashSet<Type> without)
{
With = with;
Without = without;
}
/// <summary>
/// Returns true if this query has no "with" components (matches nothing).
/// </summary>
internal bool IsEmpty => With.Count == 0;
}

242
src/OECS/QueryExecutor.cs Normal file
View File

@ -0,0 +1,242 @@
using System.Runtime.CompilerServices;
namespace OECS;
/// <summary>
/// Provides query execution logic for <see cref="World"/>.
/// Iterates one sparse set as the driver and probes the remaining sets
/// for membership. The "without" filter is checked after all "with" probes.
/// </summary>
internal static class QueryExecutor
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool PassesWithoutFilter(ComponentStore store, Entity entity, IReadOnlySet<Type> withoutTypes)
{
foreach (var type in withoutTypes)
{
var set = store.GetSet(type);
if (set != null && set.Contains(entity))
return false;
}
return true;
}
// ── ForEach overloads ──────────────────────────────────────────────
public static void ForEach<T1>(
ComponentStore store,
QueryDescriptor query,
ForEachAction<T1> action)
where T1 : struct
{
var set = store.GetSet(typeof(T1)) as SparseSet<T1>;
if (set == null) return;
var dense = set.Dense;
var denseEntities = set.DenseEntities;
var count = set.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (!PassesWithoutFilter(store, entity, query.Without))
continue;
action(entity, ref dense[i]);
}
}
public static void ForEach<T1, T2>(
ComponentStore store,
QueryDescriptor query,
ForEachAction<T1, T2> action)
where T1 : struct where T2 : struct
{
var set1 = store.GetSet(typeof(T1)) as SparseSet<T1>;
if (set1 == null) return;
var set2 = store.GetSet(typeof(T2)) as SparseSet<T2>;
if (set2 == null) return;
if (set1.Count <= set2.Count)
{
IterateTwo(set1, set2, store, query.Without, action);
}
else
{
IterateTwoSwapped(set2, set1, store, query.Without, action);
}
}
private static void IterateTwo<TDriver, TOther>(
SparseSet<TDriver> driveSet,
SparseSet<TOther> otherSet,
ComponentStore store,
IReadOnlySet<Type> withoutTypes,
ForEachAction<TDriver, TOther> action)
where TDriver : struct where TOther : struct
{
var dense = driveSet.Dense;
var denseEntities = driveSet.DenseEntities;
var count = driveSet.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (!otherSet.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, withoutTypes))
continue;
action(entity, ref dense[i], ref otherSet.Get(entity));
}
}
private static void IterateTwoSwapped<TOther, TDriver>(
SparseSet<TDriver> driveSet,
SparseSet<TOther> otherSet,
ComponentStore store,
IReadOnlySet<Type> withoutTypes,
ForEachAction<TOther, TDriver> action)
where TDriver : struct where TOther : struct
{
var dense = driveSet.Dense;
var denseEntities = driveSet.DenseEntities;
var count = driveSet.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (!otherSet.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, withoutTypes))
continue;
action(entity, ref otherSet.Get(entity), ref dense[i]);
}
}
public static void ForEach<T1, T2, T3>(
ComponentStore store,
QueryDescriptor query,
ForEachAction<T1, T2, T3> action)
where T1 : struct where T2 : struct where T3 : struct
{
var set1 = store.GetSet(typeof(T1)) as SparseSet<T1>;
if (set1 == null) return;
var set2 = store.GetSet(typeof(T2)) as SparseSet<T2>;
if (set2 == null) return;
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
if (set3 == null) return;
var dense = set1.Dense;
var denseEntities = set1.DenseEntities;
var count = set1.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (!set2.Contains(entity)) continue;
if (!set3.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without))
continue;
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity));
}
}
public static void ForEach<T1, T2, T3, T4>(
ComponentStore store,
QueryDescriptor query,
ForEachAction<T1, T2, T3, T4> action)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
{
var set1 = store.GetSet(typeof(T1)) as SparseSet<T1>;
if (set1 == null) return;
var set2 = store.GetSet(typeof(T2)) as SparseSet<T2>;
if (set2 == null) return;
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
if (set3 == null) return;
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
if (set4 == null) return;
var dense = set1.Dense;
var denseEntities = set1.DenseEntities;
var count = set1.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (!set2.Contains(entity)) continue;
if (!set3.Contains(entity)) continue;
if (!set4.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without))
continue;
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity));
}
}
public static void ForEach<T1, T2, T3, T4, T5>(
ComponentStore store,
QueryDescriptor query,
ForEachAction<T1, T2, T3, T4, T5> action)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
{
var set1 = store.GetSet(typeof(T1)) as SparseSet<T1>;
if (set1 == null) return;
var set2 = store.GetSet(typeof(T2)) as SparseSet<T2>;
if (set2 == null) return;
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
if (set3 == null) return;
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
if (set4 == null) return;
var set5 = store.GetSet(typeof(T5)) as SparseSet<T5>;
if (set5 == null) return;
var dense = set1.Dense;
var denseEntities = set1.DenseEntities;
var count = set1.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (!set2.Contains(entity)) continue;
if (!set3.Contains(entity)) continue;
if (!set4.Contains(entity)) continue;
if (!set5.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without))
continue;
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity));
}
}
public static void ForEach<T1, T2, T3, T4, T5, T6>(
ComponentStore store,
QueryDescriptor query,
ForEachAction<T1, T2, T3, T4, T5, T6> action)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct
{
var set1 = store.GetSet(typeof(T1)) as SparseSet<T1>;
if (set1 == null) return;
var set2 = store.GetSet(typeof(T2)) as SparseSet<T2>;
if (set2 == null) return;
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
if (set3 == null) return;
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
if (set4 == null) return;
var set5 = store.GetSet(typeof(T5)) as SparseSet<T5>;
if (set5 == null) return;
var set6 = store.GetSet(typeof(T6)) as SparseSet<T6>;
if (set6 == null) return;
var dense = set1.Dense;
var denseEntities = set1.DenseEntities;
var count = set1.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (!set2.Contains(entity)) continue;
if (!set3.Contains(entity)) continue;
if (!set4.Contains(entity)) continue;
if (!set5.Contains(entity)) continue;
if (!set6.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without))
continue;
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
}
}
}

View File

@ -9,6 +9,7 @@ namespace OECS;
internal interface ISparseSet internal interface ISparseSet
{ {
void Remove(Entity entity); void Remove(Entity entity);
bool Contains(Entity entity);
int Count { get; } int Count { get; }
} }

73
src/OECS/SystemGroup.cs Normal file
View File

@ -0,0 +1,73 @@
namespace OECS;
/// <summary>
/// Manages a group of systems, running them in registration order
/// against a specific <see cref="World"/>.
/// </summary>
public class SystemGroup
{
private readonly World _world;
private readonly List<ISystem> _systems = new();
/// <summary>
/// Creates a system group bound to the given world.
/// </summary>
public SystemGroup(World world)
{
_world = world ?? throw new ArgumentNullException(nameof(world));
}
/// <summary>
/// The number of systems in this group.
/// </summary>
public int Count => _systems.Count;
/// <summary>
/// Adds a system to the group. Systems execute in the order they are added.
/// </summary>
public void Add(ISystem system)
{
_systems.Add(system);
}
/// <summary>
/// Removes a system from the group.
/// </summary>
public void Remove(ISystem system)
{
_systems.Remove(system);
}
/// <summary>
/// Runs all systems with a timed tick.
/// Systems that implement <see cref="ITickedSystem"/> receive the tick data;
/// plain <see cref="ISystem"/> implementations receive only the world.
/// </summary>
public void RunTimed(float deltaTime)
{
RunAll(Tick.Timed(deltaTime));
}
/// <summary>
/// Runs all systems with a logical tick.
/// </summary>
public void RunLogical()
{
RunAll(Tick.Logical());
}
private void RunAll(Tick tick)
{
foreach (var system in _systems)
{
if (system is ITickedSystem ticked)
{
ticked.Run(_world, tick);
}
else
{
system.Run(_world);
}
}
}
}

50
src/OECS/Tick.cs Normal file
View File

@ -0,0 +1,50 @@
namespace OECS;
/// <summary>
/// Describes the kind of tick being processed.
/// </summary>
public enum TickType
{
/// <summary>
/// A timed tick, carrying a delta time (e.g., frame-based update).
/// </summary>
Timed,
/// <summary>
/// A logical tick, carrying no delta time (e.g., fixed-step simulation).
/// </summary>
Logical
}
/// <summary>
/// Data passed to systems during a tick.
/// </summary>
public readonly struct Tick
{
/// <summary>
/// The kind of tick.
/// </summary>
public TickType Type { get; }
/// <summary>
/// The elapsed time since the last tick, in seconds.
/// Always 0 for logical ticks.
/// </summary>
public float DeltaTime { get; }
private Tick(TickType type, float deltaTime)
{
Type = type;
DeltaTime = deltaTime;
}
/// <summary>
/// Creates a timed tick with the given delta time.
/// </summary>
public static Tick Timed(float deltaTime) => new(TickType.Timed, deltaTime);
/// <summary>
/// Creates a logical tick (no delta time).
/// </summary>
public static Tick Logical() => new(TickType.Logical, 0f);
}

View File

@ -89,6 +89,85 @@ public class World : IDisposable
return _components.Has<T>(entity); return _components.Has<T>(entity);
} }
// ── Queries ──────────────────────────────────────────────────────
/// <summary>
/// Returns a new <see cref="QueryBuilder"/> for constructing queries.
/// </summary>
public QueryBuilder Query() => new();
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// one component type.
/// </summary>
public void ForEach<T1>(
QueryDescriptor query,
ForEachAction<T1> action)
where T1 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// two component types.
/// </summary>
public void ForEach<T1, T2>(
QueryDescriptor query,
ForEachAction<T1, T2> action)
where T1 : struct where T2 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// three component types.
/// </summary>
public void ForEach<T1, T2, T3>(
QueryDescriptor query,
ForEachAction<T1, T2, T3> action)
where T1 : struct where T2 : struct where T3 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// four component types.
/// </summary>
public void ForEach<T1, T2, T3, T4>(
QueryDescriptor query,
ForEachAction<T1, T2, T3, T4> action)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// five component types.
/// </summary>
public void ForEach<T1, T2, T3, T4, T5>(
QueryDescriptor query,
ForEachAction<T1, T2, T3, T4, T5> action)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// six component types.
/// </summary>
public void ForEach<T1, T2, T3, T4, T5, T6>(
QueryDescriptor query,
ForEachAction<T1, T2, T3, T4, T5, T6> action)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
// ── Internal Access ─────────────────────────────────────────────── // ── Internal Access ───────────────────────────────────────────────
/// <summary> /// <summary>

View File

@ -0,0 +1,306 @@
using FluentAssertions;
using Xunit;
namespace OECS.Tests;
public class QueryTests
{
private struct Position { public float X; public float Y; }
private struct Velocity { public float X; public float Y; }
private struct Health { public int Value; }
private struct Frozen { }
[Fact]
public void Query_WithSingleComponent_ReturnsMatchingEntities()
{
var world = new World();
var a = world.CreateEntity();
var b = world.CreateEntity();
var c = world.CreateEntity();
world.AddComponent(a, new Position { X = 1, Y = 2 });
world.AddComponent(b, new Position { X = 3, Y = 4 });
// c has no Position
var query = world.Query().With<Position>().Build();
var results = new List<Entity>();
world.ForEach(query, (Entity entity, ref Position pos) =>
{
results.Add(entity);
});
results.Should().BeEquivalentTo([a, b]);
}
[Fact]
public void Query_WithMultipleComponents_ReturnsIntersection()
{
var world = new World();
var a = world.CreateEntity();
var b = world.CreateEntity();
var c = world.CreateEntity();
world.AddComponent(a, new Position { X = 1, Y = 2 });
world.AddComponent(a, new Velocity { X = 1, Y = 0 });
world.AddComponent(b, new Position { X = 3, Y = 4 });
// b has no Velocity
world.AddComponent(c, new Velocity { X = 0, Y = 1 });
// c has no Position
var query = world.Query().With<Position>().With<Velocity>().Build();
var results = new List<Entity>();
world.ForEach(query, (Entity entity, ref Position pos, ref Velocity vel) =>
{
results.Add(entity);
});
results.Should().BeEquivalentTo([a]);
}
[Fact]
public void Query_Without_ExcludesEntities()
{
var world = new World();
var a = world.CreateEntity();
var b = world.CreateEntity();
world.AddComponent(a, new Position { X = 1, Y = 2 });
world.AddComponent(b, new Position { X = 3, Y = 4 });
world.AddComponent(b, new Frozen());
var query = world.Query().With<Position>().Without<Frozen>().Build();
var results = new List<Entity>();
world.ForEach(query, (Entity entity, ref Position pos) =>
{
results.Add(entity);
});
results.Should().BeEquivalentTo([a]);
}
[Fact]
public void Query_EmptyResult_WhenNoEntitiesMatch()
{
var world = new World();
var query = world.Query().With<Position>().Build();
var count = 0;
world.ForEach(query, (Entity entity, ref Position pos) =>
{
count++;
});
count.Should().Be(0);
}
[Fact]
public void Query_RefMutation_IsVisible()
{
var world = new World();
var entity = world.CreateEntity();
world.AddComponent(entity, new Position { X = 0, Y = 0 });
var query = world.Query().With<Position>().Build();
world.ForEach(query, (Entity e, ref Position pos) =>
{
pos.X = 42;
});
ref var pos = ref world.GetComponent<Position>(entity);
pos.X.Should().Be(42);
}
[Fact]
public void Query_DestroyedEntities_DoNotAppear()
{
var world = new World();
var a = world.CreateEntity();
var b = world.CreateEntity();
world.AddComponent(a, new Position { X = 1, Y = 2 });
world.AddComponent(b, new Position { X = 3, Y = 4 });
world.DestroyEntity(a);
var query = world.Query().With<Position>().Build();
var results = new List<Entity>();
world.ForEach(query, (Entity entity, ref Position pos) =>
{
results.Add(entity);
});
results.Should().BeEquivalentTo([b]);
}
[Fact]
public void Query_ThreeComponents_Works()
{
var world = new World();
var entity = world.CreateEntity();
world.AddComponent(entity, new Position { X = 1, Y = 2 });
world.AddComponent(entity, new Velocity { X = 3, Y = 4 });
world.AddComponent(entity, new Health { Value = 100 });
var query = world.Query()
.With<Position>()
.With<Velocity>()
.With<Health>()
.Build();
var count = 0;
world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel, ref Health hp) =>
{
count++;
pos.X.Should().Be(1);
vel.Y.Should().Be(4);
hp.Value.Should().Be(100);
});
count.Should().Be(1);
}
}
public class SystemTests
{
private struct Position { public float X; public float Y; }
private struct Velocity { public float X; public float Y; }
private class MovementSystem : ITickedSystem
{
public QueryDescriptor Query { get; }
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;
});
}
}
[Fact]
public void System_RunTimed_UpdatesComponents()
{
var world = new World();
var group = new SystemGroup(world);
var system = new MovementSystem(world);
group.Add(system);
var entity = world.CreateEntity();
world.AddComponent(entity, new Position { X = 0, Y = 0 });
world.AddComponent(entity, new Velocity { X = 1, Y = 2 });
group.RunTimed(0.5f);
ref var pos = ref world.GetComponent<Position>(entity);
pos.X.Should().Be(0.5f);
pos.Y.Should().Be(1.0f);
}
[Fact]
public void System_RunLogical_DoesNotUseDeltaTime()
{
var world = new World();
var group = new SystemGroup(world);
var system = new TestLogicalSystem(world);
group.Add(system);
group.RunLogical();
system.WasCalled.Should().BeTrue();
system.ReceivedTick.Type.Should().Be(TickType.Logical);
system.ReceivedTick.DeltaTime.Should().Be(0);
}
private class TestLogicalSystem : ITickedSystem
{
public QueryDescriptor Query { get; }
public bool WasCalled { get; private set; }
public Tick ReceivedTick { get; private set; }
public TestLogicalSystem(World world)
{
Query = world.Query().With<Position>().Build();
}
public void Run(World world) => Run(world, Tick.Logical());
public void Run(World world, Tick tick)
{
WasCalled = true;
ReceivedTick = tick;
}
}
[Fact]
public void Systems_RunInRegistrationOrder()
{
var world = new World();
var group = new SystemGroup(world);
var order = new List<int>();
group.Add(new OrderTrackingSystem(world, 1, order));
group.Add(new OrderTrackingSystem(world, 2, order));
group.Add(new OrderTrackingSystem(world, 3, order));
group.RunLogical();
order.Should().BeInAscendingOrder();
order.Should().BeEquivalentTo([1, 2, 3]);
}
private class OrderTrackingSystem : ISystem
{
private readonly int _id;
private readonly List<int> _order;
public QueryDescriptor Query { get; }
public OrderTrackingSystem(World world, int id, List<int> order)
{
_id = id;
_order = order;
Query = world.Query().With<Position>().Build();
}
public void Run(World world)
{
_order.Add(_id);
}
}
[Fact]
public void System_CanBeRemoved()
{
var world = new World();
var group = new SystemGroup(world);
var system = new TestLogicalSystem(world);
group.Add(system);
group.Count.Should().Be(1);
group.Remove(system);
group.Count.Should().Be(0);
group.RunLogical();
system.WasCalled.Should().BeFalse();
}
}