Ralith / hecs

A handy ECS
Apache License 2.0
989 stars 84 forks source link

OneOf Query Type #376

Open Moulberry opened 1 month ago

Moulberry commented 1 month ago

Hello, hope you are doing well.

This issue is about the discussion of a potential new query type OneOf. I am willing to create the pull request for this feature if it's desired, I'm creating an issue first to gauge whether or not this would be suitable for hecs.

Motivation

I can't find a built-in way to query one of a set of types, the closest is the Or query type but this has certain limitations as discussed below. The motivation for OneOf would be to implement different behaviour when the player interacts with some entity, i.e. clicking an Enemy/Item/Corpse/etc.

Example

The query type OneOf3<A, B, C> will match any entity that contains either A, B or C, but only up to the first match.

You can then use code similar on the result:

match item {
    One(a) => {},
    Two(b) => {},
    Three(c) => {},
}

edit: An alternative implementation would be to implement #[derive(Query)] for enum types as well, allowing users to define their own enum queries like:

#[derive(hecs::Query)]
pub enum AttackTarget {
    Enemy(Enemy),
    Item(Item),
}

I think this approach might be superior.

Alternatives that already exist (to my knowledge)

Layering Ors, like Or<A, Or<B, C>> Problems:

  1. Lots of boilerplate to set up the query with the repeated or and brackets
  2. Not very easy to concisely match on the result
  3. The query engine will continue checking for B and C even if A is present, i.e. it doesn't stop when you've already matched

Using a lot of options, like (Option<A>, Option<B>, Option<C>) Problems:

  1. Code using the query result will end up having a lot of if-else chains to check every option
  2. 3 Nones is a valid result, meaning you're going to end up iterating over every entity unless you constrain it with a more complex query eg. With<(Option<A>, Option<B>, Option<C>), Or<A, Or<B, C>>

Some other workaround, like inserting an Attack component when an entity is clicked Problems:

  1. Doesn't work for every use-case
  2. Performance penalty due to changing archetype
  3. More architecturally complex than just performing the logic you want directly

I apologize in advance if there's built-in a way to do OneOf. Once again, I am willing to contribute this feature if it's within the scope of hecs. Thank you for your time

VinTarZ commented 1 month ago

Permanently add

enum ClickTarget {
Enemy,
Item,
Button,
}

Component and query for (Entity, ClickTarget), then match click_target and get component you want using entity_id?

Moulberry commented 1 month ago

Permanently add

enum ClickTarget {
Enemy,
Item,
Button,
}

Component and query for (Entity, ClickTarget), then match click_target and get component you want using entity_id?

A few problems:

  1. This solution requires multiple queries
  2. No architectural guarantees that the ClickTarget is correct. The ECS allows you to add a ClickTarget to an entity that doesn't match and it also allows you to forget to add ClickTarget to an entity that would match
  3. Whether or not you want to target something is often more complex than what is expressible with a single enum variant. With this solution you'd need to either update/remote the ClickTarget or do additional checks to make sure it actually matches the query you want.
  4. Adding components just for making a query slightly easier seems like it'd be problematic long-term. A codebase could potentially have many queries it needs to do in this manner, and needing to add a unique component for every query could quickly spiral out of control.

While tagging entities with an enum might be an acceptable workaround for some use cases, I still think there's significant value in creating a way to do this with queries

Ralith commented 1 month ago

The motivation for OneOf would be to implement different behaviour when the player interacts with some entity, i.e. clicking an Enemy/Item/Corpse/etc.

That sounds like an operation on a single entity, rather than on a potentially large set of entities. You might be better served by obtaining an EntityRef than by using queries.

Performance penalty due to changing archetype

Moving a single entity between archetypes might not be free, but it is still very fast. Even moving thousands around is unlikely to ever be your bottleneck. If adding/removing components is a convenient way to express your desired semantics, don't hesitate to do it.

I think queries that accept one of multiple component types are usually an antipattern, and you should consider normalizing the information that your query wants to access into a single component that might be populated/updated by other systems. For example, prefer putting sprite identifiers in the ECS over deciding what sprite to draw based on which components an entity has.

That said, I have absolutely nothing against merging an extension to derive(Query) to support enums. There's just not much cost to supporting it, the semantics are obvious, and we already have Or anyway.