From 10fdba7fd4563bcb23b65fc4fdf8e7bfb84268f4 Mon Sep 17 00:00:00 2001 From: Ricardo Antunes Date: Wed, 28 Feb 2024 15:46:44 +0000 Subject: [PATCH] feat(ecs): add Observers class --- core/CMakeLists.txt | 3 + core/include/cubos/core/ecs/observer/id.hpp | 22 ++++++ .../cubos/core/ecs/observer/module.dox | 9 +++ .../cubos/core/ecs/observer/observers.hpp | 59 +++++++++++++++ core/src/ecs/observer/id.cpp | 1 + core/src/ecs/observer/observers.cpp | 71 +++++++++++++++++++ core/tests/CMakeLists.txt | 1 + core/tests/ecs/observer/observers.cpp | 36 ++++++++++ 8 files changed, 202 insertions(+) create mode 100644 core/include/cubos/core/ecs/observer/id.hpp create mode 100644 core/include/cubos/core/ecs/observer/module.dox create mode 100644 core/include/cubos/core/ecs/observer/observers.hpp create mode 100644 core/src/ecs/observer/id.cpp create mode 100644 core/src/ecs/observer/observers.cpp create mode 100644 core/tests/ecs/observer/observers.cpp diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 812394268..8a4805391 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -108,6 +108,9 @@ set(CUBOS_CORE_SOURCE "src/ecs/system/arguments/resources.cpp" "src/ecs/system/arguments/world.cpp" + "src/ecs/observer/id.cpp" + "src/ecs/observer/observers.cpp" + "src/ecs/query/term.cpp" "src/ecs/query/data.cpp" "src/ecs/query/filter.cpp" diff --git a/core/include/cubos/core/ecs/observer/id.hpp b/core/include/cubos/core/ecs/observer/id.hpp new file mode 100644 index 000000000..e015ffe32 --- /dev/null +++ b/core/include/cubos/core/ecs/observer/id.hpp @@ -0,0 +1,22 @@ +/// @file +/// @brief Struct @ref cubos::core::ecs::ObserverId. +/// @ingroup core-ecs-observer + +#pragma once + +#include + +namespace cubos::core::ecs +{ + /// @brief Identifies an observer. + /// @ingroup core-ecs-observer + struct ObserverId + { + std::size_t inner; ///< Observer identifier. + + /// @brief Compares two observer identifiers for equality. + /// @param other Other observer identifier. + /// @return Whether the two observer identifiers are equal. + bool operator==(const ObserverId& other) const = default; + }; +} // namespace cubos::core::ecs diff --git a/core/include/cubos/core/ecs/observer/module.dox b/core/include/cubos/core/ecs/observer/module.dox new file mode 100644 index 000000000..aee15c9f1 --- /dev/null +++ b/core/include/cubos/core/ecs/observer/module.dox @@ -0,0 +1,9 @@ +/// @dir +/// @brief @ref core-ecs-observer directory. + +namespace cubos::core::ecs +{ + /// @defgroup core-ecs-observer Observer + /// @ingroup core-ecs + /// @brief Observer part of the ECS. +} diff --git a/core/include/cubos/core/ecs/observer/observers.hpp b/core/include/cubos/core/ecs/observer/observers.hpp new file mode 100644 index 000000000..2745224fb --- /dev/null +++ b/core/include/cubos/core/ecs/observer/observers.hpp @@ -0,0 +1,59 @@ +/// @file +/// @brief Class @ref cubos::core::ecs::Observers. +/// @ingroup core-ecs-observer + +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace cubos::core::ecs +{ + /// @brief Stores and manages all of the observers associated with a world. + /// @ingroup core-ecs-observer + class Observers + { + public: + ~Observers(); + + /// @brief Notifies that the given entity has a new column. + /// @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 notifyAdd(CommandBuffer& commandBuffer, Entity entity, ColumnId column); + + /// @brief Notifies that the given entity has lost a column. + /// @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 notifyRemove(CommandBuffer& commandBuffer, Entity entity, ColumnId column); + + /// @brief Hooks an observer to the addition of a column. + /// @param column Column. + /// @param observer Observer system. + /// @return Observer identifier. + ObserverId hookOnAdd(ColumnId column, System observer); + + /// @brief Hooks an observer to the removal of a column. + /// @param column Column. + /// @param observer Observer system. + /// @return Observer identifier. + ObserverId hookOnRemove(ColumnId column, System observer); + + /// @brief Unhooks an observer. + /// @param id Observer identifier. + void unhook(ObserverId id); + + private: + std::vector*> mObservers; /// Indexed by observer identifier. + std::unordered_multimap mOnAdd; + std::unordered_multimap mOnRemove; + }; +} // namespace cubos::core::ecs diff --git a/core/src/ecs/observer/id.cpp b/core/src/ecs/observer/id.cpp new file mode 100644 index 000000000..41d519783 --- /dev/null +++ b/core/src/ecs/observer/id.cpp @@ -0,0 +1 @@ +#include diff --git a/core/src/ecs/observer/observers.cpp b/core/src/ecs/observer/observers.cpp new file mode 100644 index 000000000..5a94825e6 --- /dev/null +++ b/core/src/ecs/observer/observers.cpp @@ -0,0 +1,71 @@ +#include + +using cubos::core::ecs::ObserverId; +using cubos::core::ecs::Observers; + +Observers::~Observers() +{ + for (auto* observer : mObservers) + { + delete observer; + } +} + +bool Observers::notifyAdd(CommandBuffer& commandBuffer, Entity entity, ColumnId column) +{ + bool triggered = false; + + auto range = mOnAdd.equal_range(column); + for (auto it = range.first; it != range.second; ++it) + { + auto* observer = mObservers[it->second.inner]; + if (observer != nullptr) + { + observer->run({commandBuffer, entity}); + triggered = true; + } + } + + return triggered; +} + +bool Observers::notifyRemove(CommandBuffer& commandBuffer, Entity entity, ColumnId column) +{ + bool triggered = false; + + auto range = mOnRemove.equal_range(column); + for (auto it = range.first; it != range.second; ++it) + { + auto* observer = mObservers[it->second.inner]; + if (observer != nullptr) + { + observer->run({commandBuffer, entity}); + triggered = true; + } + } + + return triggered; +} + +ObserverId Observers::hookOnAdd(ColumnId column, System observer) +{ + ObserverId id{.inner = mObservers.size()}; + mObservers.push_back(new System(std::move(observer))); + mOnAdd.emplace(column, id); + return id; +} + +ObserverId Observers::hookOnRemove(ColumnId column, System observer) +{ + ObserverId id{.inner = mObservers.size()}; + mObservers.push_back(new System(std::move(observer))); + mOnRemove.emplace(column, id); + return id; +} + +void Observers::unhook(ObserverId id) +{ + CUBOS_ASSERT(mObservers[id.inner] != nullptr); + delete mObservers[id.inner]; + mObservers[id.inner] = nullptr; +} diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index 92a41d0ee..40fce3516 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -60,6 +60,7 @@ add_executable( ecs/query/term.cpp ecs/query/filter.cpp ecs/system/access.cpp + ecs/observer/observers.cpp ecs/stress.cpp geom/box.cpp diff --git a/core/tests/ecs/observer/observers.cpp b/core/tests/ecs/observer/observers.cpp new file mode 100644 index 000000000..0f8ec15eb --- /dev/null +++ b/core/tests/ecs/observer/observers.cpp @@ -0,0 +1,36 @@ +#include + +#include +#include + +#include "../utils.hpp" + +using namespace cubos::core::ecs; + +TEST_CASE("ecs::Observers") +{ + World world{}; + CommandBuffer cmdBuffer{world}; + Observers obs{}; + + static int acc = 0; + auto hook1 = obs.hookOnAdd(ColumnId{.inner = 1}, System::make(world, []() { acc += 1; }, {})); + auto hook2 = obs.hookOnRemove(ColumnId{.inner = 1}, System::make(world, []() { acc -= 1; }, {})); + 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.notifyAdd(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1}); + CHECK(acc == 1); + obs.notifyRemove(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1}); + CHECK(acc == 0); + obs.unhook(hook2); + obs.notifyAdd(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1}); + CHECK(acc == 1); + obs.notifyRemove(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1}); + CHECK(acc == 1); + obs.unhook(hook1); + obs.notifyAdd(cmdBuffer, Entity{0, 0}, ColumnId{.inner = 1}); + CHECK(acc == 1); +} \ No newline at end of file