Anders429 / brood

A fast and flexible entity component system library.
Apache License 2.0
39 stars 1 forks source link

Possibility of a World::retain() method? #229

Open Oberdiah opened 1 year ago

Oberdiah commented 1 year ago

Hi, first off, just want to say I love this library, it's absolutely fantastic and exactly what I've been looking for. Also the documentation is amazing, so thank you so much for putting the effort in there.

I think a retain(..) interface would be incredibly handy to have, I find myself writing the following boilerplate quite a lot and there are definitely optimisations that could be made under the hood:

let mut entities_to_remove = vec![];
for result!(identifier, data) in world.query(query).iter {
    if data.health < 0.0 {
        entities_to_remove.push(identifier);
    }
}
for identifier in entities_to_remove {
    world.remove(identifier);
}

Also, do you take pull requests? I might take a shot at implementing the above myself if you think it wouldn't be too difficult of a change to make.

Anders429 commented 1 year ago

Thanks for the kind words, I'm glad to hear you're enjoying the library :) yes, I think World::retain() is something that would fit well into this library. I think a method that looks similar to Vec::retain() would work well. Probably something that looks like retain(query: Query, f: F) where F: FnMut(Views) -> bool (more or less, there would definitely need to be more generics stuck in there) would work.

I definitely welcome pull requests! I'm not sure the difficulty of this change, as I haven't had a chance to look at it closely yet, but feel free to give it a go. I'd recommend looking at how World::query() is implemented and trying something similar. I can look at this a bit closer later and try to give a bit more guidance if needed.

If you do end up forking and submitting a PR, please work off of the dev branch to avoid merge conflicts :)

Oberdiah commented 1 year ago

I gave implementing it a shot, but got quickly bogged down in all the generics - this is the most generic-dense code I have ever seen 😄 Special shout-out to ContainsViewsOuter in sealed.rs which has 77 lines straight of code in a where clause.

The main thing that held me up though was I couldn't figure out how to get the entity identifier from the query - the end user may or may not already have an entity::Identifier in their query so I can't just add one to the end.

Perhaps I'll start with one of the easier issues, like an example or two 😄

Anders429 commented 1 year ago

No worries! I have tried my best to keep the public API as clean as I can, but I will be the first to admit that the stuff under the hood can get a bit messy, especially when it comes to traits. I sometimes get confused myself when looking at it, and I wrote the dang thing lol. I am making efforts to clean up some of the code, but it's dependent on having enough time to work on it. And yes, ContainsViewsOuter in src/registry/contains/views/sealed.rs is one of the harder-to-parse examples. Some of the scheduler logic is actually worse in that regard, but I am working to fix it along with my in-progress fix for #222.

As far as the entity::Identifier thing, that's actually the entire purpose of having separate ContainsViewsOuter and ContainsViewsInner traits. ContainsViewsOuter is implemented for two distinct cases: impl<'a, I, IS, P, V, R, Q> ContainsViewsOuter<'a, V, (Contained, P), (I, IS), Q> for (EntityIdentifierMarker, R) and impl<'a, I, P, R, V, Q> ContainsViewsOuter<'a, V, (NotContained, P), I, Q> for (EntityIdentifierMarker, R). The single letter generic names are awful to read here, but basically they're both implemented for (EntityIdentifierMarker, R), where R is the registry of components, and the only difference is the first one is implemented when the entity identifier is Contained, and the second one is implemented when it is NotContained. The definition adds EntityIdentifierMarker onto the registry list, because an entity::Identifier is a special-cased view that doesn't actually view a component in the registry. In the "canonical" form of a set of views, the entity::Identifier always comes at the beginning of the list, so this basically handles pulling out the entity::Identifier from the views before letting ContainsViewsInner handle the canonicalizing of the rest of the component views.

