Skip to content

Commit

Permalink
feat(ecs): add entity destruction detection to observers
Browse files Browse the repository at this point in the history
  • Loading branch information
kuukitenshi committed Feb 27, 2025
1 parent 00c4a2a commit 38c8983
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 5 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Entity destruction detection to observers (#1458, **@kuukitenshi**).


## [v0.6.0] - 2025-02-10

### Added
Expand Down
6 changes: 6 additions & 0 deletions core/include/cubos/core/ecs/cubos.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,11 @@ namespace cubos::core::ecs
return std::move(*this).onRemove(reflection::reflect<T>(), target);
}

/// @brief Triggers the observer whenever the given entity with it is destroyed.
/// @param target Target index. By default, the last specified target or 0.
/// @return Builder.
ObserverBuilder&& onDestroy(int target = -1) &&;

/// @brief Forces the next entity query argument to have the given target.
/// @param target Target index. By default, the last specified target or 0.
/// @return Builder.
Expand Down Expand Up @@ -704,6 +709,7 @@ namespace cubos::core::ecs
std::vector<SystemOptions> mOptions;
int mDefaultTarget{0};
bool mRemove{false};
bool mDestroy{false};
ColumnId mColumnId{ColumnId::Invalid};
};
} // namespace cubos::core::ecs
14 changes: 14 additions & 0 deletions core/include/cubos/core/ecs/observer/observers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ namespace cubos::core::ecs
/// @return Whether an observer was triggered.
bool notifyRemove(CommandBuffer& commandBuffer, Entity entity, ColumnId column);

/// @brief Notifies was destroyed.
/// @param commandBuffer Command buffer to record the any commands emitted by the observer.
/// @param entity Entity.
/// @param column Column.
/// @return Whether an observer was triggered.
bool notifyDestroy(CommandBuffer& commandBuffer, Entity entity, ColumnId column);

/// @brief Hooks an observer to the addition of a column.
/// @param column Column.
/// @param observer Observer system.
Expand All @@ -47,6 +54,12 @@ namespace cubos::core::ecs
/// @return Observer identifier.
ObserverId hookOnRemove(ColumnId column, System<void> observer);

/// @brief Hooks an observer to the entity destruction.
/// @param column Column.
/// @param observer Observer system.
/// @return Observer identifier.
ObserverId hookOnDestroy(ColumnId column, System<void> observer);

/// @brief Unhooks an observer.
/// @param id Observer identifier.
void unhook(ObserverId id);
Expand All @@ -55,5 +68,6 @@ namespace cubos::core::ecs
std::vector<System<void>*> mObservers; /// Indexed by observer identifier.
std::unordered_multimap<ColumnId, ObserverId, ColumnIdHash> mOnAdd;
std::unordered_multimap<ColumnId, ObserverId, ColumnIdHash> mOnRemove;
std::unordered_multimap<ColumnId, ObserverId, ColumnIdHash> mOnDestroy;
};
} // namespace cubos::core::ecs
1 change: 1 addition & 0 deletions core/include/cubos/core/ecs/plugin_queue.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace cubos::core::ecs
{
std::vector<Plugin> toAdd;
std::vector<Plugin> toRemove;
std::vector<Plugin> toDestroy;
std::mutex mutex;
};
} // namespace cubos::core::ecs
38 changes: 37 additions & 1 deletion core/src/ecs/cubos.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,16 +315,21 @@ bool Cubos::update()
{
this->uninstall(plugin);
}
for (auto plugin : mState->pluginQueue.toDestroy)
{
this->uninstall(plugin);

Check warning on line 320 in core/src/ecs/cubos.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/ecs/cubos.cpp#L320

Added line #L320 was not covered by tests
}
for (auto plugin : mState->pluginQueue.toAdd)
{
this->install(plugin);
}

// If any plugins were added, recompile the system execution chains, and rerun the startup systems.
if (!mState->pluginQueue.toAdd.empty() || !mState->pluginQueue.toRemove.empty())
if (!mState->pluginQueue.toAdd.empty() || !mState->pluginQueue.toRemove.empty() || !mState->pluginQueue.toDestroy.empty())
{
// Clear the plugin queue.
mState->pluginQueue.toRemove.clear();
mState->pluginQueue.toDestroy.clear();
mState->pluginQueue.toAdd.clear();

// Build new schedules.
Expand Down Expand Up @@ -872,6 +877,7 @@ auto Cubos::ObserverBuilder::onAdd(const reflection::Type& type, int target) &&

mOptions.back().observedTarget = target;
mRemove = false;
mDestroy = false;
mColumnId = ColumnId::make(mCubos.mWorld->types().id(type));
return std::move(*this);
}
Expand Down Expand Up @@ -900,10 +906,36 @@ auto Cubos::ObserverBuilder::onRemove(const reflection::Type& type, int target)

mOptions.back().observedTarget = target;
mRemove = true;
mDestroy = false;
mColumnId = ColumnId::make(mCubos.mWorld->types().id(type));
return std::move(*this);
}

auto Cubos::ObserverBuilder::onDestroy(int target) && -> ObserverBuilder&&

Check warning on line 914 in core/src/ecs/cubos.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/ecs/cubos.cpp#L914

