refactor: replace ForEach with Select iterators

Replace the delegate-based ForEach pattern with zero-allocation ref
struct Select iterators. This change moves iteration logic from
QueryExecutor to WorldQueryExtensions and introduces a new fluent
Query<T> API.

Other changes:
- Update singleton handling to use a dictionary instead of a reserved
  ID.
- Add IsSingleton flag to ComponentDescriptor.
- Replace QueryDescriptor with type-safe Query<T> structs.
- Update tests to use the new Select API.
This commit is contained in:
hypercross 2026-07-21 00:21:01 +08:00
parent 91c6f4aa2c
commit 737136e2ef
18 changed files with 1066 additions and 1515 deletions

View File

@ -114,13 +114,11 @@ public class CommandTests
world.ExecuteCommands();
var query = world.Query().With<TestPosition>().With<TestHealth>().Build();
var positions = new List<(float X, float Y, int Health)>();
world.ForEach(query, (Entity e, ref TestPosition pos, ref TestHealth hp) =>
foreach (var it in world.Select<TestPosition, TestHealth>())
{
positions.Add((pos.X, pos.Y, hp.Value));
});
positions.Add((it.Item1.X, it.Item1.Y, it.Item2.Value));
}
positions.Should().BeEquivalentTo([
(1, 2, 100),
@ -151,13 +149,11 @@ public class CommandTests
world.Commands.Enqueue(new ChainedCommand { X = 0, Y = 0 });
world.ExecuteCommands();
var query = world.Query().With<TestPosition>().Build();
var positions = new List<(float X, float Y)>();
world.ForEach(query, (Entity e, ref TestPosition pos) =>
foreach (var it in world.Select<TestPosition>())
{
positions.Add((pos.X, pos.Y));
});
positions.Add((it.Item1.X, it.Item1.Y));
}
positions.Should().BeEquivalentTo([(10, 20)]);
}
@ -173,9 +169,9 @@ public class CommandTests
world.ExecuteCommands();
var query = world.Query().With<TestPosition>().Build();
var count = 0;
world.ForEach(query, (Entity e, ref TestPosition pos) => count++);
foreach (var it in world.Select<TestPosition>())
count++;
count.Should().Be(2);
world.Commands.Errors.Should().HaveCount(1);
@ -222,9 +218,9 @@ public class CommandTests
world.ExecuteCommands();
var query = world.Query().With<TestPosition>().Build();
var count = 0;
world.ForEach(query, (Entity e, ref TestPosition pos) => count++);
foreach (var it in world.Select<TestPosition>())
count++;
count.Should().Be(0);
}
@ -236,7 +232,7 @@ public class CommandTests
group.Add(new CommandEnqueueSystem(world, new SpawnCommand { X = 1, Y = 2, Health = 100 }));
var observer = new EntityCountObserver(world);
var observer = new EntityCountObserver();
group.Add(observer);
group.RunLogical();
@ -263,20 +259,12 @@ public class CommandTests
private class EntityCountObserver : ISystem
{
private readonly QueryDescriptor _query;
public int SeenCount { get; private set; }
public EntityCountObserver(World world)
{
_query = world.Query().With<TestPosition>().Build();
}
public void Run(World world)
{
world.ForEach(_query, (Entity e, ref TestPosition pos) =>
{
foreach (var it in world.Select<TestPosition>())
SeenCount++;
});
}
}
}

View File

@ -153,45 +153,6 @@ public class EdgeCaseTests
c.Kind == ChangeKind.EntityRemoved && c.Entity == entity);
}
// ── QueryDescriptor equality ─────────────────────────────────────
[Fact]
public void QueryDescriptor_EqualQueries_AreEqual()
{
var q1 = new QueryBuilder().With<Position>().With<Velocity>().Without<Frozen>().Build();
var q2 = new QueryBuilder().With<Position>().With<Velocity>().Without<Frozen>().Build();
q1.Should().Be(q2);
q1.GetHashCode().Should().Be(q2.GetHashCode());
}
[Fact]
public void QueryDescriptor_DifferentWith_AreNotEqual()
{
var q1 = new QueryBuilder().With<Position>().Build();
var q2 = new QueryBuilder().With<Velocity>().Build();
q1.Should().NotBe(q2);
}
[Fact]
public void QueryDescriptor_DifferentWithout_AreNotEqual()
{
var q1 = new QueryBuilder().With<Position>().Without<Frozen>().Build();
var q2 = new QueryBuilder().With<Position>().Without<Burning>().Build();
q1.Should().NotBe(q2);
}
[Fact]
public void QueryDescriptor_DifferentCount_AreNotEqual()
{
var q1 = new QueryBuilder().With<Position>().Build();
var q2 = new QueryBuilder().With<Position>().With<Velocity>().Build();
q1.Should().NotBe(q2);
}
// ── Query with 4, 5, 6 components ────────────────────────────────
[Fact]
@ -204,18 +165,14 @@ public class EdgeCaseTests
world.AddComponent(entity, new Health { Value = 100 });
world.AddComponent(entity, new Frozen());
var query = world.Query()
.With<Position>().With<Velocity>().With<Health>().With<Frozen>()
.Build();
var count = 0;
world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel, ref Health hp, ref Frozen f) =>
foreach (var it in world.Select<Position, Velocity, Health, Frozen>())
{
count++;
pos.X.Should().Be(1);
vel.Y.Should().Be(4);
hp.Value.Should().Be(100);
});
it.Item1.X.Should().Be(1);
it.Item2.Y.Should().Be(4);
it.Item3.Value.Should().Be(100);
}
count.Should().Be(1);
}
@ -230,15 +187,11 @@ public class EdgeCaseTests
world.AddComponent(entity, new Frozen());
world.AddComponent(entity, new Burning());
var query = world.Query()
.With<Position>().With<Velocity>().With<Health>().With<Frozen>().With<Burning>()
.Build();
var count = 0;
world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel, ref Health hp, ref Frozen f, ref Burning b) =>
foreach (var it in world.Select<Position, Velocity, Health, Frozen, Burning>())
{
count++;
});
}
count.Should().Be(1);
}
@ -254,17 +207,11 @@ public class EdgeCaseTests
world.AddComponent(entity, new Burning());
world.AddComponent(entity, new Poisoned());
var query = world.Query()
.With<Position>().With<Velocity>().With<Health>()
.With<Frozen>().With<Burning>().With<Poisoned>()
.Build();
var count = 0;
world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel, ref Health hp,
ref Frozen f, ref Burning b, ref Poisoned p) =>
foreach (var it in world.Select<Position, Velocity, Health, Frozen, Burning, Poisoned>())
{
count++;
});
}
count.Should().Be(1);
}
@ -282,13 +229,13 @@ public class EdgeCaseTests
world.AddComponent(b, new Frozen());
// Without<Frozen> filters out entity b which has Frozen.
var query = world.Query().Without<Frozen>().Build();
var query = new Query<Position>().Without<Frozen>();
var results = new List<Entity>();
world.ForEach(query, (Entity e, ref Position pos) =>
foreach (var it in world.Select(query))
{
results.Add(e);
});
results.Add(it.Entity);
}
results.Should().BeEquivalentTo([a]);
}
@ -416,16 +363,15 @@ public class EdgeCaseTests
world.AddComponent(a, new Position { X = 1, Y = 2 });
world.AddComponent(b, new Position { X = 3, Y = 4 });
var query = world.Query().With<Position>().Build();
var seen = new List<Entity>();
world.ForEach(query, (Entity e, ref Position pos) =>
foreach (var it in world.Select<Position>())
{
seen.Add(e);
seen.Add(it.Entity);
// Add a component to the other entity during iteration.
// This should not affect the current iteration.
world.AddComponent(e, new Velocity { X = 1, Y = 1 });
});
world.AddComponent(it.Entity, new Velocity { X = 1, Y = 1 });
}
seen.Should().BeEquivalentTo([a, b]);
}
@ -439,94 +385,16 @@ public class EdgeCaseTests
world.AddComponent(a, new Position { X = 1, Y = 2 });
world.AddComponent(b, new Position { X = 3, Y = 4 });
var query = world.Query().With<Position>().Build();
// Destroying an entity during iteration: the sparse set uses
// swap-remove, so the destroyed entity's slot is replaced by
// the last element. This is safe as long as we don't re-iterate
// the destroyed entity (which we won't since it's swap-removed).
var act = () =>
{
world.ForEach(query, (Entity e, ref Position pos) =>
foreach (var it in world.Select<Position>())
{
world.DestroyEntity(e);
});
};
act.Should().NotThrow();
}
// ── QueryDescriptor.With enforced during ForEach ─────────────────
[Fact]
public void ForEach_Throws_WhenTypeParamsDivergeFromQueryWith()
{
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 });
// Build a query requiring Position + Velocity, but iterate only Position.
var query = world.Query().With<Position>().With<Velocity>().Build();
var act = () =>
{
world.ForEach(query, (Entity e, ref Position pos) => { });
};
act.Should().Throw<InvalidOperationException>()
.WithMessage("*ForEach*type*Position*Velocity*");
}
[Fact]
public void ForEach_Throws_WhenTypeParamsAreSubsetOfQueryWith()
{
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 });
// Build a query requiring 3 components, but iterate only 2.
var query = world.Query().With<Position>().With<Velocity>().With<Health>().Build();
var act = () =>
{
world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel) => { });
};
act.Should().Throw<InvalidOperationException>()
.WithMessage("*ForEach*type*Health*");
}
[Fact]
public void ForEach_Throws_WhenTypeParamsAreSupersetOfQueryWith()
{
var world = new World();
var entity = world.CreateEntity();
world.AddComponent(entity, new Position { X = 1, Y = 2 });
// Build a query requiring only Position, but iterate Position + Velocity.
var query = world.Query().With<Position>().Build();
var act = () =>
{
world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel) => { });
};
act.Should().Throw<InvalidOperationException>()
.WithMessage("*ForEach*type*Velocity*");
}
[Fact]
public void ForEach_DoesNotThrow_WhenTypeParamsMatchQueryWith()
{
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 });
var query = world.Query().With<Position>().With<Velocity>().Build();
var act = () =>
{
world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel) => { });
world.DestroyEntity(it.Entity);
}
};
act.Should().NotThrow();
}