That probably sounds a bit confusing, which is understandable. From a high level, a query is basically handled as follows:

  1. Filter only the archetypes that are actually viewed by the query (https://github.com/Anders429/brood/blob/dev/src/query/result/iter.rs#L109-L118)
  2. Call archetype.view() to actually iterate over the viewed components (https://github.com/Anders429/brood/blob/dev/src/query/result/iter.rs#L124-L130). Note that these components are stored in canonical order within the archetype.
  3. The archetype defers to ContainsViews to actually view the components (https://github.com/Anders429/brood/blob/dev/src/archetype/mod.rs#L260-L267). ContainsViews allows converting the Views into their canonical form, so they can actually be viewed.
  4. ContainsViews uses the sealed trait pattern, so the method is actually defined on the Sealed trait. From there, Sealed defers to ContainsViewsOuter::view(). Like I mentioned before, this is implemented in two cases: the entity::Identifier being present, or not. When present, the identifiers are iterated over. When not provided, we don't iterate over them.
  5. ContainsViewsInner is used to just define the canonical shape of the remaining components. The where clauses specify that the registry should implement CanonicalViews with respect to this canonical set of views. When we view the rest of the components (https://github.com/Anders429/brood/blob/dev/src/registry/contains/views/sealed.rs#L262), it is calling into CanonicalViews::view(), which is where the iterators over the components are actually created.
  6. Finally, since view is now in "canonical order", it is reshaped back to the order specified by the user.

The whole point is to allow users to specify components in any order they wish (similar logic exists for inserting components and other cases, but querying is far more complicated). This is how you can switch the order of components in queries and it doesn't break anything. This is the full power of heterogeneous lists :) if you want to see similar generic-heavy code in the Rust ecosystem, the frunk library's hlist module is full of this kind of stuff, though this is a much more specific application.

I can definitely take a look at more of the specifics of implementing retain(), but it will be a few weeks at least. My free time is limited, and I'm first working to resolve #222. The deletion aspect of this method might actually get a bit complicated, since we will actually need to delete all components in the entity, not just the ones used in the function f. We'll also need to make sure the entity::Allocator is aware of any changes to locations that are made as a result, but that part should be fairly straightforward :)

Oberdiah commented 1 year ago

Thanks for the in-depth response, that's really very helpful, I might go give it another bash with all this new knowledge in mind and see what I can come up with. For the deletion, I was planning just to defer to self.remove(..), at least initially for a POC, as performance-wise the entities to remove will be likely scattered all over the archetypes so there's possibly less to gain than I'd first hoped by treating it as a batch (although we can probably avoid the overhead of maintaining a separate entity::Identifier vec at least).

I was doing some fiddling with these heterogeneous entity views and with a bit of magic you can define a get_component() trait on them. I'm not quite sure of how useful it is yet, but it's certainly a cool thing you can't do in any other ECS system I've ever seen.

struct A1;
struct A2;

trait GetComponent<Q, T> {
    fn get_component(&mut self) -> &mut T;
}

impl<T> GetComponent<A1, T> for T {
    fn get_component(&mut self) -> &mut T {
        self
    }
}

impl<T, Q, A: GetComponent<Q, T>, B> GetComponent<(A1, Q), T> for (A, B) {
    fn get_component(&mut self) -> &mut T {
        self.0.get_component()
    }
}

impl<T, Q, A, B: GetComponent<Q, T>> GetComponent<(A2, Q), T> for (A, B) {
    fn get_component(&mut self) -> &mut T {
        self.1.get_component()
    }
}

(I'm using the generic Q and structs A1 and A2 here just as a discriminatory to prevent my impl definitions from overlapping)

Which can then be used as follows, for example, to define get_aabb() on all entity views that have both a Size and a Pos component:

trait GetAABB<T> {
    fn get_aabb(&mut self) -> AABB;
}

impl<Q, T: GetComponent<Q, Size> + GetComponent<Q, Pos>> GetAABB<(A1, Q)> for T {
    fn get_aabb(&mut self) -> AABB {
        let pos: Pos = *self.get_component();
        let size: Size = *self.get_component();

        AABB {
            min: pos - size / 2.0,
            max: pos + size / 2.0,
        }
    }
}