refactor: optimize query execution and improve change tracking
- Optimize `QueryExecutor` to use the smallest sparse set as a driver for multi-component queries, reducing iteration overhead. - Implement automatic cleanup of unused `Subject` instances in `ChangeBuffer` using a subscriber count. - Improve `ChangeSet` deduplication to correctly handle rapid add/remove transitions for the same component. - Add validation to ensure `IRelationship` components are added to their declared `Source` entity. - Update documentation to reflect correct `Entity` bit layout.
This commit is contained in:
parent
339e850e10
commit
32f4aa3e35
|
|
@ -45,8 +45,8 @@ signature) and sparse sets (one dense array per component type).
|
||||||
**Context:** Entities need to be cheap to copy, comparable, and safe against
|
**Context:** Entities need to be cheap to copy, comparable, and safe against
|
||||||
use-after-free (accessing a recycled entity ID).
|
use-after-free (accessing a recycled entity ID).
|
||||||
|
|
||||||
**Decision:** `readonly struct Entity` wrapping a `uint`. Upper 24 bits are the
|
**Decision:** `readonly struct Entity` wrapping a `uint`. Lower 24 bits are the
|
||||||
ID, lower 8 bits are the version.
|
ID, upper 8 bits are the version.
|
||||||
|
|
||||||
**Rationale:**
|
**Rationale:**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,18 @@ internal class ChangeBuffer
|
||||||
private readonly ChangeSet _pending = new();
|
private readonly ChangeSet _pending = new();
|
||||||
|
|
||||||
private readonly Subject<EntityChange> _entitySubject = new();
|
private readonly Subject<EntityChange> _entitySubject = new();
|
||||||
private readonly Dictionary<Type, Subject<EntityChange>> _componentSubjects = new();
|
private readonly Dictionary<Type, TrackedSubject> _componentSubjects = new();
|
||||||
private readonly Dictionary<QueryDescriptor, Subject<EntityChange>> _querySubjects = new();
|
private readonly Dictionary<QueryDescriptor, TrackedSubject> _querySubjects = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps a <see cref="Subject{T}"/> with a subscriber count so that
|
||||||
|
/// subjects with no remaining subscribers can be cleaned up.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TrackedSubject
|
||||||
|
{
|
||||||
|
public readonly Subject<EntityChange> Subject = new();
|
||||||
|
public int SubscriberCount;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The change set currently accumulating. Cleared after each <see cref="Post"/>.
|
/// The change set currently accumulating. Cleared after each <see cref="Post"/>.
|
||||||
|
|
@ -40,18 +50,18 @@ internal class ChangeBuffer
|
||||||
// Push to component-specific subjects.
|
// Push to component-specific subjects.
|
||||||
if (change.ComponentType != null)
|
if (change.ComponentType != null)
|
||||||
{
|
{
|
||||||
if (_componentSubjects.TryGetValue(change.ComponentType, out var compSubject))
|
if (_componentSubjects.TryGetValue(change.ComponentType, out var compTracked))
|
||||||
{
|
{
|
||||||
compSubject.OnNext(change);
|
compTracked.Subject.OnNext(change);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push to matching query subjects.
|
// Push to matching query subjects.
|
||||||
foreach (var (query, subject) in _querySubjects)
|
foreach (var (query, tracked) in _querySubjects)
|
||||||
{
|
{
|
||||||
if (ChangeMatchesQuery(change, query))
|
if (ChangeMatchesQuery(change, query))
|
||||||
{
|
{
|
||||||
subject.OnNext(change);
|
tracked.Subject.OnNext(change);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,12 +82,22 @@ internal class ChangeBuffer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Observable<EntityChange> ObserveComponentChanges(Type componentType)
|
public Observable<EntityChange> ObserveComponentChanges(Type componentType)
|
||||||
{
|
{
|
||||||
if (!_componentSubjects.TryGetValue(componentType, out var subject))
|
if (!_componentSubjects.TryGetValue(componentType, out var tracked))
|
||||||
{
|
{
|
||||||
subject = new Subject<EntityChange>();
|
tracked = new TrackedSubject();
|
||||||
_componentSubjects[componentType] = subject;
|
_componentSubjects[componentType] = tracked;
|
||||||
}
|
}
|
||||||
return subject;
|
|
||||||
|
tracked.SubscriberCount++;
|
||||||
|
return WrapWithCleanup(tracked.Subject, () =>
|
||||||
|
{
|
||||||
|
tracked.SubscriberCount--;
|
||||||
|
if (tracked.SubscriberCount <= 0)
|
||||||
|
{
|
||||||
|
tracked.Subject.Dispose();
|
||||||
|
_componentSubjects.Remove(componentType);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -85,12 +105,40 @@ internal class ChangeBuffer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Observable<EntityChange> ObserveQuery(QueryDescriptor query)
|
public Observable<EntityChange> ObserveQuery(QueryDescriptor query)
|
||||||
{
|
{
|
||||||
if (!_querySubjects.TryGetValue(query, out var subject))
|
if (!_querySubjects.TryGetValue(query, out var tracked))
|
||||||
{
|
{
|
||||||
subject = new Subject<EntityChange>();
|
tracked = new TrackedSubject();
|
||||||
_querySubjects[query] = subject;
|
_querySubjects[query] = tracked;
|
||||||
}
|
}
|
||||||
return subject;
|
|
||||||
|
tracked.SubscriberCount++;
|
||||||
|
return WrapWithCleanup(tracked.Subject, () =>
|
||||||
|
{
|
||||||
|
tracked.SubscriberCount--;
|
||||||
|
if (tracked.SubscriberCount <= 0)
|
||||||
|
{
|
||||||
|
tracked.Subject.Dispose();
|
||||||
|
_querySubjects.Remove(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps an observable so that <paramref name="onLastDispose"/> is called
|
||||||
|
/// when the last subscriber disposes.
|
||||||
|
/// </summary>
|
||||||
|
private static Observable<EntityChange> WrapWithCleanup(
|
||||||
|
Observable<EntityChange> source, Action onLastDispose)
|
||||||
|
{
|
||||||
|
return Observable.Create<EntityChange>(observer =>
|
||||||
|
{
|
||||||
|
var subscription = source.Subscribe(observer);
|
||||||
|
return Disposable.Create(() =>
|
||||||
|
{
|
||||||
|
subscription.Dispose();
|
||||||
|
onLastDispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -99,14 +147,14 @@ internal class ChangeBuffer
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_entitySubject.Dispose();
|
_entitySubject.Dispose();
|
||||||
foreach (var subject in _componentSubjects.Values)
|
foreach (var tracked in _componentSubjects.Values)
|
||||||
{
|
{
|
||||||
subject.Dispose();
|
tracked.Subject.Dispose();
|
||||||
}
|
}
|
||||||
_componentSubjects.Clear();
|
_componentSubjects.Clear();
|
||||||
foreach (var subject in _querySubjects.Values)
|
foreach (var tracked in _querySubjects.Values)
|
||||||
{
|
{
|
||||||
subject.Dispose();
|
tracked.Subject.Dispose();
|
||||||
}
|
}
|
||||||
_querySubjects.Clear();
|
_querySubjects.Clear();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ namespace OECS;
|
||||||
/// Accumulates <see cref="EntityChange"/> entries during a system run.
|
/// Accumulates <see cref="EntityChange"/> entries during a system run.
|
||||||
///
|
///
|
||||||
/// Deduplication: if the same (entity, kind, componentType) change is marked
|
/// Deduplication: if the same (entity, kind, componentType) change is marked
|
||||||
/// multiple times, only one entry is kept. Entity-level changes (Added/Removed)
|
/// multiple times, only one entry is kept. However, if a structurally opposed
|
||||||
/// take precedence over component-level changes for the same entity.
|
/// change arrives (Added vs Removed) for the same (entity, componentType),
|
||||||
|
/// the old entry is removed so observers see the full state transition.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class ChangeSet
|
internal class ChangeSet
|
||||||
{
|
{
|
||||||
private readonly List<EntityChange> _changes = new();
|
private readonly List<EntityChange> _changes = new();
|
||||||
private readonly HashSet<(Entity Entity, ChangeKind Kind, Type? ComponentType)> _dedup = new();
|
private readonly Dictionary<(Entity Entity, ChangeKind Kind, Type? ComponentType), int> _dedup = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All accumulated changes in insertion order.
|
/// All accumulated changes in insertion order.
|
||||||
|
|
@ -75,9 +76,31 @@ internal class ChangeSet
|
||||||
private void TryAdd(EntityChange change)
|
private void TryAdd(EntityChange change)
|
||||||
{
|
{
|
||||||
var key = (change.Entity, change.Kind, change.ComponentType);
|
var key = (change.Entity, change.Kind, change.ComponentType);
|
||||||
if (_dedup.Add(key))
|
if (_dedup.ContainsKey(key))
|
||||||
{
|
{
|
||||||
|
// Same (entity, kind, componentType) already recorded — deduplicate.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a component is re-added after being removed within the same
|
||||||
|
// batch, remove the old ComponentRemoved entry so observers see the
|
||||||
|
// full Add → Remove → Add sequence.
|
||||||
|
if (change.Kind == ChangeKind.ComponentAdded && change.ComponentType != null)
|
||||||
|
{
|
||||||
|
var removedKey = (change.Entity, ChangeKind.ComponentRemoved, change.ComponentType);
|
||||||
|
if (_dedup.Remove(removedKey, out int oldIndex))
|
||||||
|
{
|
||||||
|
_changes.RemoveAt(oldIndex);
|
||||||
|
// Adjust indices for all entries that shifted down.
|
||||||
|
foreach (var k in _dedup.Keys.ToList())
|
||||||
|
{
|
||||||
|
if (_dedup[k] > oldIndex)
|
||||||
|
_dedup[k]--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_dedup[key] = _changes.Count;
|
||||||
_changes.Add(change);
|
_changes.Add(change);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -129,21 +129,97 @@ internal static class QueryExecutor
|
||||||
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
|
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
|
||||||
if (set3 == null) return;
|
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 dense = set1.Dense;
|
||||||
var denseEntities = set1.DenseEntities;
|
var denseEntities = set1.DenseEntities;
|
||||||
var count = set1.Count;
|
var count = set1.Count;
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var entity = denseEntities[i];
|
var entity = denseEntities[i];
|
||||||
if (entity.Id == SingletonId) continue;
|
if (entity.Id == SingletonId) continue;
|
||||||
if (!set2.Contains(entity)) continue;
|
if (!set2.Contains(entity)) continue;
|
||||||
if (!set3.Contains(entity)) continue;
|
if (!set3.Contains(entity)) continue;
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||||
continue;
|
|
||||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity));
|
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void IterateThree<TDriver, TA, TB>(
|
||||||
|
SparseSet<TDriver> driveSet,
|
||||||
|
SparseSet<TA> setA,
|
||||||
|
SparseSet<TB> setB,
|
||||||
|
ComponentStore store,
|
||||||
|
IReadOnlySet<Type> withoutTypes,
|
||||||
|
ForEachAction<TA, TDriver, TB> actionWhenDriverIsT2,
|
||||||
|
ForEachAction<TDriver, TA, TB> actionWhenDriverIsT1,
|
||||||
|
ForEachAction<TA, TB, TDriver> actionWhenDriverIsT3)
|
||||||
|
where TDriver : struct where TA : struct where TB : 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 (!setA.Contains(entity)) continue;
|
||||||
|
if (!setB.Contains(entity)) continue;
|
||||||
|
if (!PassesWithoutFilter(store, entity, withoutTypes))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (actionWhenDriverIsT2 != null!)
|
||||||
|
{
|
||||||
|
// Driver is T2, so order is: T1=setA, T2=driveSet, T3=setB
|
||||||
|
actionWhenDriverIsT2(entity, ref setA.Get(entity), ref dense[i], ref setB.Get(entity));
|
||||||
|
}
|
||||||
|
else if (actionWhenDriverIsT3 != null!)
|
||||||
|
{
|
||||||
|
// Driver is T3, so order is: T1=setA, T2=setB, T3=driveSet
|
||||||
|
actionWhenDriverIsT3(entity, ref setA.Get(entity), ref setB.Get(entity), ref dense[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Driver is T1, so order is: T1=driveSet, T2=setA, T3=setB
|
||||||
|
actionWhenDriverIsT1(entity, ref dense[i], ref setA.Get(entity), ref setB.Get(entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void ForEach<T1, T2, T3, T4>(
|
public static void ForEach<T1, T2, T3, T4>(
|
||||||
ComponentStore store,
|
ComponentStore store,
|
||||||
|
|
@ -160,10 +236,63 @@ internal static class QueryExecutor
|
||||||
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
|
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
|
||||||
if (set4 == null) return;
|
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 dense = set1.Dense;
|
||||||
var denseEntities = set1.DenseEntities;
|
var denseEntities = set1.DenseEntities;
|
||||||
var count = set1.Count;
|
var count = set1.Count;
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var entity = denseEntities[i];
|
var entity = denseEntities[i];
|
||||||
|
|
@ -171,11 +300,11 @@ internal static class QueryExecutor
|
||||||
if (!set2.Contains(entity)) continue;
|
if (!set2.Contains(entity)) continue;
|
||||||
if (!set3.Contains(entity)) continue;
|
if (!set3.Contains(entity)) continue;
|
||||||
if (!set4.Contains(entity)) continue;
|
if (!set4.Contains(entity)) continue;
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||||
continue;
|
|
||||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity));
|
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void ForEach<T1, T2, T3, T4, T5>(
|
public static void ForEach<T1, T2, T3, T4, T5>(
|
||||||
ComponentStore store,
|
ComponentStore store,
|
||||||
|
|
@ -194,10 +323,83 @@ internal static class QueryExecutor
|
||||||
var set5 = store.GetSet(typeof(T5)) as SparseSet<T5>;
|
var set5 = store.GetSet(typeof(T5)) as SparseSet<T5>;
|
||||||
if (set5 == null) return;
|
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 dense = set1.Dense;
|
||||||
var denseEntities = set1.DenseEntities;
|
var denseEntities = set1.DenseEntities;
|
||||||
var count = set1.Count;
|
var count = set1.Count;
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var entity = denseEntities[i];
|
var entity = denseEntities[i];
|
||||||
|
|
@ -206,11 +408,11 @@ internal static class QueryExecutor
|
||||||
if (!set3.Contains(entity)) continue;
|
if (!set3.Contains(entity)) continue;
|
||||||
if (!set4.Contains(entity)) continue;
|
if (!set4.Contains(entity)) continue;
|
||||||
if (!set5.Contains(entity)) continue;
|
if (!set5.Contains(entity)) continue;
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||||
continue;
|
|
||||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity));
|
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void ForEach<T1, T2, T3, T4, T5, T6>(
|
public static void ForEach<T1, T2, T3, T4, T5, T6>(
|
||||||
ComponentStore store,
|
ComponentStore store,
|
||||||
|
|
@ -231,10 +433,105 @@ internal static class QueryExecutor
|
||||||
var set6 = store.GetSet(typeof(T6)) as SparseSet<T6>;
|
var set6 = store.GetSet(typeof(T6)) as SparseSet<T6>;
|
||||||
if (set6 == null) return;
|
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 dense = set1.Dense;
|
||||||
var denseEntities = set1.DenseEntities;
|
var denseEntities = set1.DenseEntities;
|
||||||
var count = set1.Count;
|
var count = set1.Count;
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var entity = denseEntities[i];
|
var entity = denseEntities[i];
|
||||||
|
|
@ -244,9 +541,9 @@ internal static class QueryExecutor
|
||||||
if (!set4.Contains(entity)) continue;
|
if (!set4.Contains(entity)) continue;
|
||||||
if (!set5.Contains(entity)) continue;
|
if (!set5.Contains(entity)) continue;
|
||||||
if (!set6.Contains(entity)) continue;
|
if (!set6.Contains(entity)) continue;
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||||
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));
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
@ -188,6 +188,17 @@ public class World : IDisposable
|
||||||
// If replacing an existing relationship, remove the old index entry first.
|
// If replacing an existing relationship, remove the old index entry first.
|
||||||
if (component is IRelationship newRel)
|
if (component is IRelationship newRel)
|
||||||
{
|
{
|
||||||
|
// Guard against mismatched Source: the relationship must be stored
|
||||||
|
// on the entity it claims as its Source, otherwise the reverse
|
||||||
|
// index becomes corrupted.
|
||||||
|
if (newRel.Source != entity)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Relationship of type {typeof(T).Name} has Source={newRel.Source} " +
|
||||||
|
$"but is being added to entity {entity}. " +
|
||||||
|
$"The Source must match the entity the component is added to.");
|
||||||
|
}
|
||||||
|
|
||||||
if (_components.TryGet<T>(entity, out var old))
|
if (_components.TryGet<T>(entity, out var old))
|
||||||
{
|
{
|
||||||
var oldRel = (IRelationship)(object)old;
|
var oldRel = (IRelationship)(object)old;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue