From 8fb2a94eef1cd5b55ad9ae901988dcd814698eaf Mon Sep 17 00:00:00 2001 From: Ricardo Antunes Date: Tue, 30 Jan 2024 10:18:47 +0000 Subject: [PATCH] feat(ecs): add relations to Blueprint --- core/include/cubos/core/ecs/blueprint.hpp | 64 ++++++++++++++++++---- core/src/cubos/core/ecs/blueprint.cpp | 58 ++++++++++++++++++-- core/src/cubos/core/ecs/command_buffer.cpp | 5 +- core/tests/ecs/blueprint.cpp | 33 ++++++++++- 4 files changed, 143 insertions(+), 17 deletions(-) diff --git a/core/include/cubos/core/ecs/blueprint.hpp b/core/include/cubos/core/ecs/blueprint.hpp index a9182bc00..880e29617 100644 --- a/core/include/cubos/core/ecs/blueprint.hpp +++ b/core/include/cubos/core/ecs/blueprint.hpp @@ -15,12 +15,12 @@ namespace cubos::core::ecs { - /// @brief Collection of entities and their respective components. + /// @brief Collection of entities and their respective components and relations. /// /// Blueprints are in a way the 'Prefab' of @b CUBOS. They act as a tiny @ref World which can /// then be spawned into an actual @ref World, as many times as needed. /// - /// When a blueprint is spawned, all of its components are scanned using the @ref + /// When a blueprint is spawned, all of its components and relations are scanned using the @ref /// core-reflection system for any references to other entities in the blueprint. These /// references are then replaced with the actual spawned entities. This has the side effect /// that if you do not expose an @ref Entity field to the @ref core-reflection system, it will @@ -42,6 +42,13 @@ namespace cubos::core::ecs /// @param component Component. using Add = void (*)(void* userData, Entity entity, memory::AnyValue component); + /// @brief Function used by @ref instantiate to add relations to entities. + /// @param userData User data passed into @ref instantiate. + /// @param fromEntity From entity. + /// @param toEntity To entity. + /// @param relation Relation. + using Relate = void (*)(void* userData, Entity fromEntity, Entity toEntity, memory::AnyValue relation); + /// @brief Constructs. Blueprint(); @@ -75,6 +82,23 @@ namespace cubos::core::ecs ...); } + /// @brief Adds a relation between two entities. Overwrites the existing relation, if there's any. + /// @param fromEntity From entity. + /// @param toEntity To entity. + /// @param relation Relation to move. + void relate(Entity fromEntity, Entity toEntity, memory::AnyValue relation); + + /// @brief Adds a relation between two entities. Overwrites the existing relation, if there's any. + /// @tparam T Relation type. + /// @param fromEntity From entity. + /// @param toEntity To entity. + /// @param relation Relation to move. + template + void relate(Entity fromEntity, Entity toEntity, T relation) + { + this->relate(fromEntity, toEntity, memory::AnyValue::moveConstruct(reflection::reflect(), &relation)); + } + /// @brief Merges another blueprint into this one. /// /// Entities in the other blueprint will have their names prefixed with the specified @@ -95,30 +119,44 @@ namespace cubos::core::ecs /// @param userData User data to pass into the functions. /// @param create Function used to create entities. /// @param add Function used to add components to entities. - void instantiate(void* userData, Create create, Add add) const; + /// @param relate Function used to add relations to entities. + void instantiate(void* userData, Create create, Add add, Relate relate) const; /// @brief Instantiates the blueprint by calling the given functors. /// @tparam C Create functor type. /// @tparam A Add functor type. + /// @tparam R Relate functor type. /// @param create Functor used to create entities. /// @param add Functor used to add components to entities. - template - void instantiate(C create, A add) const + /// @param relate Functor used to add relations to entities. + template + void instantiate(C create, A add, R relate) const { - auto functors = std::make_pair(create, add); + struct Functors + { + C create; + A add; + R relate; + }; + + Functors functors{create, add, relate}; Create createFunc = [](void* userData, std::string name) -> Entity { - return static_cast(userData)->first(name); + return static_cast(userData)->create(name); }; Add addFunc = [](void* userData, Entity entity, memory::AnyValue component) { - return static_cast(userData)->second(entity, std::move(component)); + return static_cast(userData)->add(entity, std::move(component)); + }; + + Relate relateFunc = [](void* userData, Entity fromEntity, Entity toEntity, memory::AnyValue relation) { + return static_cast(userData)->relate(fromEntity, toEntity, std::move(relation)); }; // We pass the functors pair using the userData argument. We could use std::function // here and pass them directly, but that would mean unnecessary heap allocations and an // extra large include on the header. - this->instantiate(&functors, createFunc, addFunc); + this->instantiate(&functors, createFunc, addFunc, relateFunc); } /// @brief Checks if the given name is a valid entity name. @@ -129,10 +167,16 @@ namespace cubos::core::ecs static bool validEntityName(const std::string& name); private: + template + using EntityMap = std::unordered_map; + /// @brief Maps entities to their names. memory::UnorderedBimap mBimap; /// @brief Maps component types to maps of entities to the component values. - memory::TypeMap> mComponents; + memory::TypeMap> mComponents; + + /// @brief Maps component types to maps of entities to maps of entities to the relation values. + memory::TypeMap>> mRelations; }; } // namespace cubos::core::ecs diff --git a/core/src/cubos/core/ecs/blueprint.cpp b/core/src/cubos/core/ecs/blueprint.cpp index acd3e8746..d26421ba7 100644 --- a/core/src/cubos/core/ecs/blueprint.cpp +++ b/core/src/cubos/core/ecs/blueprint.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -47,6 +48,36 @@ void Blueprint::add(Entity entity, AnyValue component) mComponents.at(component.type()).emplace(entity, std::move(component)); } +void Blueprint::relate(Entity fromEntity, Entity toEntity, AnyValue relation) +{ + CUBOS_ASSERT(relation.type().get().hasCopyConstruct(), + "Blueprint relations must be copy constructible, but '{}' isn't", relation.type().name()); + + // Sanity check to catch errors where the user passes an entity which doesn't belong to this blueprint. + // We can't make sure it never happens, but we might as well catch some of the possible cases. + CUBOS_ASSERT(mBimap.containsLeft(fromEntity), "Entity wasn't created with this blueprint"); + CUBOS_ASSERT(mBimap.containsLeft(toEntity), "Entity wasn't created with this blueprint"); + + if (relation.type().has() && fromEntity.index > toEntity.index) + { + // If the relation is symmetric, we always store the pair with the first index lower than the second. + std::swap(fromEntity, toEntity); + } + + if (!mRelations.contains(relation.type())) + { + mRelations.insert(relation.type(), {}); + } + + if (relation.type().has()) + { + // If the relation is a tree relation, then we want to erase any previous outgoing relation from the entity. + mRelations.at(relation.type()).erase(fromEntity); + } + + mRelations.at(relation.type())[fromEntity].insert_or_assign(toEntity, std::move(relation)); +} + void Blueprint::merge(const std::string& prefix, const Blueprint& other) { other.instantiate( @@ -60,13 +91,17 @@ void Blueprint::merge(const std::string& prefix, const Blueprint& other) mBimap.insert(entity, std::move(name)); return entity; }, - [&](Entity entity, AnyValue component) { this->add(entity, std::move(component)); }); + [&](Entity entity, AnyValue component) { this->add(entity, std::move(component)); }, + [&](Entity fromEntity, Entity toEntity, AnyValue relation) { + this->relate(fromEntity, toEntity, std::move(relation)); + }); } void Blueprint::clear() { mBimap.clear(); mComponents.clear(); + mRelations.clear(); } const UnorderedBimap& Blueprint::bimap() const @@ -84,9 +119,8 @@ static void convertToInstancedEntities(const std::unordered_map(value); if (!entity.isNull()) { - CUBOS_ASSERT( - toInstanced.contains(entity), - "Entities stored in components must either be null or reference valid entities on their blueprints"); + CUBOS_ASSERT(toInstanced.contains(entity), "Entities stored in components/relations must either be null or " + "reference valid entities on their blueprints"); entity = toInstanced.at(entity); } } @@ -119,7 +153,7 @@ static void convertToInstancedEntities(const std::unordered_map thisToInstance{}; @@ -141,6 +175,20 @@ void Blueprint::instantiate(void* userData, Create create, Add add) const add(userData, thisToInstance.at(entity), std::move(copied)); } } + + // Do the same but for relations. + for (const auto& [type, relations] : mRelations) + { + for (const auto& [fromEntity, outgoing] : relations) + { + for (const auto& [toEntity, relation] : outgoing) + { + auto copied = AnyValue::copyConstruct(relation.type(), relation.get()); + convertToInstancedEntities(thisToInstance, copied.type(), copied.get()); + relate(userData, thisToInstance.at(fromEntity), thisToInstance.at(toEntity), std::move(copied)); + } + } + } } bool Blueprint::validEntityName(const std::string& name) diff --git a/core/src/cubos/core/ecs/command_buffer.cpp b/core/src/cubos/core/ecs/command_buffer.cpp index 20882408a..924ebc9ab 100644 --- a/core/src/cubos/core/ecs/command_buffer.cpp +++ b/core/src/cubos/core/ecs/command_buffer.cpp @@ -38,7 +38,10 @@ std::unordered_map CommandBuffer::spawn(const Blueprint& bl nameToEntity.emplace(name, entity); return entity; }, - [&](Entity entity, memory::AnyValue component) { this->add(entity, component.type(), component.get()); }); + [&](Entity entity, memory::AnyValue component) { this->add(entity, component.type(), component.get()); }, + [&](Entity fromEntity, Entity toEntity, memory::AnyValue relation) { + this->relate(fromEntity, toEntity, relation.type(), relation.get()); + }); return nameToEntity; } diff --git a/core/tests/ecs/blueprint.cpp b/core/tests/ecs/blueprint.cpp index 02714f8da..e84d5d500 100644 --- a/core/tests/ecs/blueprint.cpp +++ b/core/tests/ecs/blueprint.cpp @@ -44,23 +44,34 @@ TEST_CASE("ecs::Blueprint") // return a null identifier. CHECK_FALSE(blueprint.bimap().containsRight("foo")); - // Create two entities on the blueprint. + // Create three entities on the blueprint. auto bar = blueprint.create("bar"); auto baz = blueprint.create("baz"); + auto qux = blueprint.create("qux"); blueprint.add(baz, IntegerComponent{2}); blueprint.add(baz, ParentComponent{bar}); + // Add some relations between the entities. + blueprint.relate(bar, baz, EmptyRelation{}); + blueprint.relate(baz, bar, EmptyRelation{}); + blueprint.relate(bar, baz, SymmetricRelation{.value = 1}); + blueprint.relate(baz, bar, SymmetricRelation{.value = 2}); // Should overwrite the relation above. + blueprint.relate(bar, baz, TreeRelation{.value = 1}); + blueprint.relate(bar, qux, TreeRelation{.value = 2}); // Should overwrite the relation above. + SUBCASE("spawn the blueprint") { // Spawn the blueprint into the world and get the identifiers of the spawned entities. auto spawned = cmds.spawn(blueprint); auto spawnedBar = spawned.entity("bar"); auto spawnedBaz = spawned.entity("baz"); + auto spawnedQux = spawned.entity("qux"); cmdBuffer.commit(); // Check if the spawned entities have the right components. auto barComponents = world.components(spawnedBar); auto bazComponents = world.components(spawnedBaz); + auto quxComponents = world.components(spawnedQux); // "bar" has no components. CHECK(barComponents.begin() == barComponents.end()); @@ -70,6 +81,22 @@ TEST_CASE("ecs::Blueprint") REQUIRE(bazComponents.has()); CHECK(bazComponents.get().id == spawnedBar); CHECK(bazComponents.get().value == 2); + + // "qux" has no components. + CHECK(quxComponents.begin() == quxComponents.end()); + + // EmptyRelation's were added correctly. + CHECK(world.related(spawnedBar, spawnedBaz)); + CHECK(world.related(spawnedBaz, spawnedBar)); + + // The symmetric relation was added correctly. + REQUIRE(world.related(spawnedBar, spawnedBaz)); + CHECK(world.relation(spawnedBar, spawnedBaz).value == 2); + + // The tree relation was added correctly. + CHECK_FALSE(world.related(spawnedBar, spawnedBaz)); + REQUIRE(world.related(spawnedBar, spawnedQux)); + CHECK(world.relation(spawnedBar, spawnedQux).value == 2); } SUBCASE("merge one blueprint into another blueprint and then spawn it") @@ -78,6 +105,7 @@ TEST_CASE("ecs::Blueprint") Blueprint merged{}; auto foo = merged.create("foo"); merged.add(foo, IntegerComponent{1}); + merged.relate(foo, foo, EmptyRelation{}); // Merge the original blueprint into the new one. merged.merge("sub", blueprint); @@ -103,6 +131,9 @@ TEST_CASE("ecs::Blueprint") REQUIRE(fooComponents.has()); CHECK(fooComponents.get().value == 1); + // "foo" is related with "foo" by EmptyRelation + CHECK(world.related(spawnedFoo, spawnedFoo)); + // "bar" has no components. CHECK(barComponents.begin() == barComponents.end());