diff --git a/src/Arch.Tests/BitSetTest.cs b/src/Arch.Tests/BitSetTest.cs
index 548025f8..8af11e14 100644
--- a/src/Arch.Tests/BitSetTest.cs
+++ b/src/Arch.Tests/BitSetTest.cs
@@ -51,7 +51,7 @@ public void HashSimilarity()
[Test]
public void BitsetSetAll()
{
- var bitSet = new BitSet();
+ var bitSet = new BitSet(5);
bitSet.SetAll();
var count = 0;
diff --git a/src/Arch.Tests/ChangedQueryTest.cs b/src/Arch.Tests/ChangedQueryTest.cs
new file mode 100644
index 00000000..3af50818
--- /dev/null
+++ b/src/Arch.Tests/ChangedQueryTest.cs
@@ -0,0 +1,107 @@
+using Arch.Core;
+
+namespace Arch.Tests;
+
+[TestFixture]
+public class ChangedQueryTest
+{
+ private struct A;
+ private struct B;
+ private struct C;
+
+ private World _world;
+ private Entity _e0, _e1, _e2, _e3, _e4, _e5;
+ private Entity[] _nothing = [];
+
+ [OneTimeSetUp]
+ public void Setup()
+ {
+ _world = World.Create();
+
+ _e0 = _world.Create();
+
+ _e1 = _world.Create();
+ _world.MarkChanged(_e1);
+
+ _e2 = _world.Create();
+ _world.MarkChanged(_e2);
+
+ _e3 = _world.Create();
+ _world.MarkChanged(_e3);
+
+ _e4 = _world.Create();
+ _world.MarkChanged(_e4);
+
+ _e5 = _world.Create();
+ }
+
+ [Test]
+ public void WithChangedOnly()
+ {
+ var query = new QueryDescription().WithChanged();
+ AssertMatches(query, _e1, _e2, _e3);
+ }
+
+ [Test]
+ public void WithAnyAndChanged()
+ {
+ var query = new QueryDescription().WithAny().WithChanged();
+ AssertMatches(query, _e4);
+ }
+
+ [Test]
+ public void WithDisjointAnyAndChanged()
+ {
+ var query = new QueryDescription().WithAny().WithChanged();
+ AssertMatches(query, _e1, _e2, _e3);
+ }
+
+ [Test]
+ public void WithNoneAndChanged()
+ {
+ var query = new QueryDescription().WithNone().WithChanged();
+ AssertMatches(query, _nothing);
+ }
+
+ [Test]
+ public void WithDisjointNoneAndChanged()
+ {
+ var query = new QueryDescription().WithNone().WithChanged();
+ AssertMatches(query, _e4);
+ }
+
+ [Test]
+ public void WithExclusiveAndChanged()
+ {
+ var query = new QueryDescription().WithExclusive().WithChanged();
+ AssertMatches(query, _e1);
+ }
+
+ [Test]
+ public void WithDisjointExclusiveAndChanged()
+ {
+ var query = new QueryDescription().WithExclusive().WithChanged();
+ AssertMatches(query, _nothing);
+ }
+
+ [Test]
+ public void WithDisjointAnyNoneAndChanged()
+ {
+ var query = new QueryDescription().WithAny().WithChanged().WithNone();
+ AssertMatches(query, _e1, _e3);
+ }
+
+ private void AssertMatches(QueryDescription query, params Entity[] expected)
+ {
+ var entities = new Entity[10];
+ var total = _world.GetEntities(query, entities);
+ entities = entities[..total];
+ CollectionAssert.AreEquivalent(expected, entities);
+ }
+
+ [OneTimeTearDown]
+ public void TearDown()
+ {
+ _world.Dispose();
+ }
+}
diff --git a/src/Arch/Arch.csproj b/src/Arch/Arch.csproj
index 35b5ce92..a61a66b4 100644
--- a/src/Arch/Arch.csproj
+++ b/src/Arch/Arch.csproj
@@ -41,29 +41,29 @@
- TRACE;
+ TRACE;CHANGED_FLAGS;
false
AnyCPU
- TRACE;PURE_ECS;
+ TRACE;PURE_ECS;CHANGED_FLAGS;
- TRACE;EVENTS;
+ TRACE;EVENTS;CHANGED_FLAGS;
- TRACE
+ TRACE;CHANGED_FLAGS;
- TRACE;EVENTS;
+ TRACE;EVENTS;CHANGED_FLAGS;
- TRACE;PURE_ECS
+ TRACE;PURE_ECS;CHANGED_FLAGS;
@@ -590,6 +590,11 @@
True
World.CreateBulk.tt
+
+ True
+ True
+ QueryDescription.WithChanged.tt
+
diff --git a/src/Arch/Core/Archetype.cs b/src/Arch/Core/Archetype.cs
index 931df944..e9087ca3 100644
--- a/src/Arch/Core/Archetype.cs
+++ b/src/Arch/Core/Archetype.cs
@@ -1,14 +1,9 @@
-using System.Buffers;
using System.Diagnostics.Contracts;
-using Arch.Core.Extensions;
using Arch.Core.Extensions.Internal;
using Arch.Core.Utils;
-using Arch.LowLevel;
using Arch.LowLevel.Jagged;
using Collections.Pooled;
using CommunityToolkit.HighPerformance;
-using Array = System.Array;
-using System.Runtime.InteropServices;
namespace Arch.Core;
@@ -339,10 +334,7 @@ internal int[] LookupArray
/// The number of 's within the array.
///
public int ChunkCount {
- get
- {
- return Chunks.Count;
- }
+ get => Chunks.Count;
}
///
@@ -350,10 +342,7 @@ public int ChunkCount {
/// The total capacity.
///
public int ChunkCapacity {
- get
- {
- return Chunks.Capacity;
- }
+ get => Chunks.Capacity;
}
///
@@ -436,7 +425,7 @@ internal int Add(Entity entity, out Chunk chunk, out Slot slot) // TODO: Store
ref var currentChunk = ref GetChunk(count);
// Fill chunk
- if (currentChunk.IsEmpty)
+ if (!currentChunk.IsFull)
{
slot = new Slot(currentChunk.Add(entity), count);
chunk = currentChunk;
@@ -958,3 +947,67 @@ internal static void CopyComponents(Archetype source, ref Slot fromSlot, Archety
Chunk.CopyComponents(ref oldChunk, fromSlot.Index, ref sourceSignature, ref newChunk, toSlot.Index, 1);
}
}
+
+
+#if CHANGED_FLAGS
+
+public sealed partial class Archetype
+{
+ ///
+ /// Checks whether the component of an at a given has been flagged changed.
+ ///
+ /// The at which the component of an is to be checked.
+ /// The component type.
+ /// True if the component is changed, false otherwise.
+ public bool IsChanged(ref Slot slot, ComponentType componentType)
+ {
+ ref var chunk = ref GetChunk(slot.ChunkIndex);
+ return chunk.IsChanged(slot.Index, componentType);
+ }
+
+ ///
+ /// Flags the component of an at a given as changed.
+ ///
+ /// The at which the component of an is to be marked changed.
+ /// The component type.
+ internal void MarkChanged(ref Slot slot, ComponentType componentType)
+ {
+ ref var chunk = ref GetChunk(slot.ChunkIndex);
+ chunk.MarkChanged(slot.Index, componentType);
+ }
+
+ ///
+ /// Clears the changed flag of the component of an at a given .
+ ///
+ /// The at which the component of an is to be cleared.
+ /// The component type.
+ internal void ClearChanged(ref Slot slot, ComponentType componentType)
+ {
+ ref var chunk = ref GetChunk(slot.ChunkIndex);
+ chunk.ClearChanged(slot.Index, componentType);
+ }
+
+ ///
+ /// Clears the changed flag for all components of an at a given .
+ ///
+ /// The slot.
+ internal void ClearChanged(ref Slot slot)
+ {
+ ref var chunk = ref GetChunk(slot.ChunkIndex);
+ chunk.ClearChanged(slot.Index);
+ }
+
+ ///
+ /// Clears all the changed flags in this Archetype.
+ ///
+ internal void ClearAllChanged()
+ {
+ for (var i = 0; i < Chunks.Count; i++)
+ {
+ ref var chunk = ref Chunks[i];
+ chunk.ClearAllChanged();
+ }
+ }
+}
+
+#endif
diff --git a/src/Arch/Core/Chunk.cs b/src/Arch/Core/Chunk.cs
index 7ce1a838..15b430ed 100644
--- a/src/Arch/Core/Chunk.cs
+++ b/src/Arch/Core/Chunk.cs
@@ -1,12 +1,9 @@
using System.Buffers;
using System.Diagnostics.Contracts;
-using System.Drawing;
using Arch.Core.Events;
-using Arch.Core.Extensions;
using Arch.Core.Extensions.Internal;
using Arch.Core.Utils;
using Arch.LowLevel;
-using Collections.Pooled;
using CommunityToolkit.HighPerformance;
using Array = System.Array;
@@ -175,6 +172,15 @@ internal Chunk(int capacity, int[] componentIdToArrayIndex, Span
var type = types[index];
Components[index] = ArrayRegistry.GetArray(type, Capacity);
}
+
+#if CHANGED_FLAGS
+ _changedFlags = new BitSet[Capacity + 1]; // Last index contains the "any" bitset
+ var typeCapacity = ComponentTypeExtensions.GetMaxValue(types);
+ for (var i = 0; i <= Capacity; i++)
+ {
+ _changedFlags[i] = new BitSet(typeCapacity);
+ }
+#endif
}
@@ -216,11 +222,6 @@ internal Chunk(int capacity, int[] componentIdToArrayIndex, Span
///
public readonly bool IsFull { [Pure] get => Count >= Capacity; }
- ///
- /// Checks whether this instance is full or not.
- ///
- public readonly bool IsEmpty { [Pure] get => Count < Capacity; }
-
///
/// Inserts an entity into the .
/// This won't fire an event for .
@@ -666,3 +667,102 @@ internal int Transfer(int index, ref Chunk chunk)
return lastEntity.Id;
}
}
+
+#if CHANGED_FLAGS
+
+public partial struct Chunk
+{
+ private readonly BitSet[] _changedFlags;
+
+ ///
+ /// Checks whether any component of the given type has been flagged changed.
+ ///
+ /// The component type.
+ /// True if the component is changed, false otherwise.
+ [Pure]
+ public bool IsAnyChanged(ComponentType type)
+ {
+ return _changedFlags.DangerousGetReferenceAt(Capacity).IsSet(type.Id);
+ }
+
+ ///
+ /// Checks whether any component of the given types has been flagged changed.
+ ///
+ /// A representing the component types.
+ /// True if any of the components have changed, false otherwise.
+ [Pure]
+ public bool IsAnyChanged(BitSet types)
+ {
+ var changed = _changedFlags.DangerousGetReferenceAt(Capacity);
+ return types.Any(changed);
+ }
+
+ ///
+ /// Checks whether the component at the given index has been flagged changed.
+ ///
+ /// The index.
+ /// The component type.
+ /// True if the component is changed, false otherwise.
+ [Pure]
+ public bool IsChanged(int index, ComponentType type)
+ {
+ return _changedFlags.DangerousGetReferenceAt(index).IsSet(type.Id);
+ }
+
+ ///
+ /// Checks whether any of the components at the given index has been flagged changed.
+ ///
+ /// The index.
+ /// A representing the component types.
+ /// True if the component is changed, false otherwise.
+ [Pure]
+ public bool IsChanged(int index, BitSet types)
+ {
+ var changed = _changedFlags.DangerousGetReferenceAt(index);
+ return types.Any(changed);
+ }
+
+ ///
+ /// Flags the component at the given index as changed.
+ ///
+ /// The index.
+ /// The component type.
+ public void MarkChanged(int index, ComponentType type)
+ {
+ _changedFlags.DangerousGetReferenceAt(index).SetBit(type.Id);
+ _changedFlags.DangerousGetReferenceAt(Capacity).SetBit(type.Id);
+ }
+
+ ///
+ /// Clears all the changed flags in this Chunk.
+ ///
+ public void ClearAllChanged()
+ {
+ for (var i = 0; i < _changedFlags.Length; i++)
+ {
+ var flags = _changedFlags.DangerousGetReferenceAt(i);
+ flags.ClearAll();
+ }
+ }
+
+ ///
+ /// Clears the changed flag for the specified component type at the specified index.
+ ///
+ /// The type.
+ /// The index.
+ public void ClearChanged(int index, ComponentType type)
+ {
+ _changedFlags.DangerousGetReferenceAt(index).ClearBit(type.Id);
+ }
+
+ ///
+ /// Clears the changed flag for all components at the specified index.
+ ///
+ /// The index.
+ public void ClearChanged(int index)
+ {
+ _changedFlags.DangerousGetReferenceAt(index).ClearAll();
+ }
+}
+
+#endif
diff --git a/src/Arch/Core/Enumerators.cs b/src/Arch/Core/Enumerators.cs
index 6e45292f..f63f87a0 100644
--- a/src/Arch/Core/Enumerators.cs
+++ b/src/Arch/Core/Enumerators.cs
@@ -171,7 +171,7 @@ public QueryArchetypeEnumerator GetEnumerator()
/// represents an enumerator with which one can iterate over all non empty 's that matches the given .
///
[SkipLocalsInit]
-public ref struct QueryChunkEnumerator
+public ref partial struct QueryChunkEnumerator
{
private QueryArchetypeEnumerator _archetypeEnumerator;
private int _index;
@@ -538,4 +538,3 @@ public RangeEnumerator GetEnumerator()
return new RangeEnumerator(_threads, _size);
}
}
-
diff --git a/src/Arch/Core/Extensions/EntityExtensions.cs b/src/Arch/Core/Extensions/EntityExtensions.cs
index cc890c4e..e258fe8e 100644
--- a/src/Arch/Core/Extensions/EntityExtensions.cs
+++ b/src/Arch/Core/Extensions/EntityExtensions.cs
@@ -359,3 +359,60 @@ public static void RemoveRange(this in Entity entity, Span types)
#endif
}
+
+public static partial class EntityExtensions
+{
+
+#if CHANGED_FLAGS && !PURE_ECS
+
+ ///
+ public static bool IsChanged(this Entity entity)
+ {
+ var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId);
+ return world.IsChanged(entity);
+ }
+
+ ///
+ public static bool IsChanged(this Entity entity, ComponentType type)
+ {
+ var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId);
+ return world.IsChanged(entity, type);
+ }
+
+ ///
+ public static void Markchanged(this Entity entity)
+ {
+ var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId);
+ world.Markchanged(entity);
+ }
+
+ ///
+ public static void Markchanged(this Entity entity, ComponentType componentType)
+ {
+ var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId);
+ world.Markchanged(entity, componentType);
+ }
+
+ ///
+ public static void ClearChanged(this Entity entity)
+ {
+ var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId);
+ world.ClearChanged(entity);
+ }
+
+ ///
+ public static void ClearChanged(this Entity entity, ComponentType componentType)
+ {
+ var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId);
+ world.ClearChanged(entity, componentType);
+ }
+
+ ///
+ public static void ClearChanged(this Entity entity)
+ {
+ var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId);
+ world.ClearChanged(entity);
+ }
+
+#endif
+}
diff --git a/src/Arch/Core/Extensions/Internal/ComponentTypeExtensions.cs b/src/Arch/Core/Extensions/Internal/ComponentTypeExtensions.cs
index 6f3c3215..c0a6b1fd 100644
--- a/src/Arch/Core/Extensions/Internal/ComponentTypeExtensions.cs
+++ b/src/Arch/Core/Extensions/Internal/ComponentTypeExtensions.cs
@@ -38,15 +38,7 @@ internal static int ToByteSize(this Span types)
internal static int[] ToLookupArray(this Span types)
{
// Get maximum component ID.
- var max = 0;
- foreach (var type in types)
- {
- var componentId = type.Id;
- if (componentId >= max)
- {
- max = componentId;
- }
- }
+ var max = GetMaxValue(types);
// Create lookup table where the component ID points to the component index.
var array = new int[max + 1];
@@ -61,4 +53,20 @@ internal static int[] ToLookupArray(this Span types)
return array;
}
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static int GetMaxValue(Span types)
+ {
+ var max = 0;
+ foreach (var type in types)
+ {
+ var componentId = type.Id;
+ if (componentId >= max)
+ {
+ max = componentId;
+ }
+ }
+
+ return max;
+ }
}
diff --git a/src/Arch/Core/Query.cs b/src/Arch/Core/Query.cs
index feed4a1e..9ce2ef52 100644
--- a/src/Arch/Core/Query.cs
+++ b/src/Arch/Core/Query.cs
@@ -1,7 +1,5 @@
-using Arch.Core.Extensions;
using Arch.Core.Extensions.Internal;
using Arch.Core.Utils;
-using Collections.Pooled;
using CommunityToolkit.HighPerformance;
namespace Arch.Core;
@@ -356,6 +354,8 @@ public QueryDescription()
_hashCode = -1;
}
+#if !CHANGED_FLAGS
+
///
/// Initializes a new instance of the struct.
///
@@ -374,6 +374,8 @@ public QueryDescription(Signature? all = null, Signature? any = null, Signature?
_hashCode = GetHashCode();
}
+#endif
+
///
/// Builds this instance by calculating a new .
/// Is actually only needed if the passed arrays are changed afterwards.
@@ -487,6 +489,9 @@ public override int GetHashCode()
hash = (hash * 23) + Any.GetHashCode();
hash = (hash * 23) + None.GetHashCode();
hash = (hash * 23) + Exclusive.GetHashCode();
+#if CHANGED_FLAGS
+ hash = (hash * 23) + Changed.GetHashCode();
+#endif
_hashCode = hash;
return hash;
}
@@ -535,6 +540,11 @@ public partial class Query : IEquatable
private readonly BitSet _none;
private readonly BitSet _exclusive;
+#if CHANGED_FLAGS
+ public readonly bool HasChangedFilter;
+ public readonly BitSet Changed;
+#endif
+
private readonly bool _isExclusive;
///
@@ -558,10 +568,26 @@ internal Query(Archetypes allArchetypes, QueryDescription description)
// Convert to `BitSet`s.
_all = description.All;
- _any = description.Any;
_none = description.None;
_exclusive = description.Exclusive;
+#if CHANGED_FLAGS
+ if (description.Changed.Count > 0)
+ {
+ Changed = description.Changed;
+ _any = Signature.Add(description.Any, description.Changed);
+ HasChangedFilter = true;
+ }
+ else
+ {
+ Changed = new BitSet();
+ _any = description.Any;
+ HasChangedFilter = false;
+ }
+#else
+ _any = description.Any;
+#endif
+
// Handle exclusive.
if (description.Exclusive.Count != 0)
{
@@ -578,7 +604,9 @@ internal Query(Archetypes allArchetypes, QueryDescription description)
/// True if it matches, otherwise false.
public bool Matches(BitSet bitset)
{
- return _isExclusive ? _exclusive.Exclusive(bitset) : _all.All(bitset) && _any.Any(bitset) && _none.None(bitset);
+ return _isExclusive
+ ? _exclusive.Exclusive(bitset)
+ : _all.All(bitset) && _any.Any(bitset) && _none.None(bitset);
}
///
@@ -599,8 +627,7 @@ private void Match()
_matchingArchetypes.Clear();
foreach (var archetype in allArchetypes)
{
- var matches = Matches(archetype.BitSet);
- if (matches)
+ if (Matches(archetype.BitSet))
{
_matchingArchetypes.Add(archetype);
}
@@ -639,6 +666,18 @@ public QueryChunkEnumerator GetEnumerator()
return new QueryChunkEnumerator(_matchingArchetypes.AsSpan());
}
+ public QueryEntityEnumerator GetEntityEnumerator()
+ {
+ Match();
+ return new QueryEntityEnumerator(this);
+ }
+
+ public QueryComponentEnumerator GetComponentEnumerator()
+ {
+ Match();
+ return new QueryComponentEnumerator(this);
+ }
+
///
/// Checks this for equality with another.
///
@@ -646,7 +685,14 @@ public QueryChunkEnumerator GetEnumerator()
/// True if they are equal, false if not.
public bool Equals(Query other)
{
- return Equals(_any, other._any) && Equals(_all, other._all) && Equals(_none, other._none) && Equals(_exclusive, other._exclusive) && _queryDescription.Equals(other._queryDescription);
+ return Equals(_any, other._any) &&
+ Equals(_all, other._all) &&
+ Equals(_none, other._none) &&
+#if CHANGED_FLAGS
+ Equals(Changed, other.Changed) &&
+#endif
+ Equals(_exclusive, other._exclusive) &&
+ _queryDescription.Equals(other._queryDescription);
}
///
@@ -667,10 +713,13 @@ public override int GetHashCode()
{
unchecked
{
- var hashCode = _any is not null ? _any.GetHashCode() : 0;
- hashCode = (hashCode * 397) ^ (_all is not null ? _all.GetHashCode() : 0);
- hashCode = (hashCode * 397) ^ (_none is not null ? _none.GetHashCode() : 0);
+ var hashCode = _any?.GetHashCode() ?? 0;
+ hashCode = (hashCode * 397) ^ (_all?.GetHashCode() ?? 0);
+ hashCode = (hashCode * 397) ^ (_none?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ (_exclusive?.GetHashCode() ?? 0);
+#if CHANGED_FLAGS
+ hashCode = (hashCode * 397) ^ (Changed?.GetHashCode() ?? 0);
+#endif
hashCode = (hashCode * 397) ^ _queryDescription.GetHashCode();
return hashCode;
@@ -699,3 +748,57 @@ public override int GetHashCode()
return !left.Equals(right);
}
}
+
+
+// Changed flags support
+#if CHANGED_FLAGS
+
+public partial struct QueryDescription
+{
+ ///
+ /// A of all components that an should have and which should be flagged as changed.
+ /// If the content of the array is subsequently changed, a should be carried out.
+ ///
+ public Signature Changed { get; private set; } = Signature.Null;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// An array of all components that an should have mandatory.
+ /// An array of all components of which an should have at least one.
+ /// An array of all components of which an should not have any.
+ /// All components that an should have mandatory.
+ /// All components that an should have and which should be flagged as changed.
+
+ public QueryDescription(Signature? all = null, Signature? any = null, Signature? none = null, Signature? exclusive = null, Signature? changed = null)
+ {
+ All = all ?? All;
+ Any = any ?? Any;
+ None = none ?? None;
+ Exclusive = exclusive ?? Exclusive;
+
+ if (changed != null)
+ {
+ Changed = changed.Value;
+ Any = Signature.Add(Any, Changed);
+ }
+
+ _hashCode = -1;
+ _hashCode = GetHashCode();
+ }
+
+ ///
+ /// All components that an should have and which should be flagged as changed.
+ ///
+ /// The generic type.
+ /// The same instance for chained operations.
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+}
+
+#endif
diff --git a/src/Arch/Core/QueryEnumerators.cs b/src/Arch/Core/QueryEnumerators.cs
new file mode 100644
index 00000000..9951ca56
--- /dev/null
+++ b/src/Arch/Core/QueryEnumerators.cs
@@ -0,0 +1,282 @@
+using Arch.Core.Utils;
+using CommunityToolkit.HighPerformance;
+
+namespace Arch.Core;
+
+[SkipLocalsInit]
+public ref struct QueryEntityEnumerator
+{
+#if CHANGED_FLAGS
+ private readonly bool _checkChanged;
+ private readonly BitSet _changedMask;
+#endif
+
+ private QueryChunkEnumerator _chunkEnumerator;
+ private Ref _entityPtr;
+ private Ref _chunk;
+ private int _entityIndex = -1;
+
+ public QueryEntityEnumerator(Query query)
+ {
+#if CHANGED_FLAGS
+ _checkChanged = query.HasChangedFilter;
+ _changedMask = query.Changed;
+#endif
+ _chunkEnumerator = query.GetEnumerator();
+ }
+
+ public readonly ref Entity Current
+ {
+ get => ref Unsafe.Add(ref _entityPtr.Value, _entityIndex);
+ }
+
+ public bool MoveNext()
+ {
+#if CHANGED_FLAGS
+ if (_checkChanged)
+ {
+ while (true)
+ {
+ // Move to the next entity in the current chunk
+ if (MoveNextChangedEntity())
+ {
+ return true; // Return true if the next entity is valid
+ }
+
+ // Move to the next chunk in the query
+ if (!MoveNextChangedChunk())
+ {
+ return false; // Return false if there's no more chunks
+ }
+ }
+ }
+#endif
+
+ while (true)
+ {
+ // Move to the next entity in the current chunk
+ if (MoveNextEntity())
+ {
+ return true; // Return true if the next entity is valid
+ }
+
+ // Move to the next chunk in the query
+ if (!MoveNextChunk())
+ {
+ return false; // Return false if there's no more chunks
+ }
+ }
+ }
+
+ public QueryEntityEnumerator GetEnumerator()
+ {
+ return this;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool MoveNextChunk()
+ {
+ while (_chunkEnumerator.MoveNext())
+ {
+ ref var chunk = ref _chunkEnumerator.Current;
+
+ // Skip empty chunks
+ if (chunk.Count <= 0)
+ {
+ continue;
+ }
+
+ _chunk = new Ref(ref chunk);
+ _entityIndex = chunk.Count;
+ _entityPtr = new Ref(ref chunk.Entity(0));
+ return true;
+ }
+
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool MoveNextEntity()
+ {
+ unchecked
+ {
+ return --_entityIndex >= 0;
+ }
+ }
+
+#if CHANGED_FLAGS
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool MoveNextChangedChunk()
+ {
+ while (_chunkEnumerator.MoveNext())
+ {
+ ref var chunk = ref _chunkEnumerator.Current;
+
+ // Skip empty and non-changed chunks
+ if (chunk.Count <= 0 || !chunk.IsAnyChanged(_changedMask))
+ {
+ continue;
+ }
+
+ _chunk = new Ref(ref chunk);
+ _entityIndex = chunk.Count;
+ _entityPtr = new Ref(ref chunk.Entity(0));
+ return true;
+ }
+
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool MoveNextChangedEntity()
+ {
+ unchecked
+ {
+ // Skip non-changed entities
+ return --_entityIndex >= 0 && _chunk.Value.IsChanged(_entityIndex, _changedMask);
+ }
+ }
+
+#endif
+}
+
+[SkipLocalsInit]
+public ref struct QueryComponentEnumerator
+{
+#if CHANGED_FLAGS
+ private readonly bool _checkChanged;
+ private readonly BitSet _changedMask;
+#endif
+
+ private QueryChunkEnumerator _chunkEnumerator;
+ private Ref _cmpPtr;
+ private Ref _chunkPtr;
+ private int _entityIndex = -1;
+
+ public QueryComponentEnumerator(Query query)
+ {
+#if CHANGED_FLAGS
+ _checkChanged = query.HasChangedFilter;
+ _changedMask = query.Changed;
+#endif
+ _chunkEnumerator = query.GetEnumerator();
+ }
+
+ public readonly ref T Current
+ {
+ get => ref Unsafe.Add(ref _cmpPtr.Value, _entityIndex);
+ }
+
+ public bool MoveNext()
+ {
+#if CHANGED_FLAGS
+ if (_checkChanged)
+ {
+ while (true)
+ {
+ // Move to the next entity in the current chunk
+ if (MoveNextChangedEntity())
+ {
+ return true; // Return true if the next entity is valid
+ }
+
+ // Move to the next chunk in the query
+ if (!MoveNextChangedChunk())
+ {
+ return false; // Return false if there's no more chunks
+ }
+ }
+ }
+#endif
+
+ while (true)
+ {
+ // Move to the next entity in the current chunk
+ if (MoveNextEntity())
+ {
+ return true; // Return true if the next entity is valid
+ }
+
+ // Move to the next chunk in the query
+ if (!MoveNextChunk())
+ {
+ return false; // Return false if there's no more chunks
+ }
+ }
+ }
+
+ public QueryComponentEnumerator GetEnumerator()
+ {
+ return this;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool MoveNextChunk()
+ {
+ while (_chunkEnumerator.MoveNext())
+ {
+ ref var chunk = ref _chunkEnumerator.Current;
+
+ // Skip empty chunks
+ if (chunk.Count <= 0)
+ {
+ continue;
+ }
+
+ _chunkPtr = new Ref(ref chunk);
+ _entityIndex = chunk.Count;
+ _cmpPtr = new Ref(ref chunk.GetFirst());
+ return true;
+ }
+
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool MoveNextEntity()
+ {
+ unchecked
+ {
+ return --_entityIndex >= 0;
+ }
+ }
+
+#if CHANGED_FLAGS
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool MoveNextChangedChunk()
+ {
+ while (_chunkEnumerator.MoveNext())
+ {
+ ref var chunk = ref _chunkEnumerator.Current;
+
+ // Skip empty and non-changed chunks
+ if (chunk.Count <= 0 || !chunk.IsAnyChanged(_changedMask))
+ {
+ continue;
+ }
+
+ _chunkPtr = new Ref(ref chunk);
+ _entityIndex = chunk.Count;
+ _cmpPtr = new Ref(ref chunk.GetFirst());
+ return true;
+ }
+
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool MoveNextChangedEntity()
+ {
+ unchecked
+ {
+ // Skip non-changed entities
+ return --_entityIndex >= 0 && _chunkPtr.Value.IsChanged(_entityIndex, _changedMask);
+ }
+ }
+
+#endif
+}
+
+
diff --git a/src/Arch/Core/Utils/AssertionUtils.cs b/src/Arch/Core/Utils/AssertionUtils.cs
new file mode 100644
index 00000000..a5f19123
--- /dev/null
+++ b/src/Arch/Core/Utils/AssertionUtils.cs
@@ -0,0 +1,10 @@
+namespace Arch.Core.Utils;
+
+public static class AssertionUtils
+{
+ [Conditional("DEBUG"), Conditional("CHANGED_FLAGS")]
+ public static void AssertNoChangedFilter(QueryDescription queryDescription, [CallerMemberName] string? callerName = null)
+ {
+ Debug.Assert(queryDescription.Changed.Count == 0, $"The method {callerName} does not support queries with dirty filters.");
+ }
+}
diff --git a/src/Arch/Core/Utils/BitSet.cs b/src/Arch/Core/Utils/BitSet.cs
index a82c593a..e39f7c9f 100644
--- a/src/Arch/Core/Utils/BitSet.cs
+++ b/src/Arch/Core/Utils/BitSet.cs
@@ -24,7 +24,7 @@ public sealed class BitSet
public static int RequiredLength(int id)
{
-#if NET7_0
+#if NET7_0_OR_GREATER
return (id >> 5) + int.Sign(id & BitSize);
#else
return (int)Math.Ceiling((float)id / BitSize);
@@ -53,7 +53,22 @@ public static int RequiredLength(int id)
///
public BitSet()
{
- _bits = new uint[_padding];
+ _bits = [];
+ }
+
+ ///
+ /// Initializes a new instance of the class with the required capacity.
+ ///
+ /// The initial capacity.
+ public BitSet(int capacity)
+ {
+ // Calculate the number of uint blocks needed for 'capacity' bits
+ // Each block holds BitSize + 1 bits (usually 32)
+ int requiredBlocks = (capacity + BitSize) / (BitSize + 1);
+
+ // Round up to nearest multiple of _padding
+ int size = (requiredBlocks + _padding - 1) / _padding * _padding;
+ _bits = new uint[size];
}
///
@@ -169,6 +184,16 @@ public void ClearAll()
[SkipLocalsInit]
public bool All(BitSet other)
{
+ if (Length == 0)
+ {
+ return true;
+ }
+
+ if (other.Length == 0)
+ {
+ return false;
+ }
+
var min = Math.Min(Math.Min(Length, other.Length), _max);
if (!Vector.IsHardwareAccelerated || min < _padding)
{
@@ -230,6 +255,16 @@ public bool All(BitSet other)
/// True if they match, false if not.
public bool Any(BitSet other)
{
+ if (Length == 0)
+ {
+ return true;
+ }
+
+ if (other.Length == 0)
+ {
+ return false;
+ }
+
var min = Math.Min(Math.Min(Length, other.Length), _max);
if (!Vector.IsHardwareAccelerated || min < _padding)
{
@@ -291,6 +326,11 @@ public bool Any(BitSet other)
/// True if none match, false if not.
public bool None(BitSet other)
{
+ if (Length == 0 || other.Length == 0)
+ {
+ return true;
+ }
+
var min = Math.Min(Math.Min(Length, other.Length), _max);
if (!Vector.IsHardwareAccelerated || min < _padding)
{
@@ -333,8 +373,17 @@ public bool None(BitSet other)
/// True if they match, false if not.
public bool Exclusive(BitSet other)
{
- var min = Math.Min(Math.Min(Length, other.Length), _max);
+ if (Length == 0 && other.Length == 0)
+ {
+ return true;
+ }
+ if (Length == 0 || other.Length == 0)
+ {
+ return false;
+ }
+
+ var min = Math.Min(Math.Min(Length, other.Length), _max);
if (!Vector.IsHardwareAccelerated || min < _padding)
{
var bits = _bits.AsSpan();
diff --git a/src/Arch/Core/World.cs b/src/Arch/Core/World.cs
index 5af4c0ba..1c7b1fe5 100644
--- a/src/Arch/Core/World.cs
+++ b/src/Arch/Core/World.cs
@@ -407,6 +407,8 @@ public Query Query(in QueryDescription queryDescription)
[Pure]
public int CountEntities(in QueryDescription queryDescription)
{
+ AssertionUtils.AssertNoChangedFilter(queryDescription);
+
var counter = 0;
var query = Query(in queryDescription);
foreach (var archetype in query.GetArchetypeIterator())
@@ -424,20 +426,19 @@ public int CountEntities(in QueryDescription queryDescription)
/// The which specifies the components or s for which to search.
/// The receiving the found s.
/// The start index inside the . Default is 0.
- public void GetEntities(in QueryDescription queryDescription, Span list, int start = 0)
+ /// The amount of entities matched by the query.
+ public int GetEntities(in QueryDescription queryDescription, Span list, int start = 0)
{
var index = 0;
var query = Query(in queryDescription);
- foreach (ref var chunk in query)
+
+ foreach (ref var entity in query.GetEntityEnumerator())
{
- ref var entityFirstElement = ref chunk.Entity(0);
- foreach (var entityIndex in chunk)
- {
- var entity = Unsafe.Add(ref entityFirstElement, entityIndex);
- list[start + index] = entity;
- index++;
- }
+ list[start + index] = entity;
+ index++;
}
+
+ return index;
}
///
@@ -446,8 +447,10 @@ public void GetEntities(in QueryDescription queryDescription, Span list,
/// The which specifies the components for which to search.
/// The receiving s containing s with the matching components.
/// The start index inside the . Default is 0.
- public void GetArchetypes(in QueryDescription queryDescription, Span archetypes, int start = 0)
+ public int GetArchetypes(in QueryDescription queryDescription, Span archetypes, int start = 0)
{
+ AssertionUtils.AssertNoChangedFilter(queryDescription);
+
var index = 0;
var query = Query(in queryDescription);
foreach (var archetype in query.GetArchetypeIterator())
@@ -455,6 +458,8 @@ public void GetArchetypes(in QueryDescription queryDescription, Span
archetypes[start + index] = archetype;
index++;
}
+
+ return index;
}
///
@@ -463,15 +468,37 @@ public void GetArchetypes(in QueryDescription queryDescription, Span
/// The which specifies which components are searched for.
/// The receiving s containing s with the matching components.
/// The start index inside the . Default is 0.
- public void GetChunks(in QueryDescription queryDescription, Span chunks, int start = 0)
+ public int GetChunks(in QueryDescription queryDescription, Span chunks, int start = 0)
{
var index = 0;
var query = Query(in queryDescription);
+
+#if CHANGED_FLAGS
+ if (query.HasChangedFilter)
+ {
+ var changedMask = query.Changed;
+ foreach (ref var chunk in query)
+ {
+ if (!chunk.IsAnyChanged(changedMask))
+ {
+ continue;
+ }
+
+ chunks[start + index] = chunk;
+ index++;
+ }
+
+ return index;
+ }
+#endif
+
foreach (ref var chunk in query)
{
chunks[start + index] = chunk;
index++;
}
+
+ return index;
}
///
@@ -735,14 +762,9 @@ public partial class World
public void Query(in QueryDescription queryDescription, ForEach forEntity)
{
var query = Query(in queryDescription);
- foreach (ref var chunk in query)
+ foreach (ref var entity in query.GetEntityEnumerator())
{
- ref var entityLastElement = ref chunk.Entity(0);
- foreach (var entityIndex in chunk)
- {
- var entity = Unsafe.Add(ref entityLastElement, entityIndex);
- forEntity(entity);
- }
+ forEntity(entity);
}
}
@@ -755,16 +777,10 @@ public void Query(in QueryDescription queryDescription, ForEach forEntity)
public void InlineQuery(in QueryDescription queryDescription) where T : struct, IForEach
{
var t = new T();
-
var query = Query(in queryDescription);
- foreach (ref var chunk in query)
+ foreach (ref var entity in query.GetEntityEnumerator())
{
- ref var entityFirstElement = ref chunk.Entity(0);
- foreach (var entityIndex in chunk)
- {
- var entity = Unsafe.Add(ref entityFirstElement, entityIndex);
- t.Update(entity);
- }
+ t.Update(entity);
}
}
@@ -778,14 +794,9 @@ public void InlineQuery(in QueryDescription queryDescription) where T : struc
public void InlineQuery(in QueryDescription queryDescription, ref T iForEach) where T : struct, IForEach
{
var query = Query(in queryDescription);
- foreach (ref var chunk in query)
+ foreach (ref var entity in query.GetEntityEnumerator())
{
- ref var entityFirstElement = ref chunk.Entity(0);
- foreach (var entityIndex in chunk)
- {
- var entity = Unsafe.Add(ref entityFirstElement, entityIndex);
- iForEach.Update(entity);
- }
+ iForEach.Update(entity);
}
}
}
@@ -808,6 +819,8 @@ public partial class World
[StructuralChange]
public void Destroy(in QueryDescription queryDescription)
{
+ AssertionUtils.AssertNoChangedFilter(queryDescription);
+
var query = Query(in queryDescription);
foreach (var archetype in query.GetArchetypeIterator())
{
@@ -847,6 +860,8 @@ public void Destroy(in QueryDescription queryDescription)
/// The value of the component to set.
public void Set(in QueryDescription queryDescription, in T? value = default)
{
+ AssertionUtils.AssertNoChangedFilter(queryDescription);
+
var query = Query(in queryDescription);
foreach (ref var chunk in query)
{
@@ -876,13 +891,15 @@ public void Set(in QueryDescription queryDescription, in T? value = default)
[StructuralChange]
public void Add(in QueryDescription queryDescription, in T? component = default)
{
+ AssertionUtils.AssertNoChangedFilter(queryDescription);
+
// BitSet to stack/span bitset, size big enough to contain ALL registered components.
Span stack = stackalloc uint[BitSet.RequiredLength(ComponentRegistry.Size)];
var query = Query(in queryDescription);
foreach (var archetype in query.GetArchetypeIterator())
{
- // Archetype with T shouldnt be skipped to prevent undefined behaviour.
+ // Archetype with T shouldn't be skipped to prevent undefined behaviour.
if (archetype.EntityCount == 0 || archetype.Has())
{
continue;
@@ -933,6 +950,8 @@ public void Add(in QueryDescription queryDescription, in T? component = defau
[StructuralChange]
public void Remove(in QueryDescription queryDescription)
{
+ AssertionUtils.AssertNoChangedFilter(queryDescription);
+
// BitSet to stack/span bitset, size big enough to contain ALL registered components.
Span stack = stackalloc uint[BitSet.RequiredLength(ComponentRegistry.Size)];
@@ -1745,3 +1764,90 @@ public Signature GetSignature(Entity entity)
}
#endregion
+
+#if CHANGED_FLAGS
+
+public partial class World
+{
+
+ ///
+ /// Checks whether the component of an has been flagged changed.
+ ///
+ /// The component type.
+ /// The .
+ public bool IsChanged(Entity entity)
+ {
+ var componentType = Component.ComponentType;
+ var entityData = EntityInfo.GetEntityData(entity.Id);
+ return entityData.Archetype.IsChanged(ref entityData.Slot, componentType);
+ }
+
+ ///
+ /// Checks whether the component of an has been flagged changed.
+ ///
+ /// The .
+ /// The component .
+ public bool IsChanged(Entity entity, ComponentType type)
+ {
+ var entityData = EntityInfo.GetEntityData(entity.Id);
+ return entityData.Archetype.IsChanged(ref entityData.Slot, type);
+ }
+
+ ///
+ /// Flags the component of type an as changed.
+ ///
+ /// The component type.
+ /// The .
+ public void MarkChanged(Entity entity)
+ {
+ var componentType = Component.ComponentType;
+ var entityData = EntityInfo.GetEntityData(entity.Id);
+ entityData.Archetype.MarkChanged(ref entityData.Slot, componentType);
+ }
+
+ ///
+ /// Flags the component of type an as changed.
+ ///
+ /// The .
+ /// The component .
+ public void MarkChanged(Entity entity, ComponentType type)
+ {
+ var entityData = EntityInfo.GetEntityData(entity.Id);
+ entityData.Archetype.MarkChanged(ref entityData.Slot, type);
+ }
+
+ ///
+ /// Clears the changed flag of the component of an .
+ ///
+ /// The component type.
+ /// The .
+ public void ClearChanged(Entity entity)
+ {
+ var componentType = Component.ComponentType;
+ var entityData = EntityInfo.GetEntityData(entity.Id);
+ entityData.Archetype.ClearChanged(ref entityData.Slot, componentType);
+ }
+
+ ///
+ /// Clears the changed flag of the component of an .
+ ///
+ /// The .
+ /// The component .
+ public void ClearChanged(Entity entity, ComponentType type)
+ {
+ var entityData = EntityInfo.GetEntityData(entity.Id);
+ entityData.Archetype.ClearChanged(ref entityData.Slot, type);
+ }
+
+ ///
+ /// Clears the changed flag for all components of an .
+ ///
+ /// The .
+ public void ClearChanged(Entity entity)
+ {
+ var entityData = EntityInfo.GetEntityData(entity.Id);
+ entityData.Archetype.ClearChanged(ref entityData.Slot);
+ }
+}
+
+#endif
diff --git a/src/Arch/Templates/QueryDescription.WithDirty.cs b/src/Arch/Templates/QueryDescription.WithDirty.cs
new file mode 100644
index 00000000..c40f9737
--- /dev/null
+++ b/src/Arch/Templates/QueryDescription.WithDirty.cs
@@ -0,0 +1,205 @@
+#if CHANGED_FLAGS
+
+using System;
+using System.Diagnostics.Contracts;
+using Arch.Core;
+using Arch.Core.Utils;
+
+namespace Arch.Core;
+public partial struct QueryDescription
+{
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged()
+ {
+ Changed = Component.Signature;
+ Build();
+ return ref this;
+ }
+}
+
+#endif
diff --git a/src/Arch/Templates/QueryDescription.WithDirty.tt b/src/Arch/Templates/QueryDescription.WithDirty.tt
new file mode 100644
index 00000000..6b07f2e3
--- /dev/null
+++ b/src/Arch/Templates/QueryDescription.WithDirty.tt
@@ -0,0 +1,35 @@
+<#@ template language="C#" #>
+<#@ output extension=".cs" #>
+<#@ import namespace="System.Text" #>
+<#@ include file="Helpers.ttinclude" #>
+
+#if CHANGED_FLAGS
+
+using System;
+using System.Diagnostics.Contracts;
+using Arch.Core;
+using Arch.Core.Utils;
+
+namespace Arch.Core;
+public partial struct QueryDescription
+{
+ <#
+ for (var index = 2; index <= Amount; index++)
+ {
+ var generics = AppendGenerics(index);
+ #>
+
+ [UnscopedRef]
+ public ref QueryDescription WithChanged<<#= generics #>>()
+ {
+ All = Component<<#= generics #>>.Signature;
+ Build();
+ return ref this;
+ }
+ <#
+ }
+ #>
+}
+
+#endif
+