What are the ways to implement many-to-many relations? #1280
-
|
I started trying EnTT and quickly got stuck on this question. The problems:
Examples:
The problem may be that I am trying to address ECS as a relational database, which is evidently wrong. Should I reimagine the application architecture or use another ECS paradigm? Below, I share the potential solutions I found. Relations as ComponentsTo my understanding, the most efficient and idiomatically correct way to represent relations is through components. Nonetheless, I am unsure how to use this method correctly for relations generated at runtime, so I need help with the questions below. StorageThere are multiple discussions concerning this matter, and this approach seems to be the most promising:
After reading ECS back and forth, Wiki on Is it okay to make a Component with #include <entt/entt.hpp>
#include <iostream>
#include <string>
using entt::operator ""_hs;
template<typename Component>
struct Multiple
{
entt::storage<Component>& storage;
};
struct Friend
{
};
// Create specific entities and their relations
void create(entt::registry& registry)
{
auto&& pc = registry.create();
auto&& person = registry.create();
auto&& cat = registry.create();
registry.emplace<std::string>(pc, "PC");
registry.emplace<std::string>(person, "Person");
registry.emplace<std::string>(cat, "Cat");
auto&& personFriendship = registry.emplace< Multiple<Friend> >(person, registry.storage<Friend>("Person friends"_hs));
personFriendship.storage.emplace(pc);
personFriendship.storage.emplace(cat);
auto&& pcFriendship = registry.emplace< Multiple<Friend> >(pc, registry.storage<Friend>("PC friends"_hs));
pcFriendship.storage.emplace(person);
auto&& catFriendship = registry.emplace< Multiple<Friend> >(cat, registry.storage<Friend>("Cat friends"_hs));
catFriendship.storage.emplace(cat); // what did you expect?
}
int main()
{
entt::registry registry;
create(registry);
registry.view< Multiple<Friend> >().each([&](auto&& entity, auto&& friends) {
entt::basic_view view{friends.storage};
view.each([&](auto&& friendlyEntity, auto&& relation) {
std::cout << registry.get<std::string>(entity) << " befriends " << registry.get<std::string>(friendlyEntity) << '\n';
});
});
return 0;
}Pros:
Cons:
TagsSometimes, compile-time tags can remove some For example, if Pros:
Cons:
Relations as EntitiesSQL-like approach, where relations are modelled as joining links. However, we can find elements efficiently by indexed fields in SQL, which does not seem to be the case in generic EnTT. Or am I missing something? Also, as I understand, you should implement a listener for
https://ajmmertens.medium.com/a-roadmap-to-entity-relationships-5b1d11ebb4eb#5d41 Linked listThe idea is described in https://skypjack.github.io/2019-06-25-ecs-baf-part-4. The general idea is to store the pointers to the other relations in the relation. struct R
{
entt::entity a;
entt::entity b;
// previous relation in the relation list of a
entt::entity previous_a{entt::null};
// previous relation in the relation list of b
entt::entity previous_b{entt::null};
// next relation in the relation list of a
entt::entity next_a{entt::null};
// next relation in relation list of b
entt::entity next_a{entt::null};
}Also, store a pointer to the last relation in the entities. struct HasR
{
entt::entity relation{entt::null};
}Thus, by multiplying twice the storage required by relations, we achieve a zero overhead in terms of the If we want to find out whether Mark and PropagateA mix of ideas from Part 4, insights - Hierarchies and beyond and #1062 for fetching chains of entities through relations. Implement simple relations: struct R
{
entt::entity a;
entt::entity b;
};
Becomes less and less efficient if there are more relations than dirty entities. Sort and Binary SearchImplement simple relations: struct R
{
entt::entity a;
entt::entity b;
};Use the tutorial, sections Sorting: is it possible? and Iterators:
Pros:
Cons:
Is there an efficient ( Brute ForceImplement simple relations: struct R
{
entt::entity a;
entt::entity b;
};Naïve solution. Just for prototyping: void f(entt::entity a)
{
registry.view<R>.each([&](auto r)
{
if (r.a == a) process(a, r);
});
}Reverse Brute ForceDiscussed in #1062. Implement simple relations: struct R
{
entt::entity a;
entt::entity b;
};If you use ECS, you probably want to process multiple entities with shared traits. registry.view<R>.each([&](auto r)
{
if (predicate(r.a)) process(r.a, r);
if (predicate(r.b)) process(r.b, r);
}); |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 12 replies
-
|
Yet another option, which is the closest one to the good old OOP design, is to enable pointer stability for the relation type. |
Beta Was this translation helpful? Give feedback.
I was talking about enabling pointer stability for certain components in EnTT (it's a feature of the ECS layer).
You can have something like this at the end of the day:
And with the right destructor(s), it's easy to keep things in sync and cleanup them eventually.
In this way, an entity points to its companions and their components directly.