View File

@ -22,13 +22,9 @@ public class QueryTests
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);
});
foreach (var it in world.Select<Position>())
results.Add(it.Entity);
results.Should().BeEquivalentTo([a, b]);
}
@ -50,13 +46,9 @@ public class QueryTests
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);
});
foreach (var it in world.Select<Position, Velocity>())
results.Add(it.Entity);
results.Should().BeEquivalentTo([a]);
}
@ -72,13 +64,10 @@ public class QueryTests
world.AddComponent(b, new Position { X = 3, Y = 4 });
world.AddComponent(b, new Frozen());
var query = world.Query().With<Position>().Without<Frozen>().Build();
var query = new Query<Position>().Without<Frozen>();
var results = new List<Entity>();
world.ForEach(query, (Entity entity, ref Position pos) =>
{
results.Add(entity);
});
foreach (var it in world.Select(query))
results.Add(it.Entity);
results.Should().BeEquivalentTo([a]);
}
@ -87,14 +76,9 @@ public class QueryTests
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) =>
{
foreach (var it in world.Select<Position>())
count++;
});
count.Should().Be(0);
}
@ -105,12 +89,8 @@ public class QueryTests
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;
});
foreach (var it in world.Select<Position>())
it.Item1.X = 42;
ref var pos = ref world.GetComponent<Position>(entity);
pos.X.Should().Be(42);
@ -128,13 +108,9 @@ public class QueryTests
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);
});
foreach (var it in world.Select<Position>())
results.Add(it.Entity);
results.Should().BeEquivalentTo([b]);
}
@ -149,23 +125,31 @@ public class QueryTests
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) =>
foreach (var it in world.Select<Position, Velocity, Health>())
{
count++;
pos.X.Should().Be(1);
vel.Y.Should().Be(4);
hp.Value.Should().Be(100);
});
it.Item1.X.Should().Be(1);
it.Item2.Y.Should().Be(4);
it.Item3.Value.Should().Be(100);
}
count.Should().Be(1);
}
[Fact]
public void FindEntity_ReturnsFirstMatch()
{
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 });
var found = world.FindEntity<Position>();
found.Should().NotBe(Entity.Null);
}
}
public class SystemTests
@ -175,23 +159,16 @@ public class SystemTests
private class MovementSystem : ITickedSystem
{
private readonly QueryDescriptor _query;
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) =>
foreach (var it in world.Select<Position, Velocity>())
{
pos.X += vel.X * dt;
pos.Y += vel.Y * dt;
});
it.Item1.X += it.Item2.X * dt;
it.Item1.Y += it.Item2.Y * dt;
}
}
}
@ -200,8 +177,7 @@ public class SystemTests
{
var world = new World();
var group = new SystemGroup(world);
var system = new MovementSystem(world);
group.Add(system);
group.Add(new MovementSystem());
var entity = world.CreateEntity();
world.AddComponent(entity, new Position { X = 0, Y = 0 });
@ -220,7 +196,7 @@ public class SystemTests
var world = new World();
var group = new SystemGroup(world);
var system = new TestLogicalSystem(world);
var system = new TestLogicalSystem();
group.Add(system);
group.RunLogical();
@ -235,11 +211,7 @@ public class SystemTests
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) { }
public void Run(World world, Tick tick)
{
@ -247,55 +219,4 @@ public class SystemTests
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();
}
}

View File

@ -208,7 +208,7 @@ public class ReactivityTests
public void ObserveQuery_OnlyReceivesMatchingChanges()
{
var world = new World();
var query = world.Query().With<Position>().Without<Frozen>().Build();
var query = new Query<Position>().Without<Frozen>();
var collector = new ChangeCollector();
using var sub = world.ObserveQuery(query).Subscribe(collector.ToObserver());

View File

@ -186,14 +186,13 @@ public class RelationshipTests
Target = parent
});
var query = world.Query().With<Relationship<ChildOf, ParentOf>>().Build();
var results = new List<Entity>();
world.ForEach(query, (Entity e, ref Relationship<ChildOf, ParentOf> rel) =>
foreach (var it in world.Select<Relationship<ChildOf, ParentOf>>())
{
results.Add(e);
rel.Target.Should().Be(parent);
});
results.Add(it.Entity);
it.Item1.Target.Should().Be(parent);
}
results.Should().BeEquivalentTo([child]);
}

View File

@ -75,13 +75,12 @@ public class SingletonTests
var regular = world.CreateEntity();
world.AddComponent(regular, new GameConfig { Gravity = 1.6f, MaxPlayers = 2 });
var query = world.Query().With<GameConfig>().Build();
var results = new List<Entity>();
world.ForEach(query, (Entity e, ref GameConfig cfg) =>
foreach (var it in world.Select<GameConfig>())
{
results.Add(e);
});
results.Add(it.Entity);
}
// Only the regular entity should appear, not the singleton.
results.Should().BeEquivalentTo([regular]);
@ -94,7 +93,9 @@ public class SingletonTests
world.SetSingleton(new GameConfig());
world.IsAlive(World.SingletonEntity).Should().BeTrue();
// Singletons are now on their own dedicated entities.
// Verify the singleton exists (its entity is alive).
world.HasSingleton<GameConfig>().Should().BeTrue();
}
[Fact]

View File

