feat: use OrderedEntitySet in RelationshipIndex

Replace HashSet with OrderedEntitySet in RelationshipIndex to
preserve the insertion order of source entities during iteration.
This commit is contained in:
hypercross 2026-07-21 22:39:35 +08:00
parent 535c8d948e
commit b0bf10f286
2 changed files with 57 additions and 5 deletions

49
OECS/OrderedEntitySet.cs Normal file
View File

@ -0,0 +1,49 @@
using System.Collections;
namespace OECS;
/// <summary>
/// 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.
/// </summary>
internal sealed class OrderedEntitySet : IReadOnlyCollection<Entity>
{
// 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<Entity> _order = new();
private readonly Dictionary<Entity, int> _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<Entity> GetEnumerator() => _order.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View File

@ -8,13 +8,16 @@ namespace OECS;
///
/// Updated automatically by <see cref="World"/> 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.
/// </summary>
internal class RelationshipIndex
{
/// <summary>
/// Per relationship type: target entity → set of source entities.
/// </summary>
private readonly Dictionary<Type, Dictionary<Entity, HashSet<Entity>>> _index = new();
private readonly Dictionary<Type, Dictionary<Entity, OrderedEntitySet>> _index = new();
/// <summary>
/// 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<Entity, HashSet<Entity>>();
targetMap = new Dictionary<Entity, OrderedEntitySet>();
_index[relationshipType] = targetMap;
}
if (!targetMap.TryGetValue(target, out var sources))
{
sources = new HashSet<Entity>();
sources = new OrderedEntitySet();
targetMap[target] = sources;
}
@ -55,7 +58,7 @@ internal class RelationshipIndex
/// <summary>
/// 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.
/// </summary>
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
where T : struct, IRelationship
@ -123,4 +126,4 @@ internal class RelationshipIndex
{
return _index.ContainsKey(relationshipType);
}
}
}