benmoran56 / esper

An ECS (Entity Component System) for Python
MIT License
537 stars 67 forks source link

Querying entities with denylist of components #86

Closed UplinkPhobia closed 1 year ago

UplinkPhobia commented 1 year ago

Hello,

Would it be possible to add a way to select entities based on components that they must not possess? If I want to select, for example, all entities except the player, I have to select them all, then make a specific case to check that the entity I'm looking at doesn't have a "player" component. It feels like an unnecessary step, since the get_components function is already doing the job of selecting entities.

It would be nice to have something along the lines of World.get_components(self, *component_types: _Type[_Any], denied_component_types: tuple = None) to allow such a filter at query time.

I do not know if this poses any design or performance issue, however, so I hope this issue isn't completely absurd.

Felecarpp commented 1 year ago

Why not use a Enemy component ? You can call itertools.chain to select multiple tags.

from itertools import chain
from esper import World

class Ally:
    pass

class Enemy:
    pass

class Position:
    pass

world = World()
query = world.get_components

for entity, (_, pos) in chain(query(Ally, Position), query(Enemy, Position)):
    pass
benmoran56 commented 1 year ago

I would also suggest "tagging" Components, like Player and Enemy. This would likely be faster to query internally as well.

UplinkPhobia commented 1 year ago

I'm new when it comes to ECS so I might be wrong, but this doesn't feel like the philosophy of ECS to define entities by what they are not, instead of by what they are.

On top of that, it also doesn't work so well when the filters are not so obviously "reversible". What is not a player isn't necessarily an enemy, and I wouldn't know what kind of tag to use if I want to define "something that is not a dog".

benmoran56 commented 1 year ago

That is true. I don't think every entity would have the Enemy Component - just actual enemies.

In my own projects, enemies typically already have some other type of Component that can be used to differentiate them in some way. An EnemyAI or Target Component, etc., but the idea is the same.

I also tend to find that if made genericly enough, Processors usually don't need to know if an entity is a Player or not during initial iteration. For example, a LifeProcessor might handle removing HP from all Life Components, adding a Flash or Blink component, and handling cases where HP falls below 0. This last part is the only time where it needs to know if it's a player or not, since you would trigger a game over instead of showing an entity death animation.

    life.hp -= damage.amount
    if life.hp <= 0:
        if self.world.has_component(ent, Player):
            # game over
        else:
            self.world.add_component(ent, Blink())
            # etc

It might help if you can describe what the Processor you're making does.

UplinkPhobia commented 1 year ago

I am not making a Processor yet as I was trying to understand the library properly to have a proper structure of components and processors. Depending on the availability of a negative filter, I might need to adjust the components and I was trying to anticipate it, thus this issue.

But I understand your point of view and the logic behind it, so I'll just close the issue. If I find a situation where it seems impossible to solve without this feature, I might reopen it.

Thanks!