@ -15,7 +15,7 @@ internal class ChangeBuffer
private readonly Subject<EntityChange> _entitySubject = new();
private readonly Dictionary<Type, TrackedSubject> _componentSubjects = new();
private readonly Dictionary<QueryDescriptor, TrackedSubject> _querySubjects = new();
private readonly Dictionary<QueryKey, TrackedSubject> _querySubjects = new();
/// <summary>
/// Wraps a <see cref="Subject{T}"/> with a subscriber count so that
@ -27,6 +27,42 @@ internal class ChangeBuffer
public int SubscriberCount;
}
/// <summary>
/// Identifies a query subscription for cleanup tracking.
/// </summary>
private readonly struct QueryKey : IEquatable<QueryKey>
{
public readonly Type[] WithTypes;
public readonly Type[] WithoutTypes;
public QueryKey(Type[] withTypes, Type[] withoutTypes)
{
WithTypes = withTypes;
WithoutTypes = withoutTypes;
}
public bool Equals(QueryKey other)
{
if (WithTypes.Length != other.WithTypes.Length) return false;
if (WithoutTypes.Length != other.WithoutTypes.Length) return false;
for (int i = 0; i < WithTypes.Length; i++)
if (WithTypes[i] != other.WithTypes[i]) return false;
for (int i = 0; i < WithoutTypes.Length; i++)
if (WithoutTypes[i] != other.WithoutTypes[i]) return false;
return true;
}
public override bool Equals(object? obj) => obj is QueryKey other && Equals(other);
public override int GetHashCode()
{
var hash = new HashCode();
foreach (var t in WithTypes) hash.Add(t);
foreach (var t in WithoutTypes) hash.Add(t);
return hash.ToHashCode();
}
}
/// <summary>
/// The change set currently accumulating. Cleared after each <see cref="Post"/>.
/// </summary>
@ -101,14 +137,33 @@ internal class ChangeBuffer
}
/// <summary>
/// Returns an observable that emits changes matching the given query.
/// Returns an observable that emits component-level changes matching
/// the given query's With types and excluding its Without types.
/// </summary>
public Observable<EntityChange> ObserveQuery(QueryDescriptor query)
public Observable<EntityChange> ObserveQuery<T1>(Query<T1> query)
where T1 : struct
{
if (!_querySubjects.TryGetValue(query, out var tracked))
var key = MakeKey(query);
return SubscribeQuery(key);
}
/// <summary>
/// Returns an observable that emits component-level changes matching
/// the given query.
/// </summary>
public Observable<EntityChange> ObserveQuery<T1, T2>(Query<T1, T2> query)
where T1 : struct where T2 : struct
{
var key = MakeKey(query);
return SubscribeQuery(key);
}
private Observable<EntityChange> SubscribeQuery(QueryKey key)
{
if (!_querySubjects.TryGetValue(key, out var tracked))
{
tracked = new TrackedSubject();
_querySubjects[query] = tracked;
_querySubjects[key] = tracked;
}
tracked.SubscriberCount++;
@ -118,11 +173,28 @@ internal class ChangeBuffer
if (tracked.SubscriberCount <= 0)
{
tracked.Subject.Dispose();
_querySubjects.Remove(query);
_querySubjects.Remove(key);
}
});
}
private static QueryKey MakeKey<T1>(Query<T1> query) where T1 : struct
{
var without = query.WithoutTypes;
return new QueryKey(
[typeof(T1)],
without != null ? [.. without] : []);
}
private static QueryKey MakeKey<T1, T2>(Query<T1, T2> query)
where T1 : struct where T2 : struct
{
var without = query.WithoutTypes;
return new QueryKey(
[typeof(T1), typeof(T2)],
without != null ? [.. without] : []);
}
/// <summary>
/// Wraps an observable so that <paramref name="onLastDispose"/> is called
/// when the last subscriber disposes.
@ -159,21 +231,26 @@ internal class ChangeBuffer
_querySubjects.Clear();
}
private static bool ChangeMatchesQuery(EntityChange change, QueryDescriptor query)
private static bool ChangeMatchesQuery(EntityChange change, QueryKey query)
{
// Entity-level changes: match if the entity was added/removed and
// the query has any "with" types (we can't know component state for
// removed entities, so we only match added entities that would satisfy
// the query — but we don't have component data here).
// For simplicity, we only match component-level changes against queries.
// Entity-level changes don't have a component type.
if (change.ComponentType == null)
return false;
// A component change matches a query if the component type is in the
// query's With set and not in the Without set.
if (query.Without.Contains(change.ComponentType))
return false;
// Must be in With types.
bool inWith = false;
foreach (var t in query.WithTypes)
{
if (t == change.ComponentType) { inWith = true; break; }
}
if (!inWith) return false;
return query.With.Contains(change.ComponentType);
// Must not be in Without types.
foreach (var t in query.WithoutTypes)
{
if (t == change.ComponentType) return false;
}
return true;
}
}

View File

@ -34,17 +34,25 @@ public sealed class ComponentDescriptor
/// </summary>
public Func<byte[], object> Deserialize { get; }
/// <summary>
/// True if this component type is used as a singleton (via <c>SetSingleton</c>).
/// Set by the source generator at compile time.
/// </summary>
public bool IsSingleton { get; }
public ComponentDescriptor(
string typeName,
Type type,
Func<object, byte[]> serialize,
Action<World, Entity, byte[]> deserializeAndAdd,
Func<byte[], object> deserialize)
Func<byte[], object> deserialize,
bool isSingleton = false)
{
TypeName = typeName;
Type = type;
Serialize = serialize;
DeserializeAndAdd = deserializeAndAdd;
Deserialize = deserialize;
IsSingleton = isSingleton;
}
}

View File

@ -10,7 +10,7 @@ namespace OECS;
internal class EntityAllocator
{
private const int DefaultCapacity = 1024;
private const uint FirstValidId = 2; // 0 = Null, 1 = Singleton
private const uint FirstValidId = 1; // 0 = Null
private byte[] _versions;
private readonly Stack<uint> _freeIds;
@ -63,16 +63,6 @@ internal class EntityAllocator
_freeIds.Push(id);
}
/// <summary>
/// Registers the singleton entity so that <see cref="IsAlive"/> returns true for it.
/// Called once by <see cref="World"/> when the first singleton accessor is used.
/// </summary>
public void RegisterSingleton(Entity singleton)
{
EnsureCapacity(singleton.Id);
_versions[singleton.Id] = (byte)singleton.Version;
}
/// <summary>
/// Reserves a specific entity ID and version for deserialization.
/// Advances <see cref="_nextId"/> past the reserved ID so future
@ -94,7 +84,6 @@ internal class EntityAllocator
{
uint id = entity.Id;
if (entity.IsNull) return false;
if (id == 1) return true; // Singleton is always alive.
if (id >= _versions.Length) return false;
return _versions[id] == entity.Version && _versions[id] != 0;
}

View File

