2026-07-18 19:33:05 +08:00
|
|
|
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
|
|
|
|
|
{
|
2026-07-18 22:22:52 +08:00
|
|
|
private readonly QueryDescriptor _query;
|
2026-07-18 19:33:05 +08:00
|
|
|
|
|
|
|
|
public MovementSystem(World world)
|
|
|
|
|
{
|
2026-07-18 22:22:52 +08:00
|
|
|
_query = world.Query().With<Position>().With<Velocity>().Build();
|
2026-07-18 19:33:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Run(World world) => Run(world, Tick.Logical());
|
|
|
|
|
|
|
|
|
|
public void Run(World world, Tick tick)
|
|
|
|
|
{
|
|
|
|
|
float dt = tick.DeltaTime;
|
2026-07-18 22:22:52 +08:00
|
|
|
world.ForEach(_query, (Entity entity, ref Position pos, ref Velocity vel) =>
|
2026-07-18 19:33:05 +08:00
|
|
|
{
|
|
|
|
|
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 bool WasCalled { get; private set; }
|
|
|
|
|
public Tick ReceivedTick { get; private set; }
|
|
|
|
|
|
|
|
|
|
public TestLogicalSystem(World world)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 OrderTrackingSystem(World world, int id, List<int> order)
|
|
|
|
|
{
|
|
|
|
|
_id = id;
|
|
|
|
|
_order = order;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|