282 lines
9.2 KiB
C#
282 lines
9.2 KiB
C#
using FluentAssertions;
|
|
using R3;
|
|
using Xunit;
|
|
|
|
namespace OECS.Tests;
|
|
|
|
public class ReactivityTests
|
|
{
|
|
private struct Position { public float X; public float Y; }
|
|
private struct Health { public int Value; }
|
|
private struct Frozen { }
|
|
|
|
/// <summary>
|
|
/// Helper that collects changes into a list and exposes an R3 Observer.
|
|
/// </summary>
|
|
private sealed class ChangeCollector : IObserver<EntityChange>
|
|
{
|
|
public List<EntityChange> Changes = new();
|
|
|
|
public void OnNext(EntityChange value) => Changes.Add(value);
|
|
public void OnError(Exception error) { }
|
|
public void OnCompleted() { }
|
|
}
|
|
|
|
[Fact]
|
|
public void EntityCreation_PostsEntityAdded()
|
|
{
|
|
var world = new World();
|
|
var collector = new ChangeCollector();
|
|
using var sub = world.ObserveEntityChanges().Subscribe(collector.ToObserver());
|
|
|
|
var entity = world.CreateEntity();
|
|
world.PostChanges();
|
|
|
|
collector.Changes.Should().ContainSingle()
|
|
.Which.Should().Match<EntityChange>(c =>
|
|
c.Entity == entity && c.Kind == ChangeKind.EntityAdded);
|
|
}
|
|
|
|
[Fact]
|
|
public void EntityDestruction_PostsEntityRemoved()
|
|
{
|
|
var world = new World();
|
|
var entity = world.CreateEntity();
|
|
world.PostChanges(); // Clear pending
|
|
|
|
var collector = new ChangeCollector();
|
|
using var sub = world.ObserveEntityChanges().Subscribe(collector.ToObserver());
|
|
|
|
world.DestroyEntity(entity);
|
|
world.PostChanges();
|
|
|
|
collector.Changes.Should().ContainSingle()
|
|
.Which.Should().Match<EntityChange>(c =>
|
|
c.Entity == entity && c.Kind == ChangeKind.EntityRemoved);
|
|
}
|
|
|
|
[Fact]
|
|
public void ComponentAdd_PostsComponentAdded()
|
|
{
|
|
var world = new World();
|
|
var entity = world.CreateEntity();
|
|
world.PostChanges(); // Clear pending
|
|
|
|
var collector = new ChangeCollector();
|
|
using var sub = world.ObserveComponentChanges<Position>().Subscribe(collector.ToObserver());
|
|
|
|
world.AddComponent(entity, new Position { X = 1, Y = 2 });
|
|
world.PostChanges();
|
|
|
|
collector.Changes.Should().ContainSingle()
|
|
.Which.Should().Match<EntityChange>(c =>
|
|
c.Entity == entity &&
|
|
c.Kind == ChangeKind.ComponentAdded &&
|
|
c.ComponentType == typeof(Position));
|
|
}
|
|
|
|
[Fact]
|
|
public void ComponentRemove_PostsComponentRemoved()
|
|
{
|
|
var world = new World();
|
|
var entity = world.CreateEntity();
|
|
world.AddComponent(entity, new Position { X = 1, Y = 2 });
|
|
world.PostChanges(); // Clear pending
|
|
|
|
var collector = new ChangeCollector();
|
|
using var sub = world.ObserveComponentChanges<Position>().Subscribe(collector.ToObserver());
|
|
|
|
world.RemoveComponent<Position>(entity);
|
|
world.PostChanges();
|
|
|
|
collector.Changes.Should().ContainSingle()
|
|
.Which.Should().Match<EntityChange>(c =>
|
|
c.Entity == entity &&
|
|
c.Kind == ChangeKind.ComponentRemoved &&
|
|
c.ComponentType == typeof(Position));
|
|
}
|
|
|
|
[Fact]
|
|
public void MarkModified_PostsComponentModified()
|
|
{
|
|
var world = new World();
|
|
var entity = world.CreateEntity();
|
|
world.AddComponent(entity, new Position { X = 0, Y = 0 });
|
|
world.PostChanges(); // Clear pending
|
|
|
|
var collector = new ChangeCollector();
|
|
using var sub = world.ObserveComponentChanges<Position>().Subscribe(collector.ToObserver());
|
|
|
|
world.MarkModified<Position>(entity);
|
|
world.PostChanges();
|
|
|
|
collector.Changes.Should().ContainSingle()
|
|
.Which.Should().Match<EntityChange>(c =>
|
|
c.Entity == entity &&
|
|
c.Kind == ChangeKind.ComponentModified &&
|
|
c.ComponentType == typeof(Position));
|
|
}
|
|
|
|
[Fact]
|
|
public void Changes_AreBatchedPerPost()
|
|
{
|
|
var world = new World();
|
|
var collector = new ChangeCollector();
|
|
using var sub = world.ObserveEntityChanges().Subscribe(collector.ToObserver());
|
|
|
|
var a = world.CreateEntity();
|
|
var b = world.CreateEntity();
|
|
world.AddComponent(a, new Position { X = 1, Y = 2 });
|
|
world.AddComponent(b, new Health { Value = 100 });
|
|
|
|
// No Post yet — subscribers should not have received anything.
|
|
collector.Changes.Should().BeEmpty();
|
|
|
|
world.PostChanges();
|
|
|
|
// All changes arrive in one batch.
|
|
collector.Changes.Should().HaveCount(4);
|
|
}
|
|
|
|
[Fact]
|
|
public void Subscribers_ReceiveChangesInOrder()
|
|
{
|
|
var world = new World();
|
|
var collector = new ChangeCollector();
|
|
using var sub = world.ObserveEntityChanges().Subscribe(collector.ToObserver());
|
|
|
|
var entity = world.CreateEntity();
|
|
world.AddComponent(entity, new Position { X = 0, Y = 0 });
|
|
world.MarkModified<Position>(entity);
|
|
world.RemoveComponent<Position>(entity);
|
|
world.DestroyEntity(entity);
|
|
|
|
world.PostChanges();
|
|
|
|
collector.Changes.Select(c => c.Kind).Should().Equal(
|
|
ChangeKind.EntityAdded,
|
|
ChangeKind.ComponentAdded,
|
|
ChangeKind.ComponentModified,
|
|
ChangeKind.ComponentRemoved,
|
|
ChangeKind.EntityRemoved
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void DisposingSubscription_StopsNotifications()
|
|
{
|
|
var world = new World();
|
|
var collector = new ChangeCollector();
|
|
var sub = world.ObserveEntityChanges().Subscribe(collector.ToObserver());
|
|
|
|
var entity = world.CreateEntity();
|
|
world.PostChanges();
|
|
collector.Changes.Should().HaveCount(1);
|
|
|
|
sub.Dispose();
|
|
|
|
world.DestroyEntity(entity);
|
|
world.PostChanges();
|
|
|
|
// No new changes after dispose.
|
|
collector.Changes.Should().HaveCount(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void ObserveComponentChanges_OnlyReceivesMatchingType()
|
|
{
|
|
var world = new World();
|
|
var posCollector = new ChangeCollector();
|
|
var hpCollector = new ChangeCollector();
|
|
|
|
using var sub1 = world.ObserveComponentChanges<Position>().Subscribe(posCollector.ToObserver());
|
|
using var sub2 = world.ObserveComponentChanges<Health>().Subscribe(hpCollector.ToObserver());
|
|
|
|
var entity = world.CreateEntity();
|
|
world.AddComponent(entity, new Position { X = 1, Y = 2 });
|
|
world.AddComponent(entity, new Health { Value = 100 });
|
|
world.PostChanges();
|
|
|
|
posCollector.Changes.Should().HaveCount(1);
|
|
posCollector.Changes[0].ComponentType.Should().Be(typeof(Position));
|
|
|
|
hpCollector.Changes.Should().HaveCount(1);
|
|
hpCollector.Changes[0].ComponentType.Should().Be(typeof(Health));
|
|
}
|
|
|
|
[Fact]
|
|
public void ObserveQuery_OnlyReceivesMatchingChanges()
|
|
{
|
|
var world = new World();
|
|
var query = world.Query().With<Position>().Without<Frozen>().Build();
|
|
var collector = new ChangeCollector();
|
|
|
|
using var sub = world.ObserveQuery(query).Subscribe(collector.ToObserver());
|
|
|
|
var entity = world.CreateEntity();
|
|
world.AddComponent(entity, new Position { X = 1, Y = 2 }); // Should match
|
|
world.AddComponent(entity, new Frozen()); // Should NOT match (not in With)
|
|
world.AddComponent(entity, new Health { Value = 100 }); // Should NOT match (not in With)
|
|
world.PostChanges();
|
|
|
|
collector.Changes.Should().HaveCount(1);
|
|
collector.Changes[0].ComponentType.Should().Be(typeof(Position));
|
|
}
|
|
|
|
[Fact]
|
|
public void Deduplication_PreventsDuplicateChanges()
|
|
{
|
|
var world = new World();
|
|
var collector = new ChangeCollector();
|
|
using var sub = world.ObserveComponentChanges<Position>().Subscribe(collector.ToObserver());
|
|
|
|
var entity = world.CreateEntity();
|
|
world.AddComponent(entity, new Position { X = 0, Y = 0 });
|
|
world.MarkModified<Position>(entity);
|
|
world.MarkModified<Position>(entity); // Duplicate
|
|
world.MarkModified<Position>(entity); // Duplicate
|
|
world.PostChanges();
|
|
|
|
// Should have exactly 2 changes: ComponentAdded + ComponentModified (deduplicated).
|
|
collector.Changes.Should().HaveCount(2);
|
|
collector.Changes[0].Kind.Should().Be(ChangeKind.ComponentAdded);
|
|
collector.Changes[1].Kind.Should().Be(ChangeKind.ComponentModified);
|
|
}
|
|
|
|
[Fact]
|
|
public void Changes_ArePostedAfterEachSystem_InSystemGroup()
|
|
{
|
|
var world = new World();
|
|
var group = new SystemGroup(world);
|
|
|
|
var collector = new ChangeCollector();
|
|
using var sub = world.ObserveEntityChanges().Subscribe(collector.ToObserver());
|
|
|
|
// System that creates an entity.
|
|
group.Add(new EntityCreatorSystem(world));
|
|
|
|
// Before running, no changes should be posted.
|
|
collector.Changes.Should().BeEmpty();
|
|
|
|
group.RunLogical();
|
|
|
|
// After the system group runs, changes should be posted.
|
|
collector.Changes.Should().Contain(c => c.Kind == ChangeKind.EntityAdded);
|
|
}
|
|
|
|
private class EntityCreatorSystem : ISystem
|
|
{
|
|
private readonly World _world;
|
|
|
|
public EntityCreatorSystem(World world)
|
|
{
|
|
_world = world;
|
|
}
|
|
|
|
public void Run(World world)
|
|
{
|
|
_world.CreateEntity();
|
|
}
|
|
}
|
|
}
|