From b0bf10f286063f56d5a18922aab56bc7aa52b20e Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 21 Jul 2026 22:39:35 +0800 Subject: [PATCH] feat: use OrderedEntitySet in RelationshipIndex Replace HashSet with OrderedEntitySet in RelationshipIndex to preserve the insertion order of source entities during iteration. --- OECS/OrderedEntitySet.cs | 49 +++++++++++++++++++++++++++++++++++++++ OECS/RelationshipIndex.cs | 13 +++++++---- 2 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 OECS/OrderedEntitySet.cs diff --git a/OECS/OrderedEntitySet.cs b/OECS/OrderedEntitySet.cs new file mode 100644 index 0000000..9e0d5a8 --- /dev/null +++ b/OECS/OrderedEntitySet.cs @@ -0,0 +1,49 @@ +using System.Collections; + +namespace OECS; + +/// +/// An insertion-ordered set of entities that supports O(1) Add, Remove, and Contains +/// while iterating in insertion order. Drop-in for cases where both +/// stable ordering and fast membership checks are needed. +/// +/// Not thread-safe. +/// +internal sealed class OrderedEntitySet : IReadOnlyCollection +{ + // Entities are stored in a packed list; a secondary dictionary maps + // each entity to its index for O(1) removal via swap-with-last. + private readonly List _order = new(); + private readonly Dictionary _index = new(); + + public int Count => _order.Count; + + public bool Add(Entity entity) + { + if (_index.ContainsKey(entity)) + return false; + + _index[entity] = _order.Count; + _order.Add(entity); + return true; + } + + public bool Remove(Entity entity) + { + if (!_index.Remove(entity, out var idx)) + return false; + + int lastIdx = _order.Count - 1; + if (idx < lastIdx) + { + var last = _order[lastIdx]; + _order[idx] = last; + _index[last] = idx; + } + _order.RemoveAt(lastIdx); + return true; + } + + public IEnumerator GetEnumerator() => _order.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/OECS/RelationshipIndex.cs b/OECS/RelationshipIndex.cs index 1436da4..f5d1572 100644 --- a/OECS/RelationshipIndex.cs +++ b/OECS/RelationshipIndex.cs @@ -8,13 +8,16 @@ namespace OECS; /// /// Updated automatically by when relationship /// components are added, removed, or when entities are destroyed. +/// +/// Source sets are insertion-ordered — the order in which relationships +/// are added is preserved during iteration. /// internal class RelationshipIndex { /// /// Per relationship type: target entity → set of source entities. /// - private readonly Dictionary>> _index = new(); + private readonly Dictionary> _index = new(); /// /// Registers that a source entity now has a relationship pointing to a target. @@ -23,13 +26,13 @@ internal class RelationshipIndex { if (!_index.TryGetValue(relationshipType, out var targetMap)) { - targetMap = new Dictionary>(); + targetMap = new Dictionary(); _index[relationshipType] = targetMap; } if (!targetMap.TryGetValue(target, out var sources)) { - sources = new HashSet(); + sources = new OrderedEntitySet(); targetMap[target] = sources; } @@ -55,7 +58,7 @@ internal class RelationshipIndex /// /// Returns all source entities that have a relationship of the given type - /// pointing to the specified target entity. + /// pointing to the specified target entity, in insertion order. /// public IReadOnlyCollection GetSources(Entity target) where T : struct, IRelationship @@ -123,4 +126,4 @@ internal class RelationshipIndex { return _index.ContainsKey(relationshipType); } -} +} \ No newline at end of file