Added line #L914 was not covered by tests
{
CUBOS_ASSERT(mColumnId == ColumnId::Invalid, "An observer can only have at most one hook");

Check warning on line 916 in core/src/ecs/cubos.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/ecs/cubos.cpp#L916

Added line #L916 was not covered by tests

if (mOptions.empty())
{
mOptions.emplace_back();

Check warning on line 920 in core/src/ecs/cubos.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/ecs/cubos.cpp#L920

Added line #L920 was not covered by tests
}

if (target == -1)
{
target = mDefaultTarget;

Check warning on line 925 in core/src/ecs/cubos.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/ecs/cubos.cpp#L925

Added line #L925 was not covered by tests
}
else
{
mDefaultTarget = target;

Check warning on line 929 in core/src/ecs/cubos.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/ecs/cubos.cpp#L929

Added line #L929 was not covered by tests
}

mOptions.back().observedTarget = target;
mRemove = true;
mDestroy = true;
return std::move(*this);

Check warning on line 935 in core/src/ecs/cubos.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/ecs/cubos.cpp#L932-L935

Added lines #L932 - L935 were not covered by tests
}


auto Cubos::ObserverBuilder::entity(int target) && -> ObserverBuilder&&
{
if (mOptions.empty())
Expand Down Expand Up @@ -1034,6 +1066,10 @@ void Cubos::ObserverBuilder::finish(System<void> system)
{
id = mCubos.mWorld->observers().hookOnRemove(mColumnId, std::move(system));
}
else if (mDestroy)
{
id = mCubos.mWorld->observers().hookOnDestroy(mColumnId, std::move(system));

Check warning on line 1071 in core/src/ecs/cubos.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/ecs/cubos.cpp#L1071

Added line #L1071 was not covered by tests
}
else
{
id = mCubos.mWorld->observers().hookOnAdd(mColumnId, std::move(system));
Expand Down
26 changes: 26 additions & 0 deletions core/src/ecs/observer/observers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ bool Observers::notifyRemove(CommandBuffer& commandBuffer, Entity entity, Column
return triggered;
}

bool Observers::notifyDestroy(CommandBuffer& commandBuffer, Entity entity, ColumnId column)
{
bool triggered = false;

auto range = mOnDestroy.equal_range(column);
for (auto it = range.first; it != range.second; ++it)
{
auto* observer = mObservers[it->second.inner];
if (observer != nullptr)
{
observer->run({.cmdBuffer = commandBuffer, .observedEntity = entity});
triggered = true;
}
}

return triggered;
}

ObserverId Observers::hookOnAdd(ColumnId column, System<void> observer)
{
ObserverId id{.inner = mObservers.size()};
Expand All @@ -63,6 +81,14 @@ ObserverId Observers::hookOnRemove(ColumnId column, System<void> observer)
return id;
}

ObserverId Observers::hookOnDestroy(ColumnId column, System<void> observer)
{
ObserverId id{.inner = mObservers.size()};
mObservers.push_back(new System<void>(std::move(observer)));
mOnDestroy.emplace(column, id);
return id;
}

void Observers::unhook(ObserverId id)
{
CUBOS_ASSERT(mObservers[id.inner] != nullptr);
Expand Down
5 changes: 5 additions & 0 deletions core/src/ecs/world.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ void World::destroy(Entity entity)

CommandBuffer cmdBuffer{*this};
bool observed = false;

// For each column, notify the observers.
for (auto columnId = mArchetypeGraph.first(archetype); columnId != ColumnId::Invalid;
columnId = mArchetypeGraph.next(archetype, columnId))
Expand All @@ -170,6 +171,10 @@ void World::destroy(Entity entity)
{
observed = true;
}
if (mObservers->notifyDestroy(cmdBuffer, entity, columnId))
{
observed = true;

Check warning on line 176 in core/src/ecs/world.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/ecs/world.cpp#L176

Added line #L176 was not covered by tests
}
}

// Remove entity from the pool and from its dense table.
Expand Down
23 changes: 19 additions & 4 deletions core/tests/ecs/observer/observers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,36 @@ TEST_CASE("ecs::Observers")
static int acc = 0;
auto hook1 = obs.hookOnAdd(ColumnId{.inner = 1}, System<void>::make(world, []() { acc += 1; }, {}));
auto hook2 = obs.hookOnRemove(ColumnId{.inner = 1}, System<void>::make(world, []() { acc -= 1; }, {}));
auto hook3 = obs.hookOnDestroy(ColumnId{.inner = 1}, System<void>::make(world, []() { acc -= 2; }, {}));
CHECK(acc == 0);

obs.notifyAdd(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 0});
CHECK(acc == 0);
obs.notifyRemove(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 2});
CHECK(acc == 0);
obs.notifyDestroy(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 2});
CHECK(acc == 0);

obs.notifyAdd(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1});
CHECK(acc == 1);
obs.notifyRemove(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1});
CHECK(acc == 0);
obs.notifyDestroy(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1});
CHECK(acc == -2);

obs.unhook(hook2);
obs.notifyAdd(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1});
CHECK(acc == 1);
CHECK(acc == -1);
obs.notifyRemove(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1});
CHECK(acc == 1);
CHECK(acc == -1);

obs.unhook(hook3);
obs.notifyAdd(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1});
CHECK(acc == 0);
obs.notifyDestroy(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1});
CHECK(acc == 0);

obs.unhook(hook1);
obs.notifyAdd(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1});
CHECK(acc == 1);
}
CHECK(acc == 0);
}
1 change: 1 addition & 0 deletions engine/include/cubos/engine/render/voxels/grid.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ namespace cubos::engine

/// @brief Translation applied to the grid.
glm::vec3 offset = {0.0F, 0.0F, 0.0F};

};
} // namespace cubos::engine

0 comments on commit 38c8983

Please sign in to comment.