From dddddbdbd61be803bd9bbc530cef156f28dcdfe0 Mon Sep 17 00:00:00 2001 From: hypercross Date: Sat, 18 Jul 2026 20:13:25 +0800 Subject: [PATCH] perf: cache hash code in QueryDescriptor fix: notify change buffer when removing components in World docs: add caution about structural changes during ForEach fix: prevent multiple disposals in World --- src/OECS/QueryDescriptor.cs | 7 ++++++- src/OECS/World.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/OECS/QueryDescriptor.cs b/src/OECS/QueryDescriptor.cs index 4caad07..7feaeea 100644 --- a/src/OECS/QueryDescriptor.cs +++ b/src/OECS/QueryDescriptor.cs @@ -17,10 +17,13 @@ public class QueryDescriptor /// public IReadOnlySet Without { get; } + private readonly int _hashCode; + internal QueryDescriptor(HashSet with, HashSet without) { With = with; Without = without; + _hashCode = ComputeHashCode(); } /// @@ -36,7 +39,9 @@ public class QueryDescriptor return With.SetEquals(other.With) && Without.SetEquals(other.Without); } - public override int GetHashCode() + public override int GetHashCode() => _hashCode; + + private int ComputeHashCode() { var hash = new HashCode(); foreach (var t in With.OrderBy(t => t.GUID.ToString())) diff --git a/src/OECS/World.cs b/src/OECS/World.cs index 760ef2d..37411dc 100644 --- a/src/OECS/World.cs +++ b/src/OECS/World.cs @@ -22,6 +22,7 @@ public class World : IDisposable private readonly RelationshipIndex _relationships; private readonly ChangeBuffer _changes; private bool _singletonCreated; + private bool _disposed; #if DEBUG // Tracks entities whose components were accessed via GetComponent @@ -82,6 +83,7 @@ public class World : IDisposable { _relationships.OnRemoved(relType, source, entity); _components.Remove(source, relType); + _changes.Pending.MarkComponentRemoved(source, relType); } } @@ -358,6 +360,13 @@ public class World : IDisposable /// /// Iterates all entities matching the query, providing ref access to /// one component type. + /// + /// Caution: Adding or removing components (including destroying + /// entities) during iteration may cause entities to be skipped or + /// processed twice. This is a consequence of the sparse set's + /// swap-remove strategy. If you need to make structural changes, + /// collect the affected entities first, then apply changes after + /// the iteration. /// public void ForEach( QueryDescriptor query, @@ -486,6 +495,9 @@ public class World : IDisposable public void Dispose() { + if (_disposed) + return; + _disposed = true; _changes.Dispose(); }