From 663e864727cd9aa58563780dc5eedc67f876d0e5 Mon Sep 17 00:00:00 2001 From: Ricardo Antunes Date: Sun, 1 Oct 2023 16:10:22 +0100 Subject: [PATCH] refactor(ecs): use EntityHash instead of std::hash --- core/CMakeLists.txt | 1 + .../cubos/core/data/old/serialization_map.hpp | 10 ++++--- core/include/cubos/core/ecs/blueprint.hpp | 7 +++-- core/include/cubos/core/ecs/commands.hpp | 22 +++++++------- core/include/cubos/core/ecs/entity/entity.hpp | 17 +---------- core/include/cubos/core/ecs/entity/hash.hpp | 22 ++++++++++++++ .../include/cubos/core/ecs/entity/manager.hpp | 1 + core/src/cubos/core/ecs/blueprint.cpp | 2 +- core/src/cubos/core/ecs/commands.cpp | 5 ++-- core/src/cubos/core/ecs/entity/entity.cpp | 10 ++++--- core/src/cubos/core/ecs/entity/hash.cpp | 20 +++++++++++++ .../collisions/broad_phase_collisions.hpp | 20 ++++++------- .../collisions/broad_phase_collisions.cpp | 9 ++---- engine/src/cubos/engine/scene/bridge.cpp | 29 +++++++++++-------- 14 files changed, 107 insertions(+), 68 deletions(-) create mode 100644 core/include/cubos/core/ecs/entity/hash.hpp create mode 100644 core/src/cubos/core/ecs/entity/hash.cpp diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index ece6d4fba..c8585a370 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -71,6 +71,7 @@ set(CUBOS_CORE_SOURCE "src/cubos/core/al/oal_audio_device.hpp" "src/cubos/core/ecs/entity/entity.cpp" + "src/cubos/core/ecs/entity/hash.cpp" "src/cubos/core/ecs/entity/manager.cpp" "src/cubos/core/ecs/component_manager.cpp" "src/cubos/core/ecs/commands.cpp" diff --git a/core/include/cubos/core/data/old/serialization_map.hpp b/core/include/cubos/core/data/old/serialization_map.hpp index 80f2f5df0..4eb0e3c3f 100644 --- a/core/include/cubos/core/data/old/serialization_map.hpp +++ b/core/include/cubos/core/data/old/serialization_map.hpp @@ -10,7 +10,9 @@ namespace cubos::core::data::old /// Class used to map between references and their serialized identifiers. /// @tparam R Reference type. /// @tparam I Serialized identifier type. - template + /// @tparam RH Reference hash type. + /// @tparam IH Identifier hash type. + template , typename IH = std::hash> class SerializationMap final { public: @@ -121,7 +123,7 @@ namespace cubos::core::data::old /// @brief Returns the internal map that maps references to IDs /// @return Map of references and Ids. - inline std::unordered_map getMap() const + inline std::unordered_map getMap() const { return mRefToId; } @@ -130,7 +132,7 @@ namespace cubos::core::data::old bool mUsingFunctions; ///< True if the map is using functions instead of keeping a map. std::function mSerialize; ///< Function used to serialize references. std::function mDeserialize; ///< Function used to deserialize references. - std::unordered_map mRefToId; ///< Map of references to serialized IDs. - std::unordered_map mIdToRef; ///< Map of serialized IDs to references. + std::unordered_map mRefToId; ///< Map of references to serialized IDs. + std::unordered_map mIdToRef; ///< Map of serialized IDs to references. }; } // namespace cubos::core::data::old diff --git a/core/include/cubos/core/ecs/blueprint.hpp b/core/include/cubos/core/ecs/blueprint.hpp index 8edf8e52a..760a646c5 100644 --- a/core/include/cubos/core/ecs/blueprint.hpp +++ b/core/include/cubos/core/ecs/blueprint.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -64,7 +65,7 @@ namespace cubos::core::ecs /// @brief Returns the internal map that maps entities to their names /// @return Map of entities and names. - inline std::unordered_map getMap() const + inline std::unordered_map getMap() const { return mMap.getMap(); } @@ -115,7 +116,7 @@ namespace cubos::core::ecs auto des = data::old::BinaryDeserializer(this->stream); des.context().pushSubContext(context); - auto& map = des.context().get>(); + auto& map = des.context().get>(); for (const auto& name : this->names) { ComponentType type; @@ -165,7 +166,7 @@ namespace cubos::core::ecs }; /// @brief Stores the entity handles and the associated names. - data::old::SerializationMap mMap; + data::old::SerializationMap mMap; /// @brief Buffers which store the serialized components of each type in this blueprint. memory::TypeMap mBuffers; diff --git a/core/include/cubos/core/ecs/commands.hpp b/core/include/cubos/core/ecs/commands.hpp index c04de3a03..5f1641a6b 100644 --- a/core/include/cubos/core/ecs/commands.hpp +++ b/core/include/cubos/core/ecs/commands.hpp @@ -8,6 +8,7 @@ #include #include +#include #include namespace cubos::core::ecs @@ -91,13 +92,14 @@ namespace cubos::core::ecs private: friend CommandBuffer; - data::old::SerializationMap mMap; ///< Maps entity names to the instantiated entities. - CommandBuffer& mCommands; ///< Commands object that created this entity. + data::old::SerializationMap + mMap; ///< Maps entity names to the instantiated entities. + CommandBuffer& mCommands; ///< Commands object that created this entity. /// @brief Constructs. /// @param map Map of entity names to the instantiated entities. /// @param commands Commands object that created this entity. - BlueprintBuilder(data::old::SerializationMap&& map, CommandBuffer& commands); + BlueprintBuilder(data::old::SerializationMap&& map, CommandBuffer& commands); }; /// @brief Used to write ECS commands and execute them at a later time. @@ -225,7 +227,7 @@ namespace cubos::core::ecs void clear() override; void move(Entity entity, ComponentManager& manager) override; - std::unordered_map components; ///< Components in the buffer. + std::unordered_map components; ///< Components in the buffer. }; /// @brief Clears the commands. @@ -234,12 +236,12 @@ namespace cubos::core::ecs std::mutex mMutex; ///< Make this thread-safe. World& mWorld; ///< World to which the commands will be applied. - std::unordered_map mBuffers; ///< Component buffers per component type. - std::unordered_set mCreated; ///< Uncommitted created entities. - std::unordered_set mDestroyed; ///< Uncommitted destroyed entities. - std::unordered_map mAdded; ///< Mask of the uncommitted added components. - std::unordered_map mRemoved; ///< Mask of the uncommitted removed components. - std::unordered_set mChanged; ///< Entities whose mask has changed. + std::unordered_map mBuffers; ///< Component buffers per component type. + std::unordered_set mCreated; ///< Uncommitted created entities. + std::unordered_set mDestroyed; ///< Uncommitted destroyed entities. + std::unordered_map mAdded; ///< Mask of the uncommitted added components. + std::unordered_map mRemoved; ///< Mask of the uncommitted removed components. + std::unordered_set mChanged; ///< Entities whose mask has changed. }; // Implementation. diff --git a/core/include/cubos/core/ecs/entity/entity.hpp b/core/include/cubos/core/ecs/entity/entity.hpp index 995a9c693..f27871f03 100644 --- a/core/include/cubos/core/ecs/entity/entity.hpp +++ b/core/include/cubos/core/ecs/entity/entity.hpp @@ -6,14 +6,13 @@ #include #include -#include namespace cubos::core::ecs { /// @brief Identifies an entity. /// /// When serializing/deserializing, if there's a - /// data::old::SerializationMap in the context, it will be used to + /// data::old::SerializationMap in the context, it will be used to /// (de)serialize strings representing the entities. Otherwise, the identifiers will be /// (de)serialized as objects with two fields: their index and their generation. /// @@ -65,17 +64,3 @@ namespace cubos::core::ecs uint32_t generation; }; } // namespace cubos::core::ecs - -namespace std -{ - // Add hash function for Entity, so that it can be used as a key in an unordered_map. - - template <> - struct hash - { - inline std::size_t operator()(const cubos::core::ecs::Entity& k) const - { - return hash()(k.index); - } - }; -} // namespace std diff --git a/core/include/cubos/core/ecs/entity/hash.hpp b/core/include/cubos/core/ecs/entity/hash.hpp new file mode 100644 index 000000000..f187380fb --- /dev/null +++ b/core/include/cubos/core/ecs/entity/hash.hpp @@ -0,0 +1,22 @@ +/// @file +/// @brief Struct @ref cubos::core::ecs::EntityHash. +/// @ingroup core-ecs + +#pragma once + +#include + +namespace cubos::core::ecs +{ + struct Entity; + + /// @brief Used to hash @ref Entity objects. + /// + /// Can be used to allow @ref Entity objects to be used as keys in an `std::unordered_map`. + /// + /// @ingroup core-ecs + struct EntityHash + { + std::size_t operator()(const Entity& entity) const noexcept; + }; +} // namespace cubos::core::ecs diff --git a/core/include/cubos/core/ecs/entity/manager.hpp b/core/include/cubos/core/ecs/entity/manager.hpp index 9110f3833..aa41fea93 100644 --- a/core/include/cubos/core/ecs/entity/manager.hpp +++ b/core/include/cubos/core/ecs/entity/manager.hpp @@ -10,6 +10,7 @@ #include #include +#include namespace cubos::core::ecs { diff --git a/core/src/cubos/core/ecs/blueprint.cpp b/core/src/cubos/core/ecs/blueprint.cpp index 71beba080..ba52253ec 100644 --- a/core/src/cubos/core/ecs/blueprint.cpp +++ b/core/src/cubos/core/ecs/blueprint.cpp @@ -30,7 +30,7 @@ Entity Blueprint::entity(const std::string& name) const void Blueprint::merge(const std::string& prefix, const Blueprint& other) { // First, merge the maps. - data::old::SerializationMap srcMap; + data::old::SerializationMap srcMap; for (uint32_t i = 0; i < static_cast(other.mMap.size()); ++i) { // Deserialize from the source buffer using the original entity names. diff --git a/core/src/cubos/core/ecs/commands.cpp b/core/src/cubos/core/ecs/commands.cpp index e326fbd4c..63db86aa2 100644 --- a/core/src/cubos/core/ecs/commands.cpp +++ b/core/src/cubos/core/ecs/commands.cpp @@ -15,7 +15,8 @@ Entity EntityBuilder::entity() const return mEntity; } -BlueprintBuilder::BlueprintBuilder(data::old::SerializationMap&& map, CommandBuffer& commands) +BlueprintBuilder::BlueprintBuilder(data::old::SerializationMap&& map, + CommandBuffer& commands) : mMap(std::move(map)) , mCommands(commands) { @@ -69,7 +70,7 @@ void CommandBuffer::destroy(Entity entity) BlueprintBuilder CommandBuffer::spawn(const Blueprint& blueprint) { - data::old::SerializationMap map; + data::old::SerializationMap map; for (uint32_t i = 0; i < static_cast(blueprint.mMap.size()); ++i) { map.add(this->create().entity(), blueprint.mMap.getId(Entity(i, 0))); diff --git a/core/src/cubos/core/ecs/entity/entity.cpp b/core/src/cubos/core/ecs/entity/entity.cpp index ffe449f29..c787c0bf0 100644 --- a/core/src/cubos/core/ecs/entity/entity.cpp +++ b/core/src/cubos/core/ecs/entity/entity.cpp @@ -2,13 +2,15 @@ #include #include #include +#include using cubos::core::ecs::Entity; +using cubos::core::ecs::EntityHash; template <> void cubos::core::data::old::serialize(Serializer& ser, const Entity& obj, const char* name) { - if (ser.context().has>()) + if (ser.context().has>()) { if (obj.isNull()) { @@ -16,7 +18,7 @@ void cubos::core::data::old::serialize(Serializer& ser, const Entity& ob return; } - auto& map = ser.context().get>(); + auto& map = ser.context().get>(); ser.write(map.getId(obj), name); } else @@ -31,7 +33,7 @@ void cubos::core::data::old::serialize(Serializer& ser, const Entity& ob template <> void cubos::core::data::old::deserialize(Deserializer& des, Entity& obj) { - if (des.context().has>()) + if (des.context().has>()) { std::string name; des.read(name); @@ -41,7 +43,7 @@ void cubos::core::data::old::deserialize(Deserializer& des, Entity& obj) return; } - auto& map = des.context().get>(); + auto& map = des.context().get>(); if (map.hasId(name)) { obj = map.getRef(name); diff --git a/core/src/cubos/core/ecs/entity/hash.cpp b/core/src/cubos/core/ecs/entity/hash.cpp new file mode 100644 index 000000000..0d2905f8c --- /dev/null +++ b/core/src/cubos/core/ecs/entity/hash.cpp @@ -0,0 +1,20 @@ +#include +#include + +using cubos::core::ecs::EntityHash; + +std::size_t EntityHash::operator()(const Entity& entity) const noexcept +{ + // If std::size_t is 64 bits or more, we can fit both the index and generation in it. + // Otherwise, we'll just use the index as the hash, which means we'll always get collisions + // for entities with the same index but different generations. That shouldn't be a problem + // though, since that use case is very unlikely. + + if constexpr (sizeof(std::size_t) < sizeof(uint32_t) * 2) + { + return static_cast(entity.index); + } + + return static_cast(entity.index) | + (static_cast(entity.generation) << (sizeof(uint32_t) * 8)); +} diff --git a/engine/include/cubos/engine/collisions/broad_phase_collisions.hpp b/engine/include/cubos/engine/collisions/broad_phase_collisions.hpp index e819310f3..85c7e2c2c 100644 --- a/engine/include/cubos/engine/collisions/broad_phase_collisions.hpp +++ b/engine/include/cubos/engine/collisions/broad_phase_collisions.hpp @@ -8,10 +8,9 @@ #include #include +#include #include -using cubos::core::ecs::Entity; - namespace cubos::engine { /// @brief Resource which stores data used in broad phase collision detection. @@ -19,14 +18,14 @@ namespace cubos::engine struct BroadPhaseCollisions { /// @brief Pair of entities that may collide. - using Candidate = std::pair; + using Candidate = std::pair; /// @brief Hash function to allow Candidates to be used as keys in an unordered_set. struct CandidateHash { std::size_t operator()(const Candidate& candidate) const { - return std::hash()(candidate.first) ^ std::hash()(candidate.second); + return core::ecs::EntityHash()(candidate.first) ^ core::ecs::EntityHash()(candidate.second); } }; @@ -50,8 +49,8 @@ namespace cubos::engine /// @brief Marker used for sweep and prune. struct SweepMarker { - Entity entity; ///< Entity referenced by the marker. - bool isMin; ///< Whether the marker is a min or max marker. + core::ecs::Entity entity; ///< Entity referenced by the marker. + bool isMin; ///< Whether the marker is a min or max marker. }; /// @brief List of ordered sweep markers for each axis. Stores the index of the marker in mMarkers. @@ -61,10 +60,11 @@ namespace cubos::engine /// /// For each each map, the key is an entity and the value is a list of entities that /// overlap with the key. Symmetrical pairs are not stored. - std::unordered_map> sweepOverlapMaps[3]; + std::unordered_map, core::ecs::EntityHash> + sweepOverlapMaps[3]; /// @brief Set of active entities during sweep for each axis. - std::unordered_set activePerAxis[3]; + std::unordered_set activePerAxis[3]; /// @brief Sets of collision candidates for each collision type. The index of the array is /// the collision type. @@ -72,11 +72,11 @@ namespace cubos::engine /// @brief Adds an entity to the list of entities tracked by sweep and prune. /// @param entity Entity to add. - void addEntity(Entity entity); + void addEntity(core::ecs::Entity entity); /// @brief Removes an entity from the list of entities tracked by sweep and prune. /// @param entity Entity to remove. - void removeEntity(Entity entity); + void removeEntity(core::ecs::Entity entity); /// @brief Clears the list of entities tracked by sweep and prune. void clearEntities(); diff --git a/engine/src/cubos/engine/collisions/broad_phase_collisions.cpp b/engine/src/cubos/engine/collisions/broad_phase_collisions.cpp index c7597de53..0c960253a 100644 --- a/engine/src/cubos/engine/collisions/broad_phase_collisions.cpp +++ b/engine/src/cubos/engine/collisions/broad_phase_collisions.cpp @@ -2,12 +2,9 @@ #include -using cubos::engine::BroadPhaseCollisions; +using cubos::core::ecs::Entity; -using Candidate = BroadPhaseCollisions::Candidate; -using CandidateHash = BroadPhaseCollisions::CandidateHash; -using CollisionType = BroadPhaseCollisions::CollisionType; -using SweepMarker = BroadPhaseCollisions::SweepMarker; +using cubos::engine::BroadPhaseCollisions; void BroadPhaseCollisions::addEntity(Entity entity) { @@ -41,7 +38,7 @@ void BroadPhaseCollisions::addCandidate(CollisionType type, Candidate candidate) candidatesPerType[static_cast(type)].insert(candidate); } -const std::unordered_set& BroadPhaseCollisions::candidates(CollisionType type) const +auto BroadPhaseCollisions::candidates(CollisionType type) const -> const std::unordered_set& { return candidatesPerType[static_cast(type)]; } diff --git a/engine/src/cubos/engine/scene/bridge.cpp b/engine/src/cubos/engine/scene/bridge.cpp index 7910a79b8..d245b3efa 100644 --- a/engine/src/cubos/engine/scene/bridge.cpp +++ b/engine/src/cubos/engine/scene/bridge.cpp @@ -1,21 +1,25 @@ #include #include +#include #include #include #include -using namespace cubos::engine; -using namespace cubos::core; - +using cubos::core::data::File; +using cubos::core::data::FileSystem; +using cubos::core::data::old::JSONDeserializer; using cubos::core::data::old::SerializationMap; using cubos::core::ecs::Entity; +using cubos::core::ecs::EntityHash; + +using namespace cubos::engine; bool SceneBridge::load(Assets& assets, const AnyAsset& handle) { // Open the scene file. auto path = assets.readMeta(handle)->get("path").value(); - auto stream = data::FileSystem::open(path, data::File::OpenMode::Read); + auto stream = FileSystem::open(path, File::OpenMode::Read); if (stream == nullptr) { CUBOS_ERROR("Could not open scene file '{}'", path); @@ -28,7 +32,7 @@ bool SceneBridge::load(Assets& assets, const AnyAsset& handle) stream.reset(); // Close the file. // Deserialize the scene file. - auto deserializer = data::old::JSONDeserializer(contents); + auto deserializer = JSONDeserializer(contents); if (deserializer.failed()) { CUBOS_ERROR("Could not parse scene file '{}' as JSON", path); @@ -38,13 +42,14 @@ bool SceneBridge::load(Assets& assets, const AnyAsset& handle) auto scene = Scene(); // Add a SerializationMap for entity handle deserialization. - deserializer.context().push(SerializationMap{[&](const Entity&, std::string&) { - return false; // Serialization not needed. - }, - [&](Entity& entity, const std::string& string) { - entity = scene.blueprint.entity(string); - return !entity.isNull(); - }}); + deserializer.context().push( + SerializationMap{[&](const Entity&, std::string&) { + return false; // Serialization not needed. + }, + [&](Entity& entity, const std::string& string) { + entity = scene.blueprint.entity(string); + return !entity.isNull(); + }}); deserializer.beginObject();