From 737136e2efe324089349c30904d99fb75cced34c Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 21 Jul 2026 00:21:01 +0800 Subject: [PATCH] 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 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 structs. - Update tests to use the new Select API. --- OECS.Tests/CommandTests.cs | 38 +- OECS.Tests/EdgeCaseTests.cs | 172 +------- OECS.Tests/QueryAndSystemTests.cs | 157 ++----- OECS.Tests/ReactivityTests.cs | 2 +- OECS.Tests/RelationshipTests.cs | 9 +- OECS.Tests/SingletonTests.cs | 11 +- OECS/ChangeBuffer.cs | 113 ++++- OECS/ComponentDescriptor.cs | 10 +- OECS/EntityAllocator.cs | 13 +- OECS/EntityIterator.cs | 327 -------------- OECS/ForEachAction.cs | 39 -- OECS/Query.cs | 100 +++++ OECS/QueryBuilder.cs | 39 -- OECS/QueryDescriptor.cs | 53 --- OECS/QueryExecutor.cs | 507 ---------------------- OECS/World.cs | 276 ++++-------- OECS/WorldQueryExtensions.cs | 682 ++++++++++++++++++++++++++++++ OECS/WorldSerializer.cs | 33 +- 18 files changed, 1066 insertions(+), 1515 deletions(-) delete mode 100644 OECS/EntityIterator.cs delete mode 100644 OECS/ForEachAction.cs create mode 100644 OECS/Query.cs delete mode 100644 OECS/QueryBuilder.cs delete mode 100644 OECS/QueryDescriptor.cs delete mode 100644 OECS/QueryExecutor.cs create mode 100644 OECS/WorldQueryExtensions.cs diff --git a/OECS.Tests/CommandTests.cs b/OECS.Tests/CommandTests.cs index f5371a0..02f693b 100644 --- a/OECS.Tests/CommandTests.cs +++ b/OECS.Tests/CommandTests.cs @@ -114,13 +114,11 @@ public class CommandTests world.ExecuteCommands(); - var query = world.Query().With().With().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()) { - 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().Build(); var positions = new List<(float X, float Y)>(); - - world.ForEach(query, (Entity e, ref TestPosition pos) => + foreach (var it in world.Select()) { - 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().Build(); var count = 0; - world.ForEach(query, (Entity e, ref TestPosition pos) => count++); + foreach (var it in world.Select()) + 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().Build(); var count = 0; - world.ForEach(query, (Entity e, ref TestPosition pos) => count++); + foreach (var it in world.Select()) + 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().Build(); - } - public void Run(World world) { - world.ForEach(_query, (Entity e, ref TestPosition pos) => - { + foreach (var it in world.Select()) SeenCount++; - }); } } -} +} \ No newline at end of file diff --git a/OECS.Tests/EdgeCaseTests.cs b/OECS.Tests/EdgeCaseTests.cs index 872ed61..dc2a440 100644 --- a/OECS.Tests/EdgeCaseTests.cs +++ b/OECS.Tests/EdgeCaseTests.cs @@ -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().With().Without().Build(); - var q2 = new QueryBuilder().With().With().Without().Build(); - - q1.Should().Be(q2); - q1.GetHashCode().Should().Be(q2.GetHashCode()); - } - - [Fact] - public void QueryDescriptor_DifferentWith_AreNotEqual() - { - var q1 = new QueryBuilder().With().Build(); - var q2 = new QueryBuilder().With().Build(); - - q1.Should().NotBe(q2); - } - - [Fact] - public void QueryDescriptor_DifferentWithout_AreNotEqual() - { - var q1 = new QueryBuilder().With().Without().Build(); - var q2 = new QueryBuilder().With().Without().Build(); - - q1.Should().NotBe(q2); - } - - [Fact] - public void QueryDescriptor_DifferentCount_AreNotEqual() - { - var q1 = new QueryBuilder().With().Build(); - var q2 = new QueryBuilder().With().With().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().With().With().With() - .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()) { 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().With().With().With().With() - .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()) { 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().With().With() - .With().With().With() - .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()) { count++; - }); + } count.Should().Be(1); } @@ -282,13 +229,13 @@ public class EdgeCaseTests world.AddComponent(b, new Frozen()); // Without filters out entity b which has Frozen. - var query = world.Query().Without().Build(); + var query = new Query().Without(); var results = new List(); - 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().Build(); var seen = new List(); - world.ForEach(query, (Entity e, ref Position pos) => + foreach (var it in world.Select()) { - 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().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()) { - 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().With().Build(); - - var act = () => - { - world.ForEach(query, (Entity e, ref Position pos) => { }); - }; - act.Should().Throw() - .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().With().With().Build(); - - var act = () => - { - world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel) => { }); - }; - act.Should().Throw() - .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().Build(); - - var act = () => - { - world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel) => { }); - }; - act.Should().Throw() - .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().With().Build(); - - var act = () => - { - world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel) => { }); + world.DestroyEntity(it.Entity); + } }; act.Should().NotThrow(); } diff --git a/OECS.Tests/QueryAndSystemTests.cs b/OECS.Tests/QueryAndSystemTests.cs index eb437d0..934a2ff 100644 --- a/OECS.Tests/QueryAndSystemTests.cs +++ b/OECS.Tests/QueryAndSystemTests.cs @@ -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().Build(); var results = new List(); - - world.ForEach(query, (Entity entity, ref Position pos) => - { - results.Add(entity); - }); + foreach (var it in world.Select()) + 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().With().Build(); var results = new List(); - - world.ForEach(query, (Entity entity, ref Position pos, ref Velocity vel) => - { - results.Add(entity); - }); + foreach (var it in world.Select()) + 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().Without().Build(); + var query = new Query().Without(); var results = new List(); - - 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().Build(); var count = 0; - - world.ForEach(query, (Entity entity, ref Position pos) => - { + foreach (var it in world.Select()) 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().Build(); - - world.ForEach(query, (Entity e, ref Position pos) => - { - pos.X = 42; - }); + foreach (var it in world.Select()) + it.Item1.X = 42; ref var pos = ref world.GetComponent(entity); pos.X.Should().Be(42); @@ -128,13 +108,9 @@ public class QueryTests world.DestroyEntity(a); - var query = world.Query().With().Build(); var results = new List(); - - world.ForEach(query, (Entity entity, ref Position pos) => - { - results.Add(entity); - }); + foreach (var it in world.Select()) + 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() - .With() - .With() - .Build(); - var count = 0; - world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel, ref Health hp) => + foreach (var it in world.Select()) { 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(); + 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().With().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()) { - 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(); - - 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 _order; - - public OrderTrackingSystem(World world, int id, List 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(); - } -} +} \ No newline at end of file diff --git a/OECS.Tests/ReactivityTests.cs b/OECS.Tests/ReactivityTests.cs index bae31be..c154290 100644 --- a/OECS.Tests/ReactivityTests.cs +++ b/OECS.Tests/ReactivityTests.cs @@ -208,7 +208,7 @@ public class ReactivityTests public void ObserveQuery_OnlyReceivesMatchingChanges() { var world = new World(); - var query = world.Query().With().Without().Build(); + var query = new Query().Without(); var collector = new ChangeCollector(); using var sub = world.ObserveQuery(query).Subscribe(collector.ToObserver()); diff --git a/OECS.Tests/RelationshipTests.cs b/OECS.Tests/RelationshipTests.cs index 70051dc..2b20ea2 100644 --- a/OECS.Tests/RelationshipTests.cs +++ b/OECS.Tests/RelationshipTests.cs @@ -186,14 +186,13 @@ public class RelationshipTests Target = parent }); - var query = world.Query().With>().Build(); var results = new List(); - world.ForEach(query, (Entity e, ref Relationship rel) => + foreach (var it in world.Select>()) { - results.Add(e); - rel.Target.Should().Be(parent); - }); + results.Add(it.Entity); + it.Item1.Target.Should().Be(parent); + } results.Should().BeEquivalentTo([child]); } diff --git a/OECS.Tests/SingletonTests.cs b/OECS.Tests/SingletonTests.cs index 0e258c7..d5570d6 100644 --- a/OECS.Tests/SingletonTests.cs +++ b/OECS.Tests/SingletonTests.cs @@ -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().Build(); var results = new List(); - world.ForEach(query, (Entity e, ref GameConfig cfg) => + foreach (var it in world.Select()) { - 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().Should().BeTrue(); } [Fact] diff --git a/OECS/ChangeBuffer.cs b/OECS/ChangeBuffer.cs index 1f77414..c3df728 100644 --- a/OECS/ChangeBuffer.cs +++ b/OECS/ChangeBuffer.cs @@ -15,7 +15,7 @@ internal class ChangeBuffer private readonly Subject _entitySubject = new(); private readonly Dictionary _componentSubjects = new(); - private readonly Dictionary _querySubjects = new(); + private readonly Dictionary _querySubjects = new(); /// /// Wraps a with a subscriber count so that @@ -27,6 +27,42 @@ internal class ChangeBuffer public int SubscriberCount; } + /// + /// Identifies a query subscription for cleanup tracking. + /// + private readonly struct QueryKey : IEquatable + { + 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(); + } + } + /// /// The change set currently accumulating. Cleared after each . /// @@ -101,14 +137,33 @@ internal class ChangeBuffer } /// - /// 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. /// - public Observable ObserveQuery(QueryDescriptor query) + public Observable ObserveQuery(Query query) + where T1 : struct { - if (!_querySubjects.TryGetValue(query, out var tracked)) + var key = MakeKey(query); + return SubscribeQuery(key); + } + + /// + /// Returns an observable that emits component-level changes matching + /// the given query. + /// + public Observable ObserveQuery(Query query) + where T1 : struct where T2 : struct + { + var key = MakeKey(query); + return SubscribeQuery(key); + } + + private Observable 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(Query query) where T1 : struct + { + var without = query.WithoutTypes; + return new QueryKey( + [typeof(T1)], + without != null ? [.. without] : []); + } + + private static QueryKey MakeKey(Query query) + where T1 : struct where T2 : struct + { + var without = query.WithoutTypes; + return new QueryKey( + [typeof(T1), typeof(T2)], + without != null ? [.. without] : []); + } + /// /// Wraps an observable so that 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; } -} +} \ No newline at end of file diff --git a/OECS/ComponentDescriptor.cs b/OECS/ComponentDescriptor.cs index 6da6fd6..73fbcaa 100644 --- a/OECS/ComponentDescriptor.cs +++ b/OECS/ComponentDescriptor.cs @@ -34,17 +34,25 @@ public sealed class ComponentDescriptor /// public Func Deserialize { get; } + /// + /// True if this component type is used as a singleton (via SetSingleton). + /// Set by the source generator at compile time. + /// + public bool IsSingleton { get; } + public ComponentDescriptor( string typeName, Type type, Func serialize, Action deserializeAndAdd, - Func deserialize) + Func deserialize, + bool isSingleton = false) { TypeName = typeName; Type = type; Serialize = serialize; DeserializeAndAdd = deserializeAndAdd; Deserialize = deserialize; + IsSingleton = isSingleton; } } \ No newline at end of file diff --git a/OECS/EntityAllocator.cs b/OECS/EntityAllocator.cs index 8e73191..c454030 100644 --- a/OECS/EntityAllocator.cs +++ b/OECS/EntityAllocator.cs @@ -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 _freeIds; @@ -63,16 +63,6 @@ internal class EntityAllocator _freeIds.Push(id); } - /// - /// Registers the singleton entity so that returns true for it. - /// Called once by when the first singleton accessor is used. - /// - public void RegisterSingleton(Entity singleton) - { - EnsureCapacity(singleton.Id); - _versions[singleton.Id] = (byte)singleton.Version; - } - /// /// Reserves a specific entity ID and version for deserialization. /// Advances 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; } diff --git a/OECS/EntityIterator.cs b/OECS/EntityIterator.cs deleted file mode 100644 index 052fd89..0000000 --- a/OECS/EntityIterator.cs +++ /dev/null @@ -1,327 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OECS; - -/// -/// Provides zero-allocation ref struct iterators for querying entities. -/// Use with foreach or manual while (iter.MoveNext()). -/// Supports break, continue, and early returns. -/// -public static class EntityIterator -{ - /// - /// Returns an iterator over entities matching the query, with ref access - /// to one component type. - /// - public static Select1 Select( - this World world, QueryDescriptor query) - where T1 : struct - { - return new Select1(world, query); - } - - /// - /// Returns an iterator over entities that have a component of type - /// . No Without filter is applied. - /// - public static Select1 Select( - this World world) - where T1 : struct - { - return new Select1(world, new QueryDescriptor(new HashSet(), new HashSet())); - } - - /// - /// Returns an iterator over entities matching the query, with ref access - /// to two component types. - /// - public static Select2 Select( - this World world, QueryDescriptor query) - where T1 : struct where T2 : struct - { - return new Select2(world, query); - } - - /// - /// Returns an iterator over entities that have both components of type - /// and . - /// No Without filter is applied. - /// - public static Select2 Select( - this World world) - where T1 : struct where T2 : struct - { - return new Select2(world, new QueryDescriptor(new HashSet(), new HashSet())); - } - - /// - /// Returns an iterator over entities matching the query, with ref access - /// to three component types. - /// - public static Select3 Select( - this World world, QueryDescriptor query) - where T1 : struct where T2 : struct where T3 : struct - { - return new Select3(world, query); - } - - /// - /// Returns an iterator over entities that have all three components of type - /// , , and - /// . No Without filter is applied. - /// - public static Select3 Select( - this World world) - where T1 : struct where T2 : struct where T3 : struct - { - return new Select3(world, new QueryDescriptor(new HashSet(), new HashSet())); - } -} - -/// -/// Ref struct iterator for queries with one component type. -/// -public ref struct Select1 where T1 : struct -{ - private readonly World _world; - private readonly SparseSet? _set; - private readonly ComponentStore _store; - private readonly IReadOnlySet _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; - _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 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; - } -} - -/// -/// Ref struct iterator for queries with two component types. -/// Drives iteration from the smaller sparse set to minimize probes. -/// -public ref struct Select2 - where T1 : struct where T2 : struct -{ - private readonly World _world; - private readonly SparseSet? _set1; - private readonly SparseSet? _set2; - private readonly ComponentStore _store; - private readonly IReadOnlySet _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; - _set2 = _store.GetSet(typeof(T2)) as SparseSet; - - 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 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; - } -} - -/// -/// Ref struct iterator for queries with three component types. -/// Drives iteration from the smallest sparse set to minimize probes. -/// -public ref struct Select3 - where T1 : struct where T2 : struct where T3 : struct -{ - private readonly World _world; - private readonly SparseSet? _set1; - private readonly SparseSet? _set2; - private readonly SparseSet? _set3; - private readonly ComponentStore _store; - private readonly IReadOnlySet _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; - _set2 = _store.GetSet(typeof(T2)) as SparseSet; - _set3 = _store.GetSet(typeof(T3)) as SparseSet; - - 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 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; - } -} \ No newline at end of file diff --git a/OECS/ForEachAction.cs b/OECS/ForEachAction.cs deleted file mode 100644 index 2182adb..0000000 --- a/OECS/ForEachAction.cs +++ /dev/null @@ -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. - -/// -/// Callback for iterating entities with one component type. -/// -public delegate void ForEachAction(Entity entity, ref T1 c1) where T1 : struct; - -/// -/// Callback for iterating entities with two component types. -/// -public delegate void ForEachAction(Entity entity, ref T1 c1, ref T2 c2) - where T1 : struct where T2 : struct; - -/// -/// Callback for iterating entities with three component types. -/// -public delegate void ForEachAction(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3) - where T1 : struct where T2 : struct where T3 : struct; - -/// -/// Callback for iterating entities with four component types. -/// -public delegate void ForEachAction(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; - -/// -/// Callback for iterating entities with five component types. -/// -public delegate void ForEachAction(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; - -/// -/// Callback for iterating entities with six component types. -/// -public delegate void ForEachAction(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; \ No newline at end of file diff --git a/OECS/Query.cs b/OECS/Query.cs new file mode 100644 index 0000000..ccea93a --- /dev/null +++ b/OECS/Query.cs @@ -0,0 +1,100 @@ +namespace OECS; + +/// +/// Describes a query over the ECS world with one required component type. +/// Add optional Without filters via the fluent API. +/// +public struct Query where T1 : struct +{ + internal HashSet? WithoutTypes; + + /// + /// Excludes entities that have a component of type . + /// + public Query Without() where W : struct + { + WithoutTypes ??= new HashSet(); + WithoutTypes.Add(typeof(W)); + return this; + } +} + +/// +/// Describes a query over the ECS world with two required component types. +/// +public struct Query where T1 : struct where T2 : struct +{ + internal HashSet? WithoutTypes; + + public Query Without() where W : struct + { + WithoutTypes ??= new HashSet(); + WithoutTypes.Add(typeof(W)); + return this; + } +} + +/// +/// Describes a query over the ECS world with three required component types. +/// +public struct Query + where T1 : struct where T2 : struct where T3 : struct +{ + internal HashSet? WithoutTypes; + + public Query Without() where W : struct + { + WithoutTypes ??= new HashSet(); + WithoutTypes.Add(typeof(W)); + return this; + } +} + +/// +/// Describes a query over the ECS world with four required component types. +/// +public struct Query + where T1 : struct where T2 : struct where T3 : struct where T4 : struct +{ + internal HashSet? WithoutTypes; + + public Query Without() where W : struct + { + WithoutTypes ??= new HashSet(); + WithoutTypes.Add(typeof(W)); + return this; + } +} + +/// +/// Describes a query over the ECS world with five required component types. +/// +public struct Query + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct +{ + internal HashSet? WithoutTypes; + + public Query Without() where W : struct + { + WithoutTypes ??= new HashSet(); + WithoutTypes.Add(typeof(W)); + return this; + } +} + +/// +/// Describes a query over the ECS world with six required component types. +/// +public struct Query + where T1 : struct where T2 : struct where T3 : struct + where T4 : struct where T5 : struct where T6 : struct +{ + internal HashSet? WithoutTypes; + + public Query Without() where W : struct + { + WithoutTypes ??= new HashSet(); + WithoutTypes.Add(typeof(W)); + return this; + } +} \ No newline at end of file diff --git a/OECS/QueryBuilder.cs b/OECS/QueryBuilder.cs deleted file mode 100644 index cde58e3..0000000 --- a/OECS/QueryBuilder.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace OECS; - -/// -/// Fluent builder for constructing instances. -/// Returned by . -/// -public class QueryBuilder -{ - private readonly HashSet _with = new(); - private readonly HashSet _without = new(); - - /// - /// Requires entities to have a component of type . - /// - public QueryBuilder With() where T : struct - { - _with.Add(typeof(T)); - return this; - } - - /// - /// Excludes entities that have a component of type . - /// - public QueryBuilder Without() where T : struct - { - _without.Add(typeof(T)); - return this; - } - - /// - /// Builds the query descriptor. - /// - public QueryDescriptor Build() - { - return new QueryDescriptor( - new HashSet(_with), - new HashSet(_without)); - } -} \ No newline at end of file diff --git a/OECS/QueryDescriptor.cs b/OECS/QueryDescriptor.cs deleted file mode 100644 index 7feaeea..0000000 --- a/OECS/QueryDescriptor.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace OECS; - -/// -/// Describes a query over the ECS world. -/// Composed of a set of required component types ("with") and -/// a set of excluded component types ("without"). -/// -public class QueryDescriptor -{ - /// - /// Component types that must be present on matching entities. - /// - public IReadOnlySet With { get; } - - /// - /// Component types that must NOT be present on matching entities. - /// - public IReadOnlySet Without { get; } - - private readonly int _hashCode; - - internal QueryDescriptor(HashSet with, HashSet without) - { - With = with; - Without = without; - _hashCode = ComputeHashCode(); - } - - /// - /// Returns true if this query has no "with" components (matches nothing). - /// - 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(); - } -} \ No newline at end of file diff --git a/OECS/QueryExecutor.cs b/OECS/QueryExecutor.cs deleted file mode 100644 index c20f69c..0000000 --- a/OECS/QueryExecutor.cs +++ /dev/null @@ -1,507 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OECS; - -/// -/// Provides query execution logic for . -/// 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. -/// -internal static class QueryExecutor -{ - private const uint SingletonId = 1; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool PassesWithoutFilter(ComponentStore store, Entity entity, IReadOnlySet 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( - ComponentStore store, - QueryDescriptor query, - ForEachAction action) - where T1 : struct - { - var set = store.GetSet(typeof(T1)) as SparseSet; - 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( - ComponentStore store, - QueryDescriptor query, - ForEachAction action) - where T1 : struct where T2 : struct - { - var set1 = store.GetSet(typeof(T1)) as SparseSet; - if (set1 == null) return; - var set2 = store.GetSet(typeof(T2)) as SparseSet; - 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( - SparseSet driveSet, - SparseSet otherSet, - ComponentStore store, - IReadOnlySet withoutTypes, - ForEachAction 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( - SparseSet driveSet, - SparseSet otherSet, - ComponentStore store, - IReadOnlySet withoutTypes, - ForEachAction 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( - ComponentStore store, - QueryDescriptor query, - ForEachAction action) - where T1 : struct where T2 : struct where T3 : struct - { - var set1 = store.GetSet(typeof(T1)) as SparseSet; - if (set1 == null) return; - var set2 = store.GetSet(typeof(T2)) as SparseSet; - if (set2 == null) return; - var set3 = store.GetSet(typeof(T3)) as SparseSet; - 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( - ComponentStore store, - QueryDescriptor query, - ForEachAction action) - where T1 : struct where T2 : struct where T3 : struct where T4 : struct - { - var set1 = store.GetSet(typeof(T1)) as SparseSet; - if (set1 == null) return; - var set2 = store.GetSet(typeof(T2)) as SparseSet; - if (set2 == null) return; - var set3 = store.GetSet(typeof(T3)) as SparseSet; - if (set3 == null) return; - var set4 = store.GetSet(typeof(T4)) as SparseSet; - 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( - ComponentStore store, - QueryDescriptor query, - ForEachAction action) - where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct - { - var set1 = store.GetSet(typeof(T1)) as SparseSet; - if (set1 == null) return; - var set2 = store.GetSet(typeof(T2)) as SparseSet; - if (set2 == null) return; - var set3 = store.GetSet(typeof(T3)) as SparseSet; - if (set3 == null) return; - var set4 = store.GetSet(typeof(T4)) as SparseSet; - if (set4 == null) return; - var set5 = store.GetSet(typeof(T5)) as SparseSet; - 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( - ComponentStore store, - QueryDescriptor query, - ForEachAction 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; - if (set1 == null) return; - var set2 = store.GetSet(typeof(T2)) as SparseSet; - if (set2 == null) return; - var set3 = store.GetSet(typeof(T3)) as SparseSet; - if (set3 == null) return; - var set4 = store.GetSet(typeof(T4)) as SparseSet; - if (set4 == null) return; - var set5 = store.GetSet(typeof(T5)) as SparseSet; - if (set5 == null) return; - var set6 = store.GetSet(typeof(T6)) as SparseSet; - 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)); - } - } - } -} \ No newline at end of file diff --git a/OECS/World.cs b/OECS/World.cs index 2b1f675..19bdcc2 100644 --- a/OECS/World.cs +++ b/OECS/World.cs @@ -11,17 +11,12 @@ namespace OECS; /// public class World : IDisposable { - /// - /// The reserved entity ID for the singleton entity. - /// - 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 _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 /// /// Marks a component of type on the given entity - /// as modified. Only needed outside of iteration scopes — during - /// or a Select loop, components accessed - /// via are auto-marked. + /// as modified. Only needed outside of iteration scopes — during a + /// Select loop, components accessed via + /// are auto-marked. /// public void MarkModified(Entity entity) where T : struct { @@ -355,11 +346,21 @@ public class World : IDisposable } /// - /// 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. /// - public Observable ObserveQuery(QueryDescriptor query) + public Observable ObserveQuery(Query query = default) + where T1 : struct + { + return _changes.ObserveQuery(query); + } + + /// + /// Returns an observable that emits component-level changes matching + /// the given query. + /// + public Observable ObserveQuery(Query query = default) + where T1 : struct where T2 : struct { return _changes.ObserveQuery(query); } @@ -368,13 +369,16 @@ public class World : IDisposable /// /// Sets (adds or replaces) a singleton component of type . - /// 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. /// public void SetSingleton(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); } /// @@ -383,8 +387,7 @@ public class World : IDisposable /// public ref T GetSingleton() where T : struct { - EnsureSingleton(); - return ref GetComponent(SingletonEntity); + return ref GetComponent(GetSingletonEntity()); } /// @@ -393,8 +396,7 @@ public class World : IDisposable /// public T ReadSingleton() where T : struct { - EnsureSingleton(); - return ReadComponent(SingletonEntity); + return ReadComponent(GetSingletonEntity()); } /// @@ -402,7 +404,8 @@ public class World : IDisposable /// public bool HasSingleton() where T : struct { - return HasComponent(SingletonEntity); + return _singletonEntities.TryGetValue(typeof(T), out var entity) + && HasComponent(entity); } /// @@ -411,20 +414,22 @@ public class World : IDisposable /// public void RemoveSingleton() where T : struct { - RemoveComponent(SingletonEntity); + if (_singletonEntities.TryGetValue(typeof(T), out var entity)) + RemoveComponent(entity); } - internal void EnsureSingleton() + /// + /// Gets the entity backing the singleton of type , + /// creating it if it doesn't exist yet. + /// + private Entity GetSingletonEntity() 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 ────────────────────────────────────────────────────── - - /// - /// Returns a new for constructing queries. - /// - public QueryBuilder Query() => new(); - - /// - /// 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. - /// - public void ForEach( - QueryDescriptor query, - ForEachAction action) - where T1 : struct - { - ValidateQueryTypes(query, typeof(T1)); - BeginIteration(); - try - { - QueryExecutor.ForEach(_components, query, action); - } - finally - { - EndIteration(); - } - } - - /// - /// Iterates all entities matching the query, providing ref access to - /// two component types. Structural mutations are deferred. - /// - public void ForEach( - QueryDescriptor query, - ForEachAction action) - where T1 : struct where T2 : struct - { - ValidateQueryTypes(query, typeof(T1), typeof(T2)); - BeginIteration(); - try - { - QueryExecutor.ForEach(_components, query, action); - } - finally - { - EndIteration(); - } - } - - /// - /// Iterates all entities matching the query, providing ref access to - /// three component types. Structural mutations are deferred. - /// - public void ForEach( - QueryDescriptor query, - ForEachAction 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(); - } - } - - /// - /// Iterates all entities matching the query, providing ref access to - /// four component types. Structural mutations are deferred. - /// - public void ForEach( - QueryDescriptor query, - ForEachAction 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(); - } - } - - /// - /// Iterates all entities matching the query, providing ref access to - /// five component types. Structural mutations are deferred. - /// - public void ForEach( - QueryDescriptor query, - ForEachAction 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(); - } - } - - /// - /// Iterates all entities matching the query, providing ref access to - /// six component types. Structural mutations are deferred. - /// - public void ForEach( - QueryDescriptor query, - ForEachAction 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 ────────────────────────────────────────── /// /// Begins an iteration scope. Structural mutations are buffered @@ -670,6 +542,33 @@ public class World : IDisposable /// internal ComponentStore Components => _components; + /// + /// Gets the entity that backs the singleton of type , + /// or if the singleton has not been set. + /// + internal Entity GetSingletonEntityOrNull() where T : struct + { + _singletonEntities.TryGetValue(typeof(T), out var entity); + return entity; + } + + /// + /// Registers an existing entity as the backing entity for a singleton component. + /// Used during deserialization to restore singleton state. + /// + internal void RegisterSingletonEntity(Type componentType, Entity entity) + { + _singletonEntities[componentType] = entity; + } + + /// + /// Returns true if the given entity is a singleton entity (backs any singleton component). + /// + 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."); } - /// - /// 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. - /// - 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()."); - } - // ── Cleanup ─────────────────────────────────────────────────────── public void Dispose() @@ -720,4 +586,4 @@ public class World : IDisposable _disposed = true; _changes.Dispose(); } -} +} \ No newline at end of file diff --git a/OECS/WorldQueryExtensions.cs b/OECS/WorldQueryExtensions.cs new file mode 100644 index 0000000..811cc20 --- /dev/null +++ b/OECS/WorldQueryExtensions.cs @@ -0,0 +1,682 @@ +using System.Runtime.CompilerServices; + +namespace OECS; + +/// +/// Provides zero-allocation ref struct iterators and entity-finding +/// extension methods for querying a . +/// +public static class WorldQueryExtensions +{ + // ── Select (ref struct enumerators) ────────────────────────────── + + public static Select1 Select(this World world, Query query = default) + where T1 : struct + { + return new Select1(world, query); + } + + public static Select2 Select(this World world, Query query = default) + where T1 : struct where T2 : struct + { + return new Select2(world, query); + } + + public static Select3 Select(this World world, Query query = default) + where T1 : struct where T2 : struct where T3 : struct + { + return new Select3(world, query); + } + + public static Select4 Select(this World world, Query query = default) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct + { + return new Select4(world, query); + } + + public static Select5 Select(this World world, Query query = default) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct + { + return new Select5(world, query); + } + + public static Select6 Select(this World world, Query query = default) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct + { + return new Select6(world, query); + } + + // ── FindEntity / FindEntities ──────────────────────────────────── + + public static Entity FindEntity(this World world, Query query = default) + where T1 : struct + { + using var iter = new Select1(world, query); + return iter.MoveNext() ? iter.Entity : Entity.Null; + } + + public static Entity FindEntity(this World world, Query query = default) + where T1 : struct where T2 : struct + { + using var iter = new Select2(world, query); + return iter.MoveNext() ? iter.Entity : Entity.Null; + } + + public static Entity FindEntity(this World world, Query query = default) + where T1 : struct where T2 : struct where T3 : struct + { + using var iter = new Select3(world, query); + return iter.MoveNext() ? iter.Entity : Entity.Null; + } + + public static List FindEntities(this World world, Query query = default) + where T1 : struct + { + var list = new List(); + using var iter = new Select1(world, query); + while (iter.MoveNext()) list.Add(iter.Entity); + return list; + } + + public static List FindEntities(this World world, Query query = default) + where T1 : struct where T2 : struct + { + var list = new List(); + using var iter = new Select2(world, query); + while (iter.MoveNext()) list.Add(iter.Entity); + return list; + } + + public static List FindEntities(this World world, Query query = default) + where T1 : struct where T2 : struct where T3 : struct + { + var list = new List(); + using var iter = new Select3(world, query); + while (iter.MoveNext()) list.Add(iter.Entity); + return list; + } +} + +// ── Ref struct enumerators ─────────────────────────────────────────── + +/// Ref struct iterator for queries with one component type. +public ref struct Select1 where T1 : struct +{ + private readonly World _world; + private readonly SparseSet? _set; + private readonly ComponentStore _store; + private readonly HashSet? _without; + private int _index; + private readonly int _count; + + internal Select1(World world, Query query) + { + _world = world; + _store = world.Components; + _without = query.WithoutTypes; + _set = _store.GetSet(typeof(T1)) as SparseSet; + _count = _set?.Count ?? 0; + _index = -1; + _world.BeginIteration(); + } + + /// The current entity handle. + public Entity Entity { get; private set; } + + /// The current enumerator (required for foreach duck-typing). + public Select1 Current => this; + + /// Read-write reference to the current entity's component of type . + 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 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; + } +} + +/// Ref struct iterator for queries with two component types. Drives from the smaller set. +public ref struct Select2 where T1 : struct where T2 : struct +{ + private readonly World _world; + private readonly SparseSet? _set1; + private readonly SparseSet? _set2; + private readonly ComponentStore _store; + private readonly HashSet? _without; + private int _index; + private readonly int _count; + private readonly bool _swapped; + + internal Select2(World world, Query query) + { + _world = world; + _store = world.Components; + _without = query.WithoutTypes; + _set1 = _store.GetSet(typeof(T1)) as SparseSet; + _set2 = _store.GetSet(typeof(T2)) as SparseSet; + + 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 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 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; + } +} + +/// Ref struct iterator for queries with three component types. Drives from the smallest set. +public ref struct Select3 + where T1 : struct where T2 : struct where T3 : struct +{ + private readonly World _world; + private readonly SparseSet? _set1; + private readonly SparseSet? _set2; + private readonly SparseSet? _set3; + private readonly ComponentStore _store; + private readonly HashSet? _without; + private int _index; + private readonly int _count; + private readonly int _driver; // 0 = set1, 1 = set2, 2 = set3 + + internal Select3(World world, Query query) + { + _world = world; + _store = world.Components; + _without = query.WithoutTypes; + _set1 = _store.GetSet(typeof(T1)) as SparseSet; + _set2 = _store.GetSet(typeof(T2)) as SparseSet; + _set3 = _store.GetSet(typeof(T3)) as SparseSet; + + 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 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 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; + } +} + +/// Ref struct iterator for queries with four component types. +public ref struct Select4 + where T1 : struct where T2 : struct where T3 : struct where T4 : struct +{ + private readonly World _world; + private readonly SparseSet? _set1; + private readonly SparseSet? _set2; + private readonly SparseSet? _set3; + private readonly SparseSet? _set4; + private readonly ComponentStore _store; + private readonly HashSet? _without; + private int _index; + private readonly int _count; + private readonly int _driver; + + internal Select4(World world, Query query) + { + _world = world; + _store = world.Components; + _without = query.WithoutTypes; + _set1 = _store.GetSet(typeof(T1)) as SparseSet; + _set2 = _store.GetSet(typeof(T2)) as SparseSet; + _set3 = _store.GetSet(typeof(T3)) as SparseSet; + _set4 = _store.GetSet(typeof(T4)) as SparseSet; + + 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 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 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; + } +} + +/// Ref struct iterator for queries with five component types. +public ref struct Select5 + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct +{ + private readonly World _world; + private readonly SparseSet? _set1; + private readonly SparseSet? _set2; + private readonly SparseSet? _set3; + private readonly SparseSet? _set4; + private readonly SparseSet? _set5; + private readonly ComponentStore _store; + private readonly HashSet? _without; + private int _index; + private readonly int _count; + private readonly int _driver; + + internal Select5(World world, Query query) + { + _world = world; + _store = world.Components; + _without = query.WithoutTypes; + _set1 = _store.GetSet(typeof(T1)) as SparseSet; + _set2 = _store.GetSet(typeof(T2)) as SparseSet; + _set3 = _store.GetSet(typeof(T3)) as SparseSet; + _set4 = _store.GetSet(typeof(T4)) as SparseSet; + _set5 = _store.GetSet(typeof(T5)) as SparseSet; + + 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 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 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; + } +} + +/// Ref struct iterator for queries with six component types. +public ref struct Select6 + 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? _set1; + private readonly SparseSet? _set2; + private readonly SparseSet? _set3; + private readonly SparseSet? _set4; + private readonly SparseSet? _set5; + private readonly SparseSet? _set6; + private readonly ComponentStore _store; + private readonly HashSet? _without; + private int _index; + private readonly int _count; + private readonly int _driver; + + internal Select6(World world, Query query) + { + _world = world; + _store = world.Components; + _without = query.WithoutTypes; + _set1 = _store.GetSet(typeof(T1)) as SparseSet; + _set2 = _store.GetSet(typeof(T2)) as SparseSet; + _set3 = _store.GetSet(typeof(T3)) as SparseSet; + _set4 = _store.GetSet(typeof(T4)) as SparseSet; + _set5 = _store.GetSet(typeof(T5)) as SparseSet; + _set6 = _store.GetSet(typeof(T6)) as SparseSet; + + 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 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 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; + } +} \ No newline at end of file diff --git a/OECS/WorldSerializer.cs b/OECS/WorldSerializer.cs index 302b8be..ddec987 100644 --- a/OECS/WorldSerializer.cs +++ b/OECS/WorldSerializer.cs @@ -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(); + 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); + } + } + } } } } \ No newline at end of file