Skip to content

Commit

Permalink
feat(ecs): add relations to Blueprint
Browse files Browse the repository at this point in the history
  • Loading branch information
RiscadoA committed Jan 30, 2024
1 parent 09aab9e commit 8fb2a94
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 17 deletions.
64 changes: 54 additions & 10 deletions core/include/cubos/core/ecs/blueprint.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();

Expand Down Expand Up @@ -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 <reflection::Reflectable T>
void relate(Entity fromEntity, Entity toEntity, T relation)
{
this->relate(fromEntity, toEntity, memory::AnyValue::moveConstruct(reflection::reflect<T>(), &relation));
}

/// @brief Merges another blueprint into this one.
///
/// Entities in the other blueprint will have their names prefixed with the specified
Expand All @@ -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 <typename C, typename A>
void instantiate(C create, A add) const
/// @param relate Functor used to add relations to entities.
template <typename C, typename A, typename R>
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<decltype(functors)*>(userData)->first(name);
return static_cast<Functors*>(userData)->create(name);
};

Add addFunc = [](void* userData, Entity entity, memory::AnyValue component) {
return static_cast<decltype(functors)*>(userData)->second(entity, std::move(component));
return static_cast<Functors*>(userData)->add(entity, std::move(component));
};

Relate relateFunc = [](void* userData, Entity fromEntity, Entity toEntity, memory::AnyValue relation) {
return static_cast<Functors*>(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.
Expand All @@ -129,10 +167,16 @@ namespace cubos::core::ecs
static bool validEntityName(const std::string& name);

private:
template <typename T>
using EntityMap = std::unordered_map<Entity, T, EntityHash>;

/// @brief Maps entities to their names.
memory::UnorderedBimap<Entity, std::string, EntityHash> mBimap;

/// @brief Maps component types to maps of entities to the component values.
memory::TypeMap<std::unordered_map<Entity, memory::AnyValue, EntityHash>> mComponents;
memory::TypeMap<EntityMap<memory::AnyValue>> mComponents;

/// @brief Maps component types to maps of entities to maps of entities to the relation values.
memory::TypeMap<EntityMap<EntityMap<memory::AnyValue>>> mRelations;
};
} // namespace cubos::core::ecs
58 changes: 53 additions & 5 deletions core/src/cubos/core/ecs/blueprint.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <cubos/core/ecs/blueprint.hpp>
#include <cubos/core/ecs/reflection.hpp>
#include <cubos/core/log.hpp>
#include <cubos/core/reflection/external/string.hpp>
#include <cubos/core/reflection/traits/array.hpp>
Expand Down Expand Up @@ -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<ConstructibleTrait>().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<SymmetricTrait>() && 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<TreeTrait>())
{
// 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(
Expand All @@ -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<Entity, std::string, EntityHash>& Blueprint::bimap() const
Expand All @@ -84,9 +119,8 @@ static void convertToInstancedEntities(const std::unordered_map<Entity, Entity,
auto& entity = *static_cast<Entity*>(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);
}
}
Expand Down Expand Up @@ -119,7 +153,7 @@ static void convertToInstancedEntities(const std::unordered_map<Entity, Entity,
}
}

void Blueprint::instantiate(void* userData, Create create, Add add) const
void Blueprint::instantiate(void* userData, Create create, Add add, Relate relate) const
{
// Instantiate our entities and create a map from them to their instanced counterparts.
std::unordered_map<Entity, Entity, EntityHash> thisToInstance{};
Expand All @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion core/src/cubos/core/ecs/command_buffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ std::unordered_map<std::string, Entity> 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;
}
Expand Down
33 changes: 32 additions & 1 deletion core/tests/ecs/blueprint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -70,6 +81,22 @@ TEST_CASE("ecs::Blueprint")
REQUIRE(bazComponents.has<IntegerComponent>());
CHECK(bazComponents.get<ParentComponent>().id == spawnedBar);
CHECK(bazComponents.get<IntegerComponent>().value == 2);

// "qux" has no components.
CHECK(quxComponents.begin() == quxComponents.end());

// EmptyRelation's were added correctly.
CHECK(world.related<EmptyRelation>(spawnedBar, spawnedBaz));
CHECK(world.related<EmptyRelation>(spawnedBaz, spawnedBar));

// The symmetric relation was added correctly.
REQUIRE(world.related<SymmetricRelation>(spawnedBar, spawnedBaz));
CHECK(world.relation<SymmetricRelation>(spawnedBar, spawnedBaz).value == 2);

// The tree relation was added correctly.
CHECK_FALSE(world.related<TreeRelation>(spawnedBar, spawnedBaz));
REQUIRE(world.related<TreeRelation>(spawnedBar, spawnedQux));
CHECK(world.relation<TreeRelation>(spawnedBar, spawnedQux).value == 2);
}

SUBCASE("merge one blueprint into another blueprint and then spawn it")
Expand All @@ -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);
Expand All @@ -103,6 +131,9 @@ TEST_CASE("ecs::Blueprint")
REQUIRE(fooComponents.has<IntegerComponent>());
CHECK(fooComponents.get<IntegerComponent>().value == 1);

// "foo" is related with "foo" by EmptyRelation
CHECK(world.related<EmptyRelation>(spawnedFoo, spawnedFoo));

// "bar" has no components.
CHECK(barComponents.begin() == barComponents.end());

Expand Down

0 comments on commit 8fb2a94

Please sign in to comment.