Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iterating through components with common base class or via ducktype #995

Closed
nhammond129 opened this issue Mar 15, 2023 · 3 comments
Closed
Assignees
Labels
question open question

Comments

@nhammond129
Copy link

Currently I'm using some wrapper components that hold another component "as" their base type, since I can't use views templating on the base class (e.g. registry.view<BaseClass>().each([](auto ent, auto& component){...}); (#78))

Consider SFML's sf::Text and sf::Sprite:
Both inherit sf::Drawable, and I create

struct IDrawable {
    sf::Drawable& drawable;
};

and emplace it on construction of either sf::Sprite or sf::Text components. Now I can registry.view<IDrawable>().each(...); and I've iterated both text and sprite components
(See example: minimal.cpp.txt)

It... works. I guess. Feels like I'm overthinking it and turning it into something more complex than it needs to be.
I -can- just have two separate views on sf::Text and sf::Sprite components, but I want to understand what the common idiom for this kind of situation is, since my expected approach[0] is currently floating in the pull request void.[1]

TL;DR: I want to iterate distinct components that satisfy a common interface. What is the typical approach?
Thanks for any input!

[0] Issue #859 and PR #871): Component polymorphism support
[1] I -could- just merge that into the entt instance we use, but maybe someone can point out something super simple that I've completely ignored here.
Related(??): Poly (Getting beyond my understanding of templates at that point)
Related: #983 (the common interface iteration mentioned but not addressed)

@skypjack skypjack self-assigned this Mar 15, 2023
@skypjack skypjack added the triage pending issue, PR or whatever label Mar 15, 2023
@Daedie-git
Copy link

In my opinion, the approach that is most in line with the data-oriented philosophy of ECS would be be to have a different component for each final type, and store the component by value and not by pointer or reference.

Struct TextDrawable {
sf::Text text;
};

struct SpriteDrawable {
sf::Sprite sprite;
};

and you simple iterate them separately:

registry.view().each([&renderTarget](const TextDrawable& rDrawable){
renderTarget.draw(rDrawable);
});

registry.view().each([&renderTarget](const SpriteDrawable& rDrawable){
renderTarget.draw(rDrawable);
});

There are several reasons for this:
Components are stored in contiguous blocks of memory per component. Your CPU really likes this, because reading from RAM is slow, so it tends to fetch memories in blocks. When you fetch a block, only use a couple of those bytes and then toss the rest out, you're wasting a lot of CPU time (~200 cycles each time your CPU needs to read from the main ram). Iterating over X components is basically iterating over X big arrays and passing 1 of each to a function every iteration, which means these blocks are mostly filled with data that you are using.

  1. Now, If you store your object by pointer/ref instead of value. Instead of a nice big block of data to directly plow through, you get pointers to random blocks of memory scattered across the heap. This adds an extra level of indirection every time you access a single component, to a random location that stores sizeof(Object) of data. If you are lucky, this data fits neatly in your cache, so you don't waste quite as much. But maybe you are storing something like glm::vec3 (12 bytes), so your cpu basically loads a huge block and uses a tiny fraction, just to toss it out again.

  2. Because all components of a type are stored into a big pool, polymorphic iteration would also mean going back and forth between different pools. To be fair, this isn't too bad bad, because your CPU can hold multiple blocks at the same time. But polymorphic datatypes almost always comes in conjunction with polymorphic functions, which leads me to 3).

  3. Another thing your CPU really likes, is predictable code to execute. It tries to predict and prepare upcoming code. It also has some local space for instructions as well. So repeating the same function1 100 times and function2 100 times will run considerable faster than alternating arbitrarily between both. This is the main reason virtual functions are considered slow (the extra indirection from the VTable access is actually not that big of a deal).

Dynamic polymorphism kind of beats the purpose of DOD/ECS, so I would advice against it.

@skypjack
Copy link
Owner

Yeah, I agree with @DaedieGit when it comes to asking should I do it or not?.
As for supporting polymorphism out of the box, there is the linked proposal in a PR but I still find it quite invasive to be honest.
I refrain from merging things that can largely affect the codebase to support minor feature that I wouldn't even suggest to use.
This is pretty much the case. The PR also contains a link to an example (a much smaller and fully extrernal one) that works with the current version of EnTT if you like. However, using different components and staying away from polymorphism is imho the way to go.

@skypjack skypjack added question open question and removed triage pending issue, PR or whatever labels Mar 17, 2023
@nhammond129
Copy link
Author

Thank you both so much for the (quick!) input. I understand the situation a lot more clearly now; I knew something was up with my approach! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question open question
Projects
None yet
Development

No branches or pull requests

3 participants