Replies: 17 comments 44 replies
-
First impressions, subject to change! Overall very excited to have anything resembling relations and a way to represent entity groupings in the engine though. I'll split my thoughts into a few messages though: one per category, to keep threading sensible. Category i-cant-believe-its-not-bsnI prototyped something in this space, built entirely out of bundles. I liked it for simple applications a lot, but the abuse of the type system had limitations (mulitple children of the same type clashed) and the performance characteristics are not good (so many archetype moves). It was also a bit magic, as the added component didn't hang around! |
Beta Was this translation helpful? Give feedback.
-
Category A
I don't like this :( This feels dangerous, and overlaps in confusing ways with hooks and observers. A1 is my favorite syntax here: I don't like the magic of the others. It should be immediately clear how to implement your own relations. |
Beta Was this translation helpful? Give feedback.
-
Category BI much prefer the clear separation between concepts in this approach. I think this will be easier to extend, refine and teach. You could add the |
Beta Was this translation helpful? Give feedback.
-
Category CI find this quite confusing to read. We're mixing our perspective throughout: the root level components are part of the parent, but the |
Beta Was this translation helpful? Give feedback.
-
Category DVery useful for weird procedurally generated stuff. I agree that this flavor of API is essential for flexibility, but I would also like to do better. With a push towards the singular closure-free forms I think this could be nice TBH. Nested scenarios are hard, but I think that a cursor-style builder API feels pretty good as a closure-alternative. You would need to preprocess this to avoid excessive archetype moves. world.spawn(Foo)
// The cursor is on the root entity by default
.related::<Child>(Bar, OhHiMark),
.related::<Child>(Bar)
// Move the cursor to the last added entity
.previous()
// Now we're nested
.related::<ChildOf>(Baz)
// Back to the root
.root()
.related::<Observer>(Observer::new(system)),
)); |
Beta Was this translation helpful? Give feedback.
-
Category EI don't like the mysterious () everywhere! I think this will worsen the experience for beginners and simple code substantially. |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
To expand on motivation, one of my key requirements from this work is a single composable trait that captures a "collection of objects tied together by relations". This is very useful for gameplay (and things like leafwing_manifest). I also think that it's the primary blocker on adding more complex widgets (which require multiple entities) to bevy_ui at this stage. With a foundational trait defined, we can have clear APIs / guidelines for what |
Beta Was this translation helpful? Give feedback.
-
Thank you for the write up. I suppose B1 is my preferred but The B's are legible and describe this set of entities and relationships to my mind: The C's I had a hard time convincing myself they described the same set of entities and relationships. I understand that they're intended to describe the same thing as above, but to me they read like this: |
Beta Was this translation helpful? Give feedback.
-
Out of curiosity, can I ask how the design space around this conversation would change if we hypothetically had a way to efficiently defer structural changes so we end up with one archetype move per entity when we "flush" those deferred changes? I ask because I've had a conversation lately with a colleague around ideas for achieving this in Bevy, and I wonder if it would alleviate some of the design pressures you're under. |
Beta Was this translation helpful? Give feedback.
-
I feel we should at least consider changing the definition of bundle from It seems to me that now that we have required components, spawning bundles (e.g. For the rare cases where you have more than 15 components, or want to extend a struct that derives Doing this removes the ambiguity with bundles of bundles and lists of child bundles, opening us up to options like this: world.spawn((
Foo,
Children::spawn(((Bar, OhHiMark), (Bar, Children::spawn(Baz)))),
Observers::spawn(Observer::new(system)),
)); I love that this doesn't require any macros, and I feel it's still very simple conceptually. A |
Beta Was this translation helpful? Give feedback.
-
copied from discord thread: Could B4 expand to C5/C1? I really like that the c variants don't need an extra Also naming nitpick, |
Beta Was this translation helpful? Give feedback.
-
I really like the feel of B4. Just like the Not entirely sure how beneficial this would be, but I think that in addition to the I'm also wondering how this would be handled: world.spawn(
Foo,
children![Bar],
children![Baz]
); Should a merge happen, adding them both? Should the compiler yell at us? What if each |
Beta Was this translation helpful? Give feedback.
-
If I understand correctly, We could have modeled these concepts as a single component containing a My question is, do we anticipate a future relations framework that would allow observers/reactions to be attached to an entity, without the need to spawn a separate satellite entity to hang them off of? I'm not really sure what's the plan for relations generally. In my own work, I decided to move away from the word "effect". I think your use of the word "effect" here means the effect upon the target entity during setup. By contrast, I defined the There was a question earlier about siblings referencing each other by entity id. There are also cases where an entity needs to refer to itself: that is, an entity contains a component which contains a closure, such that the closure has captured the entity id (because otherwise there's no way for the closure to deduce which entity it is attached to). However, to capture the id you can't insert the closure until the entity exists. In all these cases, the way I typically structure this is to generate the entity ids first, using spawn_empty; then I spawn the hierarchy, inserting in those entity ids where needed. This means that in addition to a "create child" primitive, we also need "create child with this predefined id". |
Beta Was this translation helpful? Give feedback.
-
I've built my UI framework on top of @alice-i-cecile's Note that I haven't read enough to comment about the approaches beyond ergonomics, so apologies in advance if this isn't relevant. My framework uses a builder pattern and ends up with the following syntax (note that I don't actually have the Foo
.spawn_child((Bar, OhHiMark))
.spawn_child(Bar.spawn_child(Baz))
.observe(Observer::new(system)); I also have the equivalent Under the hood, my builder pattern just boils down to tuples, i.e.: pub trait HierarchyBuilder {
fn spawn_child<B: Bundle>(self, bundle: B) -> (Self, WithChild<B>)
where
Self: Sized,
{
(self, WithChild(bundle))
}
}
impl<T> HierarchyBuilder for T where T: Bundle {} With internal access to Bevy, this could be moved up one level and become: world
.spawn(Foo)
.spawn_child((Bar, OhHiMark))
.spawn_child(Bar.spawn_child(Baz))
.observe(Observer::new(system)); I like this for a couple of reasons:
That said, moving it up one level would mean having 2 APIs since As such, I would probably actually prefer: world
.spawn(
Foo
.spawn_child((Bar, OhHiMark))
.spawn_child(Bar.spawn_child(Baz))
.observe(Observer::new(system))
); This avoids fragmenting the number of APIs too much and all the advantages of points 2-4 still apply. Anyways, my main comment would be that I would greatly appreciate if the raw API would keep in mind being able to support something like this on top of it. And, with that in mind, for the base API, I'd probably lean towards B1. I do think there is a lot of value in giving users a builder pattern similar to the one described above that encapsulates the verbosity of B1. That would get rid of the need for macros and allow users to easily 'Jump to Definition' in their editor and immediately see what |
Beta Was this translation helpful? Give feedback.
-
I read through some of the comments, and feel like there have been some really nice ideas. After some thought, I feel like a builder API which yields something which can be passed to The only other approaches with good enough DevX seem to use macros, which begs the question of why we wouldn't just use BSN (obviously there are differences, I'm sure, but this was the impression I got). Keeping the to-be-spawned hierarchy separate from the world, makes it easier for third party developers to interact (and create DSLs as another commenter mentioned). As for the benefits with a builder API, it would give developers a lot of flexibility when creating UIs without BSN, before spawning. Take a look at the following example: let mut button_wrapper = (
Name("Main Menu Button"),
Button
);
let start_button = button_wrapper
.add_child(Text::new("Start"));
let exit_button = button_wrapper
.add_child(Text::new("Exit"));
world.spawn(start_button);
world.spawn(exit_button); We could create two different buttons based on the same wrapper/core bundle. I feel this could be difficult without using a builder API. Some of these thoughts have probably already been discussed here, if so - apologies for the duplication. |
Beta Was this translation helpful? Give feedback.
-
Relations implementationI understand this discussion thread is mostly concerning the ergonomics of spawning APIs with regards to generic relationships, but I would also like to spend some time discussing the actual implementation of a basic relationships system, given it will have some influence on how the spawning APIs are implemented.
|
Beta Was this translation helpful? Give feedback.
-
I've largely wrapped up my "non fragmenting relations" implementation and ported Bevy to it. One choice that remains is to pick a new spawn pattern. There are lots of options and tradeoffs here. I'll try to paint the full picture so we can make an informed decision.
First some quick terminology (not set in stone ... I just need to call these things something so we can talk about them):
Relationship
: a new trait implemented for components that relate to another entity. This would beParent(Entity)
in our current Parent/Children implementation.RelationshipSources
: a new trait implemented for components that collect sources of relationships that relate to the current entity. This would beChildren(Vec<Entity>)
in our current Parent/Children implementation.My immediate goals are to create the following relationships: ChildOf/Children, ObserverOf/Observers, ReactionOf/Reactions.
The purpose of this post is to share where my head is currently at and solicit feedback on these ideas. If you have opinions on what is currently stated, or you have other ideas please comment below!
Motivation
Relationships are coming! We need to decide how we will spawn them. Ideally in a way that prevents unnecessary archetype moves and re-allocations.
This is not a competing proposal with things like BSN + Next Generation Scene / UI. It is instead choosing how relationships will be spawned under the hood. There is a focus on ergonomics in this post (even if BSN also solves that problem), as we can also take this as an opportunity to improve our baseline spawn experience (giving us wins pre-BSN and making scenarios that don't use BSN better). And in this case, ergonomics and performance go hand in hand as they both involve statically defining hierarchies!
Category A: RelationshipSources (ex: Children) Bundles
This category relies on a few key pieces:
Spawn<B: Bundle>
. This is what enables tuples of spawned children. This is unfortunately necessary as Bundles of bundles are ambiguous with tuples of child bundles. This is inherent to the concepts we have defined and not really a Rust limitation.RelationshipSources::spawn(spawnable: impl Spawnable)
function, which returns aRelatedEntitiesBundle
RelatedEntitiesBundle<R: Relationship, S: Spawnable>
, which both inserts the RelationshipSources component on the parent (withVec<Entity>
storage perfectly pre-allocated for the relationships) and spawns all entities in Spawnable with the Relationship component.Note that for the sake of comparison, I am not formatting these code blocks with rustfmt and instead I use the same layout for each variant. At the end of this post I use rustfmt in one final comparison.
Variant 1: Raw RelationshipSources::spawn / spawn_one calls
Variant 2: Hard-coded shorthand functions over RelationshipSources::spawn calls
Ex:
children()
isChildren::spawn()
,child()
isChildren::spawn_one
Variant 3: General purpose related! macro
This hacks around the bundle vs spawned ambiguity by treating spawned entities as "varargs" instead of a single tuple. This still produces
Spawn
wrappers under the hood.Variant 4: Hard-coded wrappers over related!
Same as Variant 3, but with convenience wrappers for common relationships.
Variant 5: General purpose entities! macro
General Category A Thoughts
Allowing Bundles to queue arbitrary commands inherently muddies the conceptual waters a bit, which could easily produce weird or unintuitive results. I don't love opening those floodgates. However Category A manages to retain the general "Each item in a Bundle is a component" concept. Ex:
(Foo, Children::spawn_one(Bar))
still insertsFoo
andChildren
on the parent. It just also spawnsBar
as a child. And unlikei_cant_believe_its_not_bsn
, it doesn't require archetype changes.Being able to count relationships and preallocate space in
RelationshipSources
prior to insertion is very nice, as is preventing the need for archetype moves.There are also some significant negative performance implications: because the Spawnable children are queued as commands, in each step down the tree we are copying the entire remaining tree onto the command queue. This is essentially inherent to the approach and is frankly probably a showstopper, as this is non-linear copying behavior.
There is also currently some weirdness, as every
Children::spawn()
insertion overwrites the previous entries. I don't think this is an intractable problem, but we'd need to develop a "merge" concept into bundles, which I'm also extremely hesitant to do.A1 has some extremely valuable benefits:
Children::spawn
behaves the same way asCustomThings::spawn
), and the author ofCustomThings::spawn
doesn't need to do any legwork to get that experience. There is "one way" to do things.The relatively meager ergonomic benefits of A2 don't feel "worth it" relative to the cost of losing (1) and (2).
However the ergonomic cost of
Spawn()
andRelationshipSources::spawn()
is high. We're paying a pretty high clarity cost.The
related!
macro in A3 is still "generic", removes bothSpawn()
andRelationshipSources::spawn()
, and allows us to remove thespawn
vsspawn_one
distinction. But it introduces macros (inherently something we try to avoid) and weirdly co-locates theRelationshipSources
type with children (it kind of looks like the "first child").The special cased macros in A4 solve the co-location weirdness and improve ergonomics at the cost of completely erasing the
RelationshipSources
collection type.A5 is not as ergonomically pleasant, but it does have the benefit of requiring no special casing. Ideally we could merge them into
Children::spawn!(Foo, Bar)
, but this style of macro isn't supported in rust to my knowledge.Category B: Effects
Effects allow the expression of the same general patterns as Category A, but they are expressed statically in such a way that does not require adding commands to bundles.
Effects are implemented for components and tuples of Effects (which means they look like Bundles, but they aren't!).
This resolves a couple of problems with Category A:
EntityWorldMut
after spawning the Bundle. This will still write data to split out bundles to insert from after effects, but it happens on the stack (and I suspect we could actually hack out the stack writes entirely with some targeted unsafe code).Fundamentally, this suffers from the same ergonomic and conceptual problems a Category A, but it resolves some of the performance concerns.
One major downside is that non-component Bundles require a wrapper. Ex:
world.spawn(BundleEffect(MyBundle { a: A(0), ..default()} ))
, or alternativelyworld.spawn_bundle(MyBundle {a: A(0), ..default()})
. Given the massive reduction of custom bundle prominence and the benefits of Category B, this seems reasonable to me.If we adopt this pattern, I think we should discourage effects that "appear like components but do not produce the component they appear to produce".
I'm omitting the variants as they are the same as Category A.
As an aside, I'll note that @viridia has campaigned for something in this category before.
Category C: Single-Relationship Bundles
This is essentially the same approach as Category B, but applied to the
Relationship
instead of theRelationshipSources
Variant 1: Raw Relationship::spawn calls
Variant 2: Hard-coded shorthand functions over
RelationshipSources::spawn
callsVariant 3: related! macro
Variant 4: Hard coded wrappers over related!
Roughly the same as Variant 2 ergonomically, but it enables tuple flattening.
Variant 5: related function
General Category C Thoughts
This whole category has the downside of no-longer preserving the "bundle is just components inserted on the entity" concept.
(Foo, ChildOf::spawn(Bar))
looks like we're insertingChildOf
on the parent, which is not the case. And when specifyingChildOf
(like we do in V1 and V3), it reads likeFoo is a ChildOf Bar
. The weirdness factor on this is higher. Spawning children under Children makes much more sense in this context.However this does have the benefit of removing the need for
Spawn
without needing macros, which is very nice. Unlike the best options in B, C1, C3, and C5 still expose one of the relationship symbols. C3 and C5 help protect against the confusion in C1, as the Relationship is inside therelated
scope instead of adjacent to other components.The special cased
child
syntax in V2 reads very nicely and is very ergonomic (even without the macros in V4). However it requires special casing to be nice and it completely erases both theRelationship
and theRelationshipSource
symbols. Both of these feel like huge losses to me.This whole category makes it more challenging to pre-allocate space in the RelationshipSource collection and naively prevents inserting the RelationshipSource alongside the bundle (as each Effect would try to produce a RelationshipSource, which would result in duplicate components, which is invalid), meaning it will need to cause a table move on the first insert, barring a solution to the problem that I haven't fully conceptualized yet (ex: a "mergable" relationship source bundle that is lazy and joins duplicates, producing a count, which is used to allocate a single RelationshipSource with capacity for all of the children).
Category D: Builders like we have today
Variant 1: Generic
related
/related_one
callsVariant 2: Hard-coded wrappers over generics for common relationships
General Category D Thoughts
This doesn't rock the boat too much. Pretty much everyone dislikes writing closures. V1 is general purpose but even worse than the current status quo ergonomically. Special cased builder functions for common relationships is essential for ergonomics.
We can / should support these APIs no matter what. But I think we can do better for our go-to spawn API.
Category E
Same as Category B, but instead of the Effect/AfterEffect split, you just always spawn with a (Bundle, AfterEffect) pair:
This would (maybe) have better memory access patterns as we're not fully building rebuilding tuples (but this would require that effects dont produce bundles that are added alongside the main bundle ... in which case we go right back to Category B performance). And it has a clearer separation between "bundles" and "effects". The biggest downside is that () now shows up everywhere (or requires the addition of variants for "has effects" vs "doesn't have effects").
Conclusion
I'm obviously biased toward Category B or C. It seems like the best tradeoff between performance and ergonomics. Within Category B, I'm still a bit undecided on the ideal variant. I've ruled out B2 as its a worst of all worlds situation. B3 feels like a necessary evil for custom relations, but also feels nonviable as the default. Here are the remainders again (this time formatted by rustfmt):
I think the
children/children!
approach is objectively better than individualchild/child!
macros, so I have excluded C2 and C4. I have also omitted C1 as I think it is too easy to mistake ChildOf as a component on the parent and not the child.Pretty hard to deny how nice B4 is to work with and read. Maximally ergonomic, it fits the "just another component in the bundle" mental model nicely, no archetype moves necessary, and it trivially enables pre-allocating space in the collection. The manual per-relation macros reduce the generic-ness of the system, and fully erasing the Relation (ChildOf) and RelationSource (Children) symbols makes me sad. But I'm leaning toward it being worth it, given how common these operations are. Fundamentally the B options all build on each other, so we could support them all. I really would like a
Children::spawn!()
macro as that feels like the ideal balance, but alas Rust does not support this.C5 also feels really nice to me: no macro weirdness, not too confusing, and consistent across relationship types. Sadly it also comes with the downside of being less ergonomic / lots of repeated symbols for each child.
For one-off / custom relations, C5 feels ideal. For common / high frequency relations, B4 feels ideal. We could consider supporting both, but I think that would benefit from solving the "coordination problem" (allow
related(ChildOf)
andchildren!
to both contribute their counts to a single preallocatedChildren
component). This would also solve the current problem of consecutivechildren!
stepping on each other (not good!).Curious what you all think!
Beta Was this translation helpful? Give feedback.
All reactions