From 1a5ad4063fcf2e0e91140ec4cdd24a8c52593920 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Wed, 15 Jul 2026 23:23:32 +0000 Subject: [PATCH] Add a structural Version to Archetype for cheap change detection Version is a monotonic counter that advances whenever an entity enters or leaves an archetype. It is advanced inside the EntityCount setter, the only way to change the count, so every add, remove, move, clear and bulk restore path advances it with no way to bypass, the Dangerous serialization extensions included. It lets a query detect structural change by comparing the Version of its matched archetypes, O(archetypes), instead of hashing every entity id, O(entities). Unchanged versions guarantee unchanged membership. A same-count remove-then-add swap still advances the Version, which an EntityCount comparison alone would miss. --- src/Arch.Tests/ArchetypeTest.cs | 31 +++++++++++++++++++++++++++++++ src/Arch/Core/Archetype.cs | 23 ++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Arch.Tests/ArchetypeTest.cs b/src/Arch.Tests/ArchetypeTest.cs index bfea697b..8cf2bee5 100644 --- a/src/Arch.Tests/ArchetypeTest.cs +++ b/src/Arch.Tests/ArchetypeTest.cs @@ -213,6 +213,37 @@ public void Move() That(otherArchetype.Get(ref newSlot).Y, Is.EqualTo(10)); } + /// + /// Checks that advances whenever s enter or leave, + /// including a same-count remove-then-add swap where ends unchanged. + /// + [Test] + public void Version() + { + var archetype = new Archetype(_group, _baseChunkSize, _baseChunkEntityCount); + + var initial = archetype.Version; + archetype.Add(new Entity(1, 0), out _, out var firstSlot); + var afterFirstAdd = archetype.Version; + That(afterFirstAdd, Is.GreaterThan(initial)); + + archetype.Add(new Entity(2, 0), out _, out _); + var afterSecondAdd = archetype.Version; + That(afterSecondAdd, Is.GreaterThan(afterFirstAdd)); + + // Same-count swap: remove one and add one. EntityCount returns to 2 but Version must still advance. + var countBefore = archetype.EntityCount; + archetype.Remove(firstSlot, out _); + archetype.Add(new Entity(3, 0), out _, out _); + That(archetype.EntityCount, Is.EqualTo(countBefore)); + That(archetype.Version, Is.GreaterThan(afterSecondAdd)); + + // Clearing a non-empty archetype advances the version too. + var beforeClear = archetype.Version; + archetype.Clear(); + That(archetype.Version, Is.GreaterThan(beforeClear)); + } + /// /// Checks if a copy operation between was successful. /// This is checked by value equality of the items and their correct order. diff --git a/src/Arch/Core/Archetype.cs b/src/Arch/Core/Archetype.cs index a06537a3..a81281eb 100644 --- a/src/Arch/Core/Archetype.cs +++ b/src/Arch/Core/Archetype.cs @@ -381,10 +381,31 @@ internal Slot CurrentSlot /// /// The number of s in this . /// + private int _entityCount; + public int EntityCount + { + get => _entityCount; + internal set + { + // Version advances on every write to the population, so an unchanged + // Version proves the entity set of this archetype is unchanged. + // Bumping in the setter makes it impossible for any add, remove, move + // or bulk restore path to change the population without advancing it. + Version++; + _entityCount = value; + } + } + + /// + /// A monotonically increasing version that changes whenever s enter or leave this . + /// If the value is unchanged between two observations, the set of s in this is unchanged. + /// This makes it a cheap structural-change signal for change detection over a query, without materialising or hashing the entities themselves. + /// + public long Version { get; - internal set; + private set; } ///