fix(ecs): sparse_set not properly removing elements

This commit is contained in:
light7734 2025-09-21 08:41:56 +03:30
parent 1b6d53f1c1
commit 04c2e59ada
Signed by: light7734
GPG key ID: 8C30176798F1A6BA
3 changed files with 60 additions and 10 deletions

View file

@ -76,8 +76,33 @@ Suite element_raii = [] {
set.remove(idx);
expect_eq(set.get_size(), 10'000 - (idx + 1));
expect_throw([&] { std::ignore = set.at(idx); });
expect_false(set.contains(idx));
expect_throw([&] { std::ignore = set.at(idx); });
}
};
Case { "removed elements won't be iterated again" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 10'000))
{
set.insert(idx, idx);
}
set.remove(0);
set.remove(32);
set.remove(69);
set.remove(420);
set.remove(9'999);
for (auto &[identifier, value] : set)
{
expect_eq(identifier, value);
expect_ne(value, 0);
expect_ne(value, 32);
expect_ne(value, 69);
expect_ne(value, 420);
expect_ne(value, 9'999);
}
};
};

View file

@ -8,9 +8,16 @@ using Entity = uint32_t;
/** A registry of components, the heart of an ECS architecture.
*
* @todo(Light): optimize multi-component views
* @todo(Light): support more than 2-component views
* @todo(Light): handle edge cases or specify the undefined behaviors
* @todo(Light): Implement grouping
* @todo(Light): Implement identifier recycling
* @todo(Light): Optimize views/each
* @todo(Light): Support >2 component views
* @todo(Light): Handle more edge cases or specify the undefined behaviors
*
* @ref https://skypjack.github.io/
* @ref https://github.com/alecthomas/entityx
* @ref https://github.com/skypjack/entt
* @ref https://github.com/SanderMertens/flecs
*/
class Registry
{
@ -93,7 +100,7 @@ public:
}
template<typename Component_T>
auto view() -> SparseSet<Component_T, Entity>&
auto view() -> SparseSet<Component_T, Entity> &
{
return get_derived_set<Component_T>();
};

View file

@ -25,10 +25,6 @@ public:
virtual void remove(Identifier_T identifier) = 0;
};
/**
*
* @todo(Light): implement identifier recycling.
*/
template<typename Value_T, typename Identifier_T = uint32_t>
class SparseSet: public TypeErasedSparseSet<Identifier_T>
{
@ -77,7 +73,29 @@ public:
auto &idx = m_sparse[identifier];
auto &[entity, component] = m_dense[idx];
idx = null_identifier;
auto &[last_entity, last_component] = m_dense.back();
auto &last_idx = m_sparse[last_entity];
// removed entity is in dense's back, just pop and invalidate sparse[identifier]
if (entity == last_entity)
{
idx = null_identifier;
m_dense.pop_back();
}
else
{
// swap dense's 'back' to 'removed'
std::swap(component, last_component);
entity = last_entity;
// make sparse to point to new idx
last_idx = idx;
// pop dense and invalidate sparse[identifier]
idx = null_identifier;
m_dense.pop_back();
}
++m_dead_count;
--m_alive_count;
}