@ -1,327 +0,0 @@
using System.Runtime.CompilerServices;
namespace OECS;
/// <summary>
/// Provides zero-allocation ref struct iterators for querying entities.
/// Use with <c>foreach</c> or manual <c>while (iter.MoveNext())</c>.
/// Supports <c>break</c>, <c>continue</c>, and early returns.
/// </summary>
public static class EntityIterator
{
/// <summary>
/// Returns an iterator over entities matching the query, with ref access
/// to one component type.
/// </summary>
public static Select1<T1> Select<T1>(
this World world, QueryDescriptor query)
where T1 : struct
{
return new Select1<T1>(world, query);
}
/// <summary>
/// Returns an iterator over entities that have a component of type
/// <typeparamref name="T1"/>. No <c>Without</c> filter is applied.
/// </summary>
public static Select1<T1> Select<T1>(
this World world)
where T1 : struct
{
return new Select1<T1>(world, new QueryDescriptor(new HashSet<Type>(), new HashSet<Type>()));
}
/// <summary>
/// Returns an iterator over entities matching the query, with ref access
/// to two component types.
/// </summary>
public static Select2<T1, T2> Select<T1, T2>(
this World world, QueryDescriptor query)
where T1 : struct where T2 : struct
{
return new Select2<T1, T2>(world, query);
}
/// <summary>
/// Returns an iterator over entities that have both components of type
/// <typeparamref name="T1"/> and <typeparamref name="T2"/>.
/// No <c>Without</c> filter is applied.
/// </summary>
public static Select2<T1, T2> Select<T1, T2>(
this World world)
where T1 : struct where T2 : struct
{
return new Select2<T1, T2>(world, new QueryDescriptor(new HashSet<Type>(), new HashSet<Type>()));
}
/// <summary>
/// Returns an iterator over entities matching the query, with ref access
/// to three component types.
/// </summary>
public static Select3<T1, T2, T3> Select<T1, T2, T3>(
this World world, QueryDescriptor query)
where T1 : struct where T2 : struct where T3 : struct
{
return new Select3<T1, T2, T3>(world, query);
}
/// <summary>
/// Returns an iterator over entities that have all three components of type
/// <typeparamref name="T1"/>, <typeparamref name="T2"/>, and
/// <typeparamref name="T3"/>. No <c>Without</c> filter is applied.
/// </summary>
public static Select3<T1, T2, T3> Select<T1, T2, T3>(
this World world)
where T1 : struct where T2 : struct where T3 : struct
{
return new Select3<T1, T2, T3>(world, new QueryDescriptor(new HashSet<Type>(), new HashSet<Type>()));
}
}
/// <summary>
/// Ref struct iterator for queries with one component type.
/// </summary>
public ref struct Select1<T1> where T1 : struct
{
private readonly World _world;
private readonly SparseSet<T1>? _set;
private readonly ComponentStore _store;
private readonly IReadOnlySet<Type> _without;
private int _index;
private readonly int _count;
internal Select1(World world, QueryDescriptor query)
{
_world = world;
_store = world.Components;
_without = query.Without;
_set = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_count = _set?.Count ?? 0;
_index = -1;
_world.BeginIteration();
}
public Entity CurrentEntity { get; private set; }
public ref T1 Current1 => ref _set!.Get(CurrentEntity);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_set == null) return false;
while (++_index < _count)
{
CurrentEntity = _set.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue; // Skip singleton.
if (!PassesWithout()) continue;
return true;
}
return false;
}
public Select1<T1> GetEnumerator() => this;
public void Dispose()
{
_world.EndIteration();
}
private bool PassesWithout()
{
foreach (var type in _without)
{
var set = _store.GetSet(type);
if (set != null && set.Contains(CurrentEntity))
return false;
}
return true;
}
}
/// <summary>
/// Ref struct iterator for queries with two component types.
/// Drives iteration from the smaller sparse set to minimize probes.
/// </summary>
public ref struct Select2<T1, T2>
where T1 : struct where T2 : struct
{
private readonly World _world;
private readonly SparseSet<T1>? _set1;
private readonly SparseSet<T2>? _set2;
private readonly ComponentStore _store;
private readonly IReadOnlySet<Type> _without;
private int _index;
private readonly int _count;
private readonly bool _swapped; // true when driving from _set2
internal Select2(World world, QueryDescriptor query)
{
_world = world;
_store = world.Components;
_without = query.Without;
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
int count1 = _set1?.Count ?? 0;
int count2 = _set2?.Count ?? 0;
_swapped = count2 < count1;
_count = _swapped ? count2 : count1;
_index = -1;
_world.BeginIteration();
}
public Entity CurrentEntity { get; private set; }
public ref T1 Current1 => ref _set1!.Get(CurrentEntity);
public ref T2 Current2 => ref _set2!.Get(CurrentEntity);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_set1 == null || _set2 == null) return false;
while (++_index < _count)
{
if (_swapped)
{
CurrentEntity = _set2!.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue;
if (!_set1.Contains(CurrentEntity)) continue;
}
else
{
CurrentEntity = _set1.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue;
if (!_set2.Contains(CurrentEntity)) continue;
}
if (!PassesWithout()) continue;
return true;
}
return false;
}
public Select2<T1, T2> GetEnumerator() => this;
public void Dispose()
{
_world.EndIteration();
}
private bool PassesWithout()
{
foreach (var type in _without)
{
var set = _store.GetSet(type);
if (set != null && set.Contains(CurrentEntity))
return false;
}
return true;
}
}
/// <summary>
/// Ref struct iterator for queries with three component types.
/// Drives iteration from the smallest sparse set to minimize probes.
/// </summary>
public ref struct Select3<T1, T2, T3>
where T1 : struct where T2 : struct where T3 : struct
{
private readonly World _world;
private readonly SparseSet<T1>? _set1;
private readonly SparseSet<T2>? _set2;
private readonly SparseSet<T3>? _set3;
private readonly ComponentStore _store;
private readonly IReadOnlySet<Type> _without;
private int _index;
private readonly int _count;
private readonly int _driver; // 0 = _set1, 1 = _set2, 2 = _set3
internal Select3(World world, QueryDescriptor query)
{
_world = world;
_store = world.Components;
_without = query.Without;
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
_set3 = _store.GetSet(typeof(T3)) as SparseSet<T3>;
int count1 = _set1?.Count ?? 0;
int count2 = _set2?.Count ?? 0;
int count3 = _set3?.Count ?? 0;
if (count2 <= count1 && count2 <= count3)
{
_driver = 1;
_count = count2;
}
else if (count3 <= count1 && count3 <= count2)
{
_driver = 2;
_count = count3;
}
else
{
_driver = 0;
_count = count1;
}
_index = -1;
_world.BeginIteration();
}
public Entity CurrentEntity { get; private set; }
public ref T1 Current1 => ref _set1!.Get(CurrentEntity);
public ref T2 Current2 => ref _set2!.Get(CurrentEntity);
public ref T3 Current3 => ref _set3!.Get(CurrentEntity);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_set1 == null || _set2 == null || _set3 == null) return false;
while (++_index < _count)
{
switch (_driver)
{
case 0:
CurrentEntity = _set1.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue;
if (!_set2.Contains(CurrentEntity)) continue;
if (!_set3.Contains(CurrentEntity)) continue;
break;
case 1:
CurrentEntity = _set2!.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue;
if (!_set1.Contains(CurrentEntity)) continue;
if (!_set3.Contains(CurrentEntity)) continue;
break;
case 2:
CurrentEntity = _set3!.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue;
if (!_set1.Contains(CurrentEntity)) continue;
if (!_set2.Contains(CurrentEntity)) continue;
break;
}
if (!PassesWithout()) continue;
return true;
}
return false;
}
public Select3<T1, T2, T3> GetEnumerator() => this;
public void Dispose()
{
_world.EndIteration();
}
private bool PassesWithout()
{
foreach (var type in _without)
{
var set = _store.GetSet(type);
if (set != null && set.Contains(CurrentEntity))
return false;
}
return true;
}
}

View File

@ -1,39 +0,0 @@
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;

100
OECS/Query.cs Normal file
View File

@ -0,0 +1,100 @@
namespace OECS;
/// <summary>
/// Describes a query over the ECS world with one required component type.
/// Add optional <c>Without</c> filters via the fluent API.
/// </summary>
public struct Query<T1> where T1 : struct
{
internal HashSet<Type>? WithoutTypes;
/// <summary>
/// Excludes entities that have a component of type <typeparamref name="W"/>.
/// </summary>
public Query<T1> Without<W>() where W : struct
{
WithoutTypes ??= new HashSet<Type>();
WithoutTypes.Add(typeof(W));
return this;
}
}
/// <summary>
/// Describes a query over the ECS world with two required component types.
/// </summary>
public struct Query<T1, T2> where T1 : struct where T2 : struct
{
internal HashSet<Type>? WithoutTypes;
public Query<T1, T2> Without<W>() where W : struct
{
WithoutTypes ??= new HashSet<Type>();
WithoutTypes.Add(typeof(W));
return this;
}
}
/// <summary>
/// Describes a query over the ECS world with three required component types.
/// </summary>
public struct Query<T1, T2, T3>
where T1 : struct where T2 : struct where T3 : struct
{
internal HashSet<Type>? WithoutTypes;
public Query<T1, T2, T3> Without<W>() where W : struct
{
WithoutTypes ??= new HashSet<Type>();
WithoutTypes.Add(typeof(W));
return this;
}
}
/// <summary>
/// Describes a query over the ECS world with four required component types.
/// </summary>
public struct Query<T1, T2, T3, T4>
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
{
internal HashSet<Type>? WithoutTypes;
public Query<T1, T2, T3, T4> Without<W>() where W : struct
{
WithoutTypes ??= new HashSet<Type>();
WithoutTypes.Add(typeof(W));
return this;
}
}
/// <summary>
/// Describes a query over the ECS world with five required component types.
/// </summary>
public struct Query<T1, T2, T3, T4, T5>
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
{
internal HashSet<Type>? WithoutTypes;
public Query<T1, T2, T3, T4, T5> Without<W>() where W : struct
{
WithoutTypes ??= new HashSet<Type>();
WithoutTypes.Add(typeof(W));
return this;
}
}
/// <summary>
/// Describes a query over the ECS world with six required component types.
/// </summary>
public struct Query<T1, T2, T3, T4, T5, T6>
where T1 : struct where T2 : struct where T3 : struct
where T4 : struct where T5 : struct where T6 : struct
{
internal HashSet<Type>? WithoutTypes;
public Query<T1, T2, T3, T4, T5, T6> Without<W>() where W : struct
{
WithoutTypes ??= new HashSet<Type>();
WithoutTypes.Add(typeof(W));
return this;
}
}

View File

@ -1,39 +0,0 @@
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

@ -1,53 +0,0 @@
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; }
private readonly int _hashCode;
internal QueryDescriptor(HashSet<Type> with, HashSet<Type> without)
{
With = with;
Without = without;
_hashCode = ComputeHashCode();
}
/// <summary>
/// Returns true if this query has no "with" components (matches nothing).
/// </summary>
internal bool IsEmpty => With.Count == 0;
public override bool Equals(object? obj)
{
if (obj is not QueryDescriptor other) return false;
if (With.Count != other.With.Count || Without.Count != other.Without.Count)
return false;
return With.SetEquals(other.With) && Without.SetEquals(other.Without);
}
public override int GetHashCode() => _hashCode;
private int ComputeHashCode()
{
var hash = new HashCode();
foreach (var t in With.OrderBy(t => t.GUID.ToString()))
hash.Add(t);
foreach (var t in Without.OrderBy(t => t.GUID.ToString()))
hash.Add(t);
return hash.ToHashCode();
}
}

View File

@ -1,507 +0,0 @@
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.
///
/// The singleton entity (ID 1) is automatically excluded from all queries.
/// </summary>
internal static class QueryExecutor
{
private const uint SingletonId = 1;
[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 (entity.Id == SingletonId) continue;
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 (entity.Id == SingletonId) continue;
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 (entity.Id == SingletonId) continue;
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;
// Pick the smallest set as the driver.
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count;
if (c2 <= c1 && c2 <= c3)
{
var dense = set2.Dense;
var denseEntities = set2.DenseEntities;
var count = set2.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
if (!set3.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
action(entity, ref set1.Get(entity), ref dense[i], ref set3.Get(entity));
}
}
else if (c3 <= c1 && c3 <= c2)
{
var dense = set3.Dense;
var denseEntities = set3.DenseEntities;
var count = set3.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
if (!set2.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i]);
}
}
else
{
var dense = set1.Dense;
var denseEntities = set1.DenseEntities;
var count = set1.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
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;
// Pick the smallest set as the driver.
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count, c4 = set4.Count;
int min = Math.Min(Math.Min(c1, c2), Math.Min(c3, c4));
if (min == c2)
{
var dense = set2.Dense;
var denseEntities = set2.DenseEntities;
var count = set2.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
if (!set3.Contains(entity)) continue;
if (!set4.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
action(entity, ref set1.Get(entity), ref dense[i], ref set3.Get(entity), ref set4.Get(entity));
}
}
else if (min == c3)
{
var dense = set3.Dense;
var denseEntities = set3.DenseEntities;
var count = set3.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
if (!set2.Contains(entity)) continue;
if (!set4.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i], ref set4.Get(entity));
}
}
else if (min == c4)
{
var dense = set4.Dense;
var denseEntities = set4.DenseEntities;
var count = set4.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
if (!set2.Contains(entity)) continue;
if (!set3.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref dense[i]);
}
}
else
{
var dense = set1.Dense;
var denseEntities = set1.DenseEntities;
var count = set1.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
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;
// Pick the smallest set as the driver.
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count, c4 = set4.Count, c5 = set5.Count;
int min = Math.Min(Math.Min(Math.Min(c1, c2), Math.Min(c3, c4)), c5);
if (min == c2)
{
var dense = set2.Dense;
var denseEntities = set2.DenseEntities;
var count = set2.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.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 set1.Get(entity), ref dense[i], ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity));
}
}
else if (min == c3)
{
var dense = set3.Dense;
var denseEntities = set3.DenseEntities;
var count = set3.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
if (!set2.Contains(entity)) continue;
if (!set4.Contains(entity)) continue;
if (!set5.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i], ref set4.Get(entity), ref set5.Get(entity));
}
}
else if (min == c4)
{
var dense = set4.Dense;
var denseEntities = set4.DenseEntities;
var count = set4.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
if (!set2.Contains(entity)) continue;
if (!set3.Contains(entity)) continue;
if (!set5.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref dense[i], ref set5.Get(entity));
}
}
else if (min == c5)
{
var dense = set5.Dense;
var denseEntities = set5.DenseEntities;
var count = set5.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
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 set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref dense[i]);
}
}
else
{
var dense = set1.Dense;
var denseEntities = set1.DenseEntities;
var count = set1.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
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;
// Pick the smallest set as the driver.
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count, c4 = set4.Count, c5 = set5.Count, c6 = set6.Count;
int min = Math.Min(Math.Min(Math.Min(c1, c2), Math.Min(c3, c4)), Math.Min(c5, c6));
if (min == c2)
{
var dense = set2.Dense;
var denseEntities = set2.DenseEntities;
var count = set2.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.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 set1.Get(entity), ref dense[i], ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
}
}
else if (min == c3)
{
var dense = set3.Dense;
var denseEntities = set3.DenseEntities;
var count = set3.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
if (!set2.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 set1.Get(entity), ref set2.Get(entity), ref dense[i], ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
}
}
else if (min == c4)
{
var dense = set4.Dense;
var denseEntities = set4.DenseEntities;
var count = set4.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
if (!set2.Contains(entity)) continue;
if (!set3.Contains(entity)) continue;
if (!set5.Contains(entity)) continue;
if (!set6.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref dense[i], ref set5.Get(entity), ref set6.Get(entity));
}
}
else if (min == c5)
{
var dense = set5.Dense;
var denseEntities = set5.DenseEntities;
var count = set5.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
if (!set2.Contains(entity)) continue;
if (!set3.Contains(entity)) continue;
if (!set4.Contains(entity)) continue;
if (!set6.Contains(entity)) continue;
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref dense[i], ref set6.Get(entity));
}
}
else if (min == c6)
{
var dense = set6.Dense;
var denseEntities = set6.DenseEntities;
var count = set6.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
if (!set1.Contains(entity)) continue;
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 set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref dense[i]);
}
}
else
{
var dense = set1.Dense;
var denseEntities = set1.DenseEntities;
var count = set1.Count;
for (int i = 0; i < count; i++)
{
var entity = denseEntities[i];
if (entity.Id == SingletonId) continue;
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

@ -11,17 +11,12 @@ namespace OECS;
/// </summary>
public class World : IDisposable
{
/// <summary>
/// The reserved entity ID for the singleton entity.
/// </summary>
public static readonly Entity SingletonEntity = new(1, 1);
private readonly EntityAllocator _allocator;
private readonly ComponentStore _components;
private readonly CommandQueue _commands;
private readonly RelationshipIndex _relationships;
private readonly ChangeBuffer _changes;
private bool _singletonCreated;
private readonly Dictionary<Type, Entity> _singletonEntities = new();
private bool _disposed;
// Deferred structural mutation support: when iterating entities,
@ -109,10 +104,6 @@ public class World : IDisposable
if (!_allocator.IsAlive(entity))
return;
// Singleton entity is never destroyed.
if (entity.Id == 1)
return;
// Remove all relationships where this entity is the target.
// Materialize to avoid collection-modified-during-enumeration when
// OnRemoved mutates the same HashSet we're iterating.
@ -316,9 +307,9 @@ public class World : IDisposable
/// <summary>
/// Marks a component of type <typeparamref name="T"/> on the given entity
/// as modified. Only needed outside of iteration scopes — during
/// <see cref="ForEach"/> or a <c>Select</c> loop, components accessed
/// via <see cref="GetComponent{T}"/> are auto-marked.
/// as modified. Only needed outside of iteration scopes — during a
/// <c>Select</c> loop, components accessed via <see cref="GetComponent{T}"/>
/// are auto-marked.
/// </summary>
public void MarkModified<T>(Entity entity) where T : struct
{
@ -355,11 +346,21 @@ public class World : IDisposable
}
/// <summary>
/// Returns an observable that emits changes matching the given query.
/// Only component-level changes whose component type is in the query's
/// "with" set and not in the "without" set are emitted.
/// Returns an observable that emits component-level changes matching
/// the given query's With types and excluding its Without types.
/// </summary>
public Observable<EntityChange> ObserveQuery(QueryDescriptor query)
public Observable<EntityChange> ObserveQuery<T1>(Query<T1> query = default)
where T1 : struct
{
return _changes.ObserveQuery(query);
}
/// <summary>
/// Returns an observable that emits component-level changes matching
/// the given query.
/// </summary>
public Observable<EntityChange> ObserveQuery<T1, T2>(Query<T1, T2> query = default)
where T1 : struct where T2 : struct
{
return _changes.ObserveQuery(query);
}
@ -368,13 +369,16 @@ public class World : IDisposable
/// <summary>
/// Sets (adds or replaces) a singleton component of type <typeparamref name="T"/>.
/// Singletons are stored on a reserved entity that is never destroyed
/// and excluded from normal queries.
/// Each singleton component type gets its own dedicated entity.
/// </summary>
public void SetSingleton<T>(T component) where T : struct
{
EnsureSingleton();
AddComponent(SingletonEntity, component);
if (!_singletonEntities.TryGetValue(typeof(T), out var entity))
{
entity = _allocator.Allocate();
_singletonEntities[typeof(T)] = entity;
}
AddComponent(entity, component);
}
/// <summary>
@ -383,8 +387,7 @@ public class World : IDisposable
/// </summary>
public ref T GetSingleton<T>() where T : struct
{
EnsureSingleton();
return ref GetComponent<T>(SingletonEntity);
return ref GetComponent<T>(GetSingletonEntity<T>());
}
/// <summary>
@ -393,8 +396,7 @@ public class World : IDisposable
/// </summary>
public T ReadSingleton<T>() where T : struct
{
EnsureSingleton();
return ReadComponent<T>(SingletonEntity);
return ReadComponent<T>(GetSingletonEntity<T>());
}
/// <summary>
@ -402,7 +404,8 @@ public class World : IDisposable
/// </summary>
public bool HasSingleton<T>() where T : struct
{
return HasComponent<T>(SingletonEntity);
return _singletonEntities.TryGetValue(typeof(T), out var entity)
&& HasComponent<T>(entity);
}
/// <summary>
@ -411,20 +414,22 @@ public class World : IDisposable
/// </summary>
public void RemoveSingleton<T>() where T : struct
{
RemoveComponent<T>(SingletonEntity);
if (_singletonEntities.TryGetValue(typeof(T), out var entity))
RemoveComponent<T>(entity);
}
internal void EnsureSingleton()
/// <summary>
/// Gets the entity backing the singleton of type <typeparamref name="T"/>,
/// creating it if it doesn't exist yet.
/// </summary>
private Entity GetSingletonEntity<T>() where T : struct
{
if (_singletonCreated)
return;
_singletonCreated = true;
// Manually register the singleton entity in the allocator so
// IsAlive returns true for it. We bypass Allocate() because
// the singleton has a fixed ID and version.
_allocator.RegisterSingleton(SingletonEntity);
if (!_singletonEntities.TryGetValue(typeof(T), out var entity))
{
entity = _allocator.Allocate();
_singletonEntities[typeof(T)] = entity;
}
return entity;
}
// ── Relationships ────────────────────────────────────────────────
@ -457,140 +462,7 @@ public class World : IDisposable
_commands.ExecuteAll(this);
}
// ── 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. Structural mutations (AddComponent, RemoveComponent,
/// DestroyEntity) made during iteration are deferred and applied after
/// the iteration completes.
/// </summary>
public void ForEach<T1>(
QueryDescriptor query,
ForEachAction<T1> action)
where T1 : struct
{
ValidateQueryTypes(query, typeof(T1));
BeginIteration();
try
{
QueryExecutor.ForEach(_components, query, action);
}
finally
{
EndIteration();
}
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// two component types. Structural mutations are deferred.
/// </summary>
public void ForEach<T1, T2>(
QueryDescriptor query,
ForEachAction<T1, T2> action)
where T1 : struct where T2 : struct
{
ValidateQueryTypes(query, typeof(T1), typeof(T2));
BeginIteration();
try
{
QueryExecutor.ForEach(_components, query, action);
}
finally
{
EndIteration();
}
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// three component types. Structural mutations are deferred.
/// </summary>
public void ForEach<T1, T2, T3>(
QueryDescriptor query,
ForEachAction<T1, T2, T3> action)
where T1 : struct where T2 : struct where T3 : struct
{
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3));
BeginIteration();
try
{
QueryExecutor.ForEach(_components, query, action);
}
finally
{
EndIteration();
}
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// four component types. Structural mutations are deferred.
/// </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
{
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4));
BeginIteration();
try
{
QueryExecutor.ForEach(_components, query, action);
}
finally
{
EndIteration();
}
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// five component types. Structural mutations are deferred.
/// </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
{
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5));
BeginIteration();
try
{
QueryExecutor.ForEach(_components, query, action);
}
finally
{
EndIteration();
}
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// six component types. Structural mutations are deferred.
/// </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
{
ValidateQueryTypes(query, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6));
BeginIteration();
try
{
QueryExecutor.ForEach(_components, query, action);
}
finally
{
EndIteration();
}
}
// ── Iteration Lifecycle ──────────────────────────────────────────
/// <summary>
/// Begins an iteration scope. Structural mutations are buffered
@ -670,6 +542,33 @@ public class World : IDisposable
/// </summary>
internal ComponentStore Components => _components;
/// <summary>
/// Gets the entity that backs the singleton of type <typeparamref name="T"/>,
/// or <see cref="Entity.Null"/> if the singleton has not been set.
/// </summary>
internal Entity GetSingletonEntityOrNull<T>() where T : struct
{
_singletonEntities.TryGetValue(typeof(T), out var entity);
return entity;
}
/// <summary>
/// Registers an existing entity as the backing entity for a singleton component.
/// Used during deserialization to restore singleton state.
/// </summary>
internal void RegisterSingletonEntity(Type componentType, Entity entity)
{
_singletonEntities[componentType] = entity;
}
/// <summary>
/// Returns true if the given entity is a singleton entity (backs any singleton component).
/// </summary>
internal bool IsSingletonEntity(Entity entity)
{
return _singletonEntities.ContainsValue(entity);
}
// ── Helpers ───────────────────────────────────────────────────────
private void ThrowIfNotAlive(Entity entity)
@ -678,39 +577,6 @@ public class World : IDisposable
throw new InvalidOperationException($"Entity {entity} is not alive.");
}
/// <summary>
/// Validates that the ForEach type parameters match the query's
/// With set. When the With set is empty (only Without filters are
/// specified), any ForEach type parameters are accepted.
/// </summary>
private static void ValidateQueryTypes(QueryDescriptor query, params Type[] forEachTypes)
{
// When With is empty, the query is using only Without filters.
// The ForEach type parameters are the sole source of truth.
if (query.With.Count == 0)
return;
if (query.With.Count != forEachTypes.Length)
ThrowMismatch(query, forEachTypes);
foreach (var t in forEachTypes)
{
if (!query.With.Contains(t))
ThrowMismatch(query, forEachTypes);
}
}
private static void ThrowMismatch(QueryDescriptor query, Type[] forEachTypes)
{
var queryTypes = string.Join(", ", query.With.Select(t => t.Name));
var forEachTypeNames = string.Join(", ", forEachTypes.Select(t => t.Name));
throw new InvalidOperationException(
$"ForEach type parameters [{forEachTypeNames}] do not match " +
$"the query's With types [{queryTypes}]. " +
$"Ensure the ForEach type parameters exactly match the types " +
$"passed to QueryBuilder.With<T>().");
}
// ── Cleanup ───────────────────────────────────────────────────────
public void Dispose()

View File

@ -0,0 +1,682 @@
using System.Runtime.CompilerServices;
namespace OECS;
/// <summary>
/// Provides zero-allocation ref struct iterators and entity-finding
/// extension methods for querying a <see cref="World"/>.
/// </summary>
public static class WorldQueryExtensions
{
// ── Select (ref struct enumerators) ──────────────────────────────
public static Select1<T1> Select<T1>(this World world, Query<T1> query = default)
where T1 : struct
{
return new Select1<T1>(world, query);
}
public static Select2<T1, T2> Select<T1, T2>(this World world, Query<T1, T2> query = default)
where T1 : struct where T2 : struct
{
return new Select2<T1, T2>(world, query);
}
public static Select3<T1, T2, T3> Select<T1, T2, T3>(this World world, Query<T1, T2, T3> query = default)
where T1 : struct where T2 : struct where T3 : struct
{
return new Select3<T1, T2, T3>(world, query);
}
public static Select4<T1, T2, T3, T4> Select<T1, T2, T3, T4>(this World world, Query<T1, T2, T3, T4> query = default)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
{
return new Select4<T1, T2, T3, T4>(world, query);
}
public static Select5<T1, T2, T3, T4, T5> Select<T1, T2, T3, T4, T5>(this World world, Query<T1, T2, T3, T4, T5> query = default)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
{
return new Select5<T1, T2, T3, T4, T5>(world, query);
}
public static Select6<T1, T2, T3, T4, T5, T6> Select<T1, T2, T3, T4, T5, T6>(this World world, Query<T1, T2, T3, T4, T5, T6> query = default)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct
{
return new Select6<T1, T2, T3, T4, T5, T6>(world, query);
}
// ── FindEntity / FindEntities ────────────────────────────────────
public static Entity FindEntity<T1>(this World world, Query<T1> query = default)
where T1 : struct
{
using var iter = new Select1<T1>(world, query);
return iter.MoveNext() ? iter.Entity : Entity.Null;
}
public static Entity FindEntity<T1, T2>(this World world, Query<T1, T2> query = default)
where T1 : struct where T2 : struct
{
using var iter = new Select2<T1, T2>(world, query);
return iter.MoveNext() ? iter.Entity : Entity.Null;
}
public static Entity FindEntity<T1, T2, T3>(this World world, Query<T1, T2, T3> query = default)
where T1 : struct where T2 : struct where T3 : struct
{
using var iter = new Select3<T1, T2, T3>(world, query);
return iter.MoveNext() ? iter.Entity : Entity.Null;
}
public static List<Entity> FindEntities<T1>(this World world, Query<T1> query = default)
where T1 : struct
{
var list = new List<Entity>();
using var iter = new Select1<T1>(world, query);
while (iter.MoveNext()) list.Add(iter.Entity);
return list;
}
public static List<Entity> FindEntities<T1, T2>(this World world, Query<T1, T2> query = default)
where T1 : struct where T2 : struct
{
var list = new List<Entity>();
using var iter = new Select2<T1, T2>(world, query);
while (iter.MoveNext()) list.Add(iter.Entity);
return list;
}
public static List<Entity> FindEntities<T1, T2, T3>(this World world, Query<T1, T2, T3> query = default)
where T1 : struct where T2 : struct where T3 : struct
{
var list = new List<Entity>();
using var iter = new Select3<T1, T2, T3>(world, query);
while (iter.MoveNext()) list.Add(iter.Entity);
return list;
}
}
// ── Ref struct enumerators ───────────────────────────────────────────
/// <summary>Ref struct iterator for queries with one component type.</summary>
public ref struct Select1<T1> where T1 : struct
{
private readonly World _world;
private readonly SparseSet<T1>? _set;
private readonly ComponentStore _store;
private readonly HashSet<Type>? _without;
private int _index;
private readonly int _count;
internal Select1(World world, Query<T1> query)
{
_world = world;
_store = world.Components;
_without = query.WithoutTypes;
_set = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_count = _set?.Count ?? 0;
_index = -1;
_world.BeginIteration();
}
/// <summary>The current entity handle.</summary>
public Entity Entity { get; private set; }
/// <summary>The current enumerator (required for foreach duck-typing).</summary>
public Select1<T1> Current => this;
/// <summary>Read-write reference to the current entity's component of type <typeparamref name="T1"/>.</summary>
public ref T1 Item1 => ref _set!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_set == null) return false;
while (++_index < _count)
{
Entity = _set.DenseEntities[_index];
if (!PassesWithout()) continue;
if (_world.IsSingletonEntity(Entity)) continue;
return true;
}
return false;
}
public Select1<T1> GetEnumerator() => this;
public void Dispose()
{
_world.EndIteration();
}
private bool PassesWithout()
{
if (_without == null) return true;
foreach (var type in _without)
{
var set = _store.GetSet(type);
if (set != null && set.Contains(Entity))
return false;
}
return true;
}
}
/// <summary>Ref struct iterator for queries with two component types. Drives from the smaller set.</summary>
public ref struct Select2<T1, T2> where T1 : struct where T2 : struct
{
private readonly World _world;
private readonly SparseSet<T1>? _set1;
private readonly SparseSet<T2>? _set2;
private readonly ComponentStore _store;
private readonly HashSet<Type>? _without;
private int _index;
private readonly int _count;
private readonly bool _swapped;
internal Select2(World world, Query<T1, T2> query)
{
_world = world;
_store = world.Components;
_without = query.WithoutTypes;
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
int c1 = _set1?.Count ?? 0;
int c2 = _set2?.Count ?? 0;
_swapped = c2 < c1;
_count = _swapped ? c2 : c1;
_index = -1;
_world.BeginIteration();
}
public Entity Entity { get; private set; }
public Select2<T1, T2> Current => this;
public ref T1 Item1 => ref _set1!.Get(Entity);
public ref T2 Item2 => ref _set2!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_set1 == null || _set2 == null) return false;
while (++_index < _count)
{
if (_swapped)
{
Entity = _set2!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
}
else
{
Entity = _set1.DenseEntities[_index];
if (!_set2.Contains(Entity)) continue;
}
if (!PassesWithout()) continue;
if (_world.IsSingletonEntity(Entity)) continue;
return true;
}
return false;
}
public Select2<T1, T2> GetEnumerator() => this;
public void Dispose() => _world.EndIteration();
private bool PassesWithout()
{
if (_without == null) return true;
foreach (var type in _without)
{
var set = _store.GetSet(type);
if (set != null && set.Contains(Entity))
return false;
}
return true;
}
}
/// <summary>Ref struct iterator for queries with three component types. Drives from the smallest set.</summary>
public ref struct Select3<T1, T2, T3>
where T1 : struct where T2 : struct where T3 : struct
{
private readonly World _world;
private readonly SparseSet<T1>? _set1;
private readonly SparseSet<T2>? _set2;
private readonly SparseSet<T3>? _set3;
private readonly ComponentStore _store;
private readonly HashSet<Type>? _without;
private int _index;
private readonly int _count;
private readonly int _driver; // 0 = set1, 1 = set2, 2 = set3
internal Select3(World world, Query<T1, T2, T3> query)
{
_world = world;
_store = world.Components;
_without = query.WithoutTypes;
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
_set3 = _store.GetSet(typeof(T3)) as SparseSet<T3>;
int c1 = _set1?.Count ?? 0;
int c2 = _set2?.Count ?? 0;
int c3 = _set3?.Count ?? 0;
if (c2 <= c1 && c2 <= c3) { _driver = 1; _count = c2; }
else if (c3 <= c1 && c3 <= c2) { _driver = 2; _count = c3; }
else { _driver = 0; _count = c1; }
_index = -1;
_world.BeginIteration();
}
public Entity Entity { get; private set; }
public Select3<T1, T2, T3> Current => this;
public ref T1 Item1 => ref _set1!.Get(Entity);
public ref T2 Item2 => ref _set2!.Get(Entity);
public ref T3 Item3 => ref _set3!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_set1 == null || _set2 == null || _set3 == null) return false;
while (++_index < _count)
{
switch (_driver)
{
case 0:
Entity = _set1.DenseEntities[_index];
if (!_set2.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
break;
case 1:
Entity = _set2!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
break;
case 2:
Entity = _set3!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set2.Contains(Entity)) continue;
break;
}
if (!PassesWithout()) continue;
if (_world.IsSingletonEntity(Entity)) continue;
return true;
}
return false;
}
public Select3<T1, T2, T3> GetEnumerator() => this;
public void Dispose() => _world.EndIteration();
private bool PassesWithout()
{
if (_without == null) return true;
foreach (var type in _without)
{
var set = _store.GetSet(type);
if (set != null && set.Contains(Entity))
return false;
}
return true;
}
}
/// <summary>Ref struct iterator for queries with four component types.</summary>
public ref struct Select4<T1, T2, T3, T4>
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
{
private readonly World _world;
private readonly SparseSet<T1>? _set1;
private readonly SparseSet<T2>? _set2;
private readonly SparseSet<T3>? _set3;
private readonly SparseSet<T4>? _set4;
private readonly ComponentStore _store;
private readonly HashSet<Type>? _without;
private int _index;
private readonly int _count;
private readonly int _driver;
internal Select4(World world, Query<T1, T2, T3, T4> query)
{
_world = world;
_store = world.Components;
_without = query.WithoutTypes;
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
_set3 = _store.GetSet(typeof(T3)) as SparseSet<T3>;
_set4 = _store.GetSet(typeof(T4)) as SparseSet<T4>;
int c1 = _set1?.Count ?? 0, c2 = _set2?.Count ?? 0;
int c3 = _set3?.Count ?? 0, c4 = _set4?.Count ?? 0;
int min = c1;
_driver = 0;
if (c2 < min) { min = c2; _driver = 1; }
if (c3 < min) { min = c3; _driver = 2; }
if (c4 < min) { min = c4; _driver = 3; }
_count = min;
_index = -1;
_world.BeginIteration();
}
public Entity Entity { get; private set; }
public Select4<T1, T2, T3, T4> Current => this;
public ref T1 Item1 => ref _set1!.Get(Entity);
public ref T2 Item2 => ref _set2!.Get(Entity);
public ref T3 Item3 => ref _set3!.Get(Entity);
public ref T4 Item4 => ref _set4!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_set1 == null || _set2 == null || _set3 == null || _set4 == null) return false;
while (++_index < _count)
{
switch (_driver)
{
case 0:
Entity = _set1.DenseEntities[_index];
if (!_set2.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
break;
case 1:
Entity = _set2!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
break;
case 2:
Entity = _set3!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set2.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
break;
case 3:
Entity = _set4!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set2.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
break;
}
if (!PassesWithout()) continue;
if (_world.IsSingletonEntity(Entity)) continue;
return true;
}
return false;
}
public Select4<T1, T2, T3, T4> GetEnumerator() => this;
public void Dispose() => _world.EndIteration();
private bool PassesWithout()
{
if (_without == null) return true;
foreach (var type in _without)
{
var set = _store.GetSet(type);
if (set != null && set.Contains(Entity))
return false;
}
return true;
}
}
/// <summary>Ref struct iterator for queries with five component types.</summary>
public ref struct Select5<T1, T2, T3, T4, T5>
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
{
private readonly World _world;
private readonly SparseSet<T1>? _set1;
private readonly SparseSet<T2>? _set2;
private readonly SparseSet<T3>? _set3;
private readonly SparseSet<T4>? _set4;
private readonly SparseSet<T5>? _set5;
private readonly ComponentStore _store;
private readonly HashSet<Type>? _without;
private int _index;
private readonly int _count;
private readonly int _driver;
internal Select5(World world, Query<T1, T2, T3, T4, T5> query)
{
_world = world;
_store = world.Components;
_without = query.WithoutTypes;
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
_set3 = _store.GetSet(typeof(T3)) as SparseSet<T3>;
_set4 = _store.GetSet(typeof(T4)) as SparseSet<T4>;
_set5 = _store.GetSet(typeof(T5)) as SparseSet<T5>;
int c1 = _set1?.Count ?? 0, c2 = _set2?.Count ?? 0;
int c3 = _set3?.Count ?? 0, c4 = _set4?.Count ?? 0;
int c5 = _set5?.Count ?? 0;
int min = c1;
_driver = 0;
if (c2 < min) { min = c2; _driver = 1; }
if (c3 < min) { min = c3; _driver = 2; }
if (c4 < min) { min = c4; _driver = 3; }
if (c5 < min) { min = c5; _driver = 4; }
_count = min;
_index = -1;
_world.BeginIteration();
}
public Entity Entity { get; private set; }
public Select5<T1, T2, T3, T4, T5> Current => this;
public ref T1 Item1 => ref _set1!.Get(Entity);
public ref T2 Item2 => ref _set2!.Get(Entity);
public ref T3 Item3 => ref _set3!.Get(Entity);
public ref T4 Item4 => ref _set4!.Get(Entity);
public ref T5 Item5 => ref _set5!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_set1 == null || _set2 == null || _set3 == null || _set4 == null || _set5 == null) return false;
while (++_index < _count)
{
switch (_driver)
{
case 0:
Entity = _set1.DenseEntities[_index];
if (!_set2.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
if (!_set5.Contains(Entity)) continue;
break;
case 1:
Entity = _set2!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
if (!_set5.Contains(Entity)) continue;
break;
case 2:
Entity = _set3!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set2.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
if (!_set5.Contains(Entity)) continue;
break;
case 3:
Entity = _set4!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set2.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
if (!_set5.Contains(Entity)) continue;
break;
case 4:
Entity = _set5!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set2.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
break;
}
if (!PassesWithout()) continue;
if (_world.IsSingletonEntity(Entity)) continue;
return true;
}
return false;
}
public Select5<T1, T2, T3, T4, T5> GetEnumerator() => this;
public void Dispose() => _world.EndIteration();
private bool PassesWithout()
{
if (_without == null) return true;
foreach (var type in _without)
{
var set = _store.GetSet(type);
if (set != null && set.Contains(Entity))
return false;
}
return true;
}
}
/// <summary>Ref struct iterator for queries with six component types.</summary>
public ref struct Select6<T1, T2, T3, T4, T5, T6>
where T1 : struct where T2 : struct where T3 : struct
where T4 : struct where T5 : struct where T6 : struct
{
private readonly World _world;
private readonly SparseSet<T1>? _set1;
private readonly SparseSet<T2>? _set2;
private readonly SparseSet<T3>? _set3;
private readonly SparseSet<T4>? _set4;
private readonly SparseSet<T5>? _set5;
private readonly SparseSet<T6>? _set6;
private readonly ComponentStore _store;
private readonly HashSet<Type>? _without;
private int _index;
private readonly int _count;
private readonly int _driver;
internal Select6(World world, Query<T1, T2, T3, T4, T5, T6> query)
{
_world = world;
_store = world.Components;
_without = query.WithoutTypes;
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
_set3 = _store.GetSet(typeof(T3)) as SparseSet<T3>;
_set4 = _store.GetSet(typeof(T4)) as SparseSet<T4>;
_set5 = _store.GetSet(typeof(T5)) as SparseSet<T5>;
_set6 = _store.GetSet(typeof(T6)) as SparseSet<T6>;
int c1 = _set1?.Count ?? 0, c2 = _set2?.Count ?? 0;
int c3 = _set3?.Count ?? 0, c4 = _set4?.Count ?? 0;
int c5 = _set5?.Count ?? 0, c6 = _set6?.Count ?? 0;
int min = c1;
_driver = 0;
if (c2 < min) { min = c2; _driver = 1; }
if (c3 < min) { min = c3; _driver = 2; }
if (c4 < min) { min = c4; _driver = 3; }
if (c5 < min) { min = c5; _driver = 4; }
if (c6 < min) { min = c6; _driver = 5; }
_count = min;
_index = -1;
_world.BeginIteration();
}
public Entity Entity { get; private set; }
public Select6<T1, T2, T3, T4, T5, T6> Current => this;
public ref T1 Item1 => ref _set1!.Get(Entity);
public ref T2 Item2 => ref _set2!.Get(Entity);
public ref T3 Item3 => ref _set3!.Get(Entity);
public ref T4 Item4 => ref _set4!.Get(Entity);
public ref T5 Item5 => ref _set5!.Get(Entity);
public ref T6 Item6 => ref _set6!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_set1 == null || _set2 == null || _set3 == null || _set4 == null || _set5 == null || _set6 == null) return false;
while (++_index < _count)
{
switch (_driver)
{
case 0:
Entity = _set1.DenseEntities[_index];
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;
break;
case 1:
Entity = _set2!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
if (!_set5.Contains(Entity)) continue;
if (!_set6.Contains(Entity)) continue;
break;
case 2:
Entity = _set3!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set2.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
if (!_set5.Contains(Entity)) continue;
if (!_set6.Contains(Entity)) continue;
break;
case 3:
Entity = _set4!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set2.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
if (!_set5.Contains(Entity)) continue;
if (!_set6.Contains(Entity)) continue;
break;
case 4:
Entity = _set5!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set2.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
if (!_set6.Contains(Entity)) continue;
break;
case 5:
Entity = _set6!.DenseEntities[_index];
if (!_set1.Contains(Entity)) continue;
if (!_set2.Contains(Entity)) continue;
if (!_set3.Contains(Entity)) continue;
if (!_set4.Contains(Entity)) continue;
if (!_set5.Contains(Entity)) continue;
break;
}
if (!PassesWithout()) continue;
if (_world.IsSingletonEntity(Entity)) continue;
return true;
}
return false;
}
public Select6<T1, T2, T3, T4, T5, T6> GetEnumerator() => this;
public void Dispose() => _world.EndIteration();
private bool PassesWithout()
{
if (_without == null) return true;
foreach (var type in _without)
{
var set = _store.GetSet(type);
if (set != null && set.Contains(Entity))
return false;
}
return true;
}
}

View File

@ -73,17 +73,19 @@ public static class WorldSerializer
foreach (var desc in ComponentRegistry.Descriptors)
lookup[desc.TypeName] = desc;
// First pass: identify which entities are singleton backings.
// Singletons are registered in ComponentRegistry with a known marker.
var singletonTypes = new HashSet<Type>();
foreach (var desc in ComponentRegistry.Descriptors)
{
if (desc.IsSingleton)
singletonTypes.Add(desc.Type);
}
foreach (var es in snapshot.Entities)
{
var entity = world.CreateEntity(es.Entity);
// If the loaded entity is the singleton entity, ensure the
// singleton infrastructure is initialized so that subsequent
// SetSingleton/GetSingleton calls work correctly.
if (entity.Id == World.SingletonEntity.Id)
{
world.EnsureSingleton();
}
bool isSingleton = false;
foreach (var ce in es.Components)
{
@ -93,10 +95,25 @@ public static class WorldSerializer
$"Ensure the component type is used with the World " +
$"API so the source generator can discover it.");
if (desc.IsSingleton)
isSingleton = true;
// Deserialize and add via the typed internal method.
var component = desc.Deserialize(ce.Data);
world.AddComponentBoxed(entity, component, desc.Type);
}
// If the entity has a singleton component, register it.
if (isSingleton)
{
foreach (var ce in es.Components)
{
if (lookup.TryGetValue(ce.TypeName, out var desc) && desc.IsSingleton)
{
world.RegisterSingletonEntity(desc.Type, entity);
}
}
}
}
}
}