-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Remove entity reserving/pending/flushing system #19451
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
base: main
Are you sure you want to change the base?
Remove entity reserving/pending/flushing system #19451
Conversation
it's not exact, but it should be good enough.
Fully agree!
Upon reviewing the code, I agree :) I'd like to make this type of failure a variant inside of EntityDoesNotExistError, but the information shouldn't be lost.
I'm pretty happy with the naming balance that you've struck here. We've avoided hitting any of the high traffic APIs, while making the lower level ones as precise as possible. IMO we can reconsider renaming more as users warm up to these ideas. |
crates/bevy_ecs/src/entity/mod.rs
Outdated
//! Entities serve the same purpose as things like game objects from Unity or nodes from Godot: | ||
//! They can be created, have their data accessed and changed, and are eventually deleted. | ||
//! In bevy, an entity can represent anything from a player character to the game's window itself (and everything in-between!) | ||
//! However, unlike other engines, entities are not represented as large class objects. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like what you have to say! I think what you have written is good info for people who are just starting to figure out how an ECS works.
But I don't think that should be the point of this module doc right here. I think what you have written in these two paragraphs would be better content for something like an "intro to entities with Bevy" post. I just don't think that people who don't know what an entity is are going to be able to find these docs in the first place. To put another way, only people who already have some idea of what an entity is are going to be able to get to this module.
That being said, I don't think you should get rid of everything, there is a lot of good stuff here! I think deleting the references to OOP would be a good start.
I don't really know what you mean by a "conceptual entity" here. The biggest difference is that the [
Entity] type does *not* represent a conceptual entity.
Love this sentence! //! Instead, the [
Entity] acts as an id, and its components are stored separate from its id in the [
World].
This is good! I think for module docs you might want to make the wording more declarative. I know that you are not saying how retrieving an entity's components actually works, but just trying to give a general idea. Still, for module docs, I want to know how something actually works, not the theoretical idea of how it works.
//! In fact, one way to think about entities and their data is to imagine each world as a list of entity ids
//! and a hashmap for each component which maps [`Entity`] values to component values if the entity has that component.
//! Of course, the [`World`] is really quite different from this and much more efficient, but thinking about it this way will be helpful to understand how entities work.
Love this!
//! In order to get an entity's components, bevy finds the values by looking up the [`Entity`]'s [`EntityRow`] and the [`Component`](crate::component::Component)'s [`ComponentId`](crate::component::ComponentId).
//! Interacting with an entity can be done through three main interfaces:
//! Use the [`World`] with methods like [`World::entity`](crate::world::World::entity) for complete and immediate access to an entity and its components.
//! Use [`Query`]s for very fast access to component values.
//! Use [`Commands`] with methods like [`Commands::entity`](crate::system::Commands::entity) for delayed but unrestricted access to entities.
IMHO, you are clearly somewhat of a domain expert on entities in an ECS at this point. If you want to, you could post a blog about what you learned working on this and I think a whole lot of people would love to read it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I don't think that should be the point of this module doc right here. I think what you have written in these two paragraphs would be better content for something like an "intro to entities with Bevy" post.
Broadly agree with this :) This is Book content.
I don't really know what you mean by a "conceptual entity" here. The biggest difference is that the [Entity] type does not represent a conceptual entity.
I would really like to completely remove the term "conceptual entity" if at all possible. I don't think it helps clarify things.
//! |Despawn an entity|[`EntityCommands::despawn`]|[`World::despawn`]| | ||
//! |Insert a component, bundle, or tuple of components and bundles to an entity|[`EntityCommands::insert`]|[`EntityWorldMut::insert`]| | ||
//! |Remove a component, bundle, or tuple of components and bundles from an entity|[`EntityCommands::remove`]|[`EntityWorldMut::remove`]| | ||
//! Entities have life cycles. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this stuff on the entity lifecycle! I think its really good
//! | ||
//! # Entity Ids | ||
//! | ||
//! As mentioned entities each have an [`Entity`] id, which is used to interact with that entity. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming you mean the entity structure has two fields: an integer of some kind for the row and an integer of some kind for the generation.
There is just a lot of the word "entity" thrown around in this entity id section so it might useful to reference the actual struct with "entity struct" literally and then name its fields. I don't know if I'm being clear here but basically I'm saying it's hard to know what entity is which or rather what entity you are referring to.
crates/bevy_ecs/src/entity/mod.rs
Outdated
//! If this is `Some`, the row is considered constructed, otherwise it is considered destructed. | ||
//! Only constructed entities, entities with `Some` [`EntityLocation`], participate in the [`World`]. | ||
//! The [`EntityLocation`] further describes which components an entity has and where to find them. | ||
//! That means each entity row can be in three states: 1) It has some components, 2) It has no components *empty*, 3) It has no location *null*. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this stuff! This is great!
Though you mention above that an An [
EntityRow] always references exactly 1 entity in the [
World]; they are always valid.
Yet here you say that each entity row can be in three states: 1) It has some components, 2) It has no components *empty*, 3) It has no location *null*.
Is a null location an invalid entity row? What about when it has no components? I guess I'm asking what you mean by a "valid" entity row
//! All an [`Entity`] id is is a [`EntityRow`] (which entity it is) and a [`EntityGeneration`] (which version of that row it references). | ||
//! When an [`Entity`] id is invalid, it just means that that generation of its row has been destructed. | ||
//! | ||
//! As mentioned, once an [`EntityRow`] is destructed, it is not discoverable until it is constructed again. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome stuff!
//! When it is despawned, all bevy does is [`World::destruct`](crate::world::World::destruct) it and return the [`Entity`] id (with the next [`EntityGeneration`] for that [`EntityRow`]) to the allocator. | ||
//! It's that simple. | ||
//! | ||
//! Bevy exposes this functionality as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also great stuff!
/// | ||
/// [generational index]: https://lucassardois.medium.com/generational-indices-guide-8e3c5f7fd594 | ||
/// This uniquely identifies an entity in a [`World`]. | ||
/// Note that this is just an id, not the entity itself. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember watching some intro videos on ECSs and I was taught that an entity is just an ID. This may be a point of confusion for some people.
At this point I know that in Bevy, an entity is a struct with a row an a generation. But maybe something to keep in mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mostly just read over the docs, but I really like the docs. And I don't think this is as complicated as I thought it would be after reading over those entity mod docs.
It seems like you are just making an allocator just like any another allocator, but it's for entities in an ECS. Then you want to make sure everyone is using the allocator instead of whatever was there before.
Thanks for doing this! Improvements like these really matter
@Carter0 Thanks for the review! That feedback was incredibly valuable. I'll work on some improvements sometime tomorrow probably. One thing you said was confusing was the "conceptual entity" phrase. If that's confusing, it should be reframed, but I'm not sure how. By conceptual entity, I mean the "thing" (ex: player, item, window, etc), the "game object". Bevy hasn't coined a term for this yet. If we renamed @alice-i-cecile You mentioned some of this stuff should be book content, which makes a lot of sense to me. Where would you like me to draw the line (ex: what should I assume the reader already knows), and how would you like me to proceed (ex: cut the book content from the pr, keep it and later transfer to a book, something else)? Also, while docs are important, this is something we should get right, which may mean a future pr to polish these docs while merging this pr first as a base. This is not the only thing blocking my remote reservation work; there's also Cart's decision on entity paging, but I wanted to toss out the idea: this could be a two phase solution. But that's up to you. I'm fine either way. |
Glad it helped!
Ahh I see what it is now that makes sense. I think for module docs, an explanation at the top saying something like, "an entity in this context refers to an id in a database like structure, not a gameobject in the world" is good enough. I think most people who have made it to the entity module will be able to pick it up from that. If you need to refer to the gameobject in its entirety, you could probs just call it the game object. You are right though that this is a point of confusion for people new to ECS. The term entity is used in a synonymous way with the term gameobject outside of an ECS, whereas here it means something more specific. This would be good to mention in the book. Let me know if there are other ECS docs you want me to check out. I want to know how this stuff works so I'm willing to help! |
So, rereading this, I think we should cut pretty much the entire first section of your module docs from this PR. Ultimately, that's book / crate docs content. The scope of these module docs should be "a high-level overview of the entity internals", and your target audience should be "someone who knows how to follow a tutorial and make simple jam games in Bevy already, but who wants to understand how it works under the hood". This content is solid and you're both a skilled writer and a domain expert. It could be adapted into either book or crate docs, but that work should be done in a different PR to simplify review and merging :) For the new structure, I would propose:
With respect to conceptual entities:
|
Co-authored-by: Alice Cecile <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this solution, makes it feel like a state stack.
Observation: there's quite a few very long doc comments that would be nice to have split across multiple lines prior to merging, but not a blocker.
//! which is different from null since these are still participating entities, discoverable through queries and interact-able through commands; | ||
//! they just happen to have no components. | ||
//! | ||
//! An [`EntityRow`] always references exactly 1 entity in the [`World`]; they always exist (even though they may still be null). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsure if you intended to repeat this phrase exactly (first appearance on line 75). Not an issue, just an observation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open to alternative phrasing. I think it's fine as is, but if anyone has a suggestion or the repetition is annoying, it can be changed.
I'm just trying to drive home the unintuitive point that an Entity
may not exist, but an EntityRow
will always exist (just might be null/not constructed).
entity_location = self | ||
.entities() | ||
.get(entity) | ||
.expect("For this to fail, a queued command would need to despawn the entity."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this panic be reached if a hook or observer despawns the entity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe. I'm not sure. But this panic existed before. It's just that instead of expecting on the option, it would have a None
/invalid location, which would cause panics elsewhere. On paper, this behavior is only different in that it is more clear where the assumption is and has a better error message if it is wrong.
Co-authored-by: Christian Hughes <[email protected]>
Objective
This is the next step for #19430 and is also convinient for #18670.
For context, the way entities work on main is as a "allocate and use" system. Entity ids are allocated, and given a location. The location can then be changed, etc. Entities that are free have an invalid location. To allocate an entity, one must also set its location. This introduced the need for pending entities, where an entity would be reserved, pending, and at some point flushed. Pending and free entities have an invalid location, and others are assumed to have a valid one.
This paradigm has a number of downsides: First, the entities metadata table is inseparable from the allocator, which makes remote reservation challenging. Second, the
World
must be flushed, even to do simple things, like allocate a temporary entity id. Third, users have little control over entity ids, only interacting with conceptual entities. This made things likeEntities::alloc_at
clunky and slow, leading to its removal, despite some users still having valid need of it.So the goal of this PR is to:
Entities
from entity allocation to make room for other allocators and resolvealloc_at
issues.reserve
andflush
patterns toalloc
andconstruct
patterns.It is possible to break this up into multiple prs, as I originally intended, but doing so would require lots of temporary scaffolding that would both hurt performance and make things harder to review.
Solution
This solution builds on #19433, which changed the representation of invalid entity locations from a constant to
None
.There's quite a few steps to this, each somewhat controversial:
Entities with no location
This pr introduces the idea of entity rows both with and without locations. This corresponds to entities that are constructed (the row has a location) and not constructed (the row has no location). When a row is free or pending, it is not constructed. When a row is outside the range of the meta list, it still exists; it's just not constructed.
This extends to conceptual entities; conceptual entities may now be in one of 3 states: empty (constructed; no components), normal (constructed; 1 or more components), or null (not constructed). This extends to entity pointers (
EntityWorldMut
, etc): These now can point to "null"/not constructed entities. Depending on the privilege of the pointer, these can also construct or destruct the entity.This also changes how
Entity
ids relate to conceptual entities. AnEntity
now exists if its generation matches that of its row. AnEntity
that has the right generation for its row will claim to exist, even if it is not constructed. This means, for example, anEntity
manually constructed with a large index and generation of 0 will exist if it has not been allocated yet.Entities
is separate from the allocatorThis pr separates entity allocation from
Entities
.Entities
is now only focused on tracking entity metadata, etc. The newEntitiesAllocator
onWorld
manages all allocations. This forcesEntities
to not rely on allocator state to determine if entities exist, etc, which is convinient for remote reservation and needed for custom allocators. It also paves the way for allocators not housed within theWorld
, makes some unsafe code easier since the allocator and metadata live under different pointers, etc.This separation requires thinking about interactions with
Entities
in a new way. Previously, theEntities
set the rules for what entities are valid and what entities are not. Now, it has no way of knowing. Instead, interaction withEntities
are more like declaring some information for it to track than changing some information it was already tracking. To reflect this,set
has been split up intodeclare
andupdate
.Constructing and destructing
As mentioned, entities that have no location (not constructed) can be constructed at any time. This takes on exactly the same meaning as the previous
spawn_non_existent
. It creates/declares a location instead of updating an old one. As an example, this makes spawning an entity now literately just allocate a new id and construct it immediately.Conversely, entities that are constructed may be destructed. This removes all components and despawns related entities, just like
despawn
. The only difference is that destructing does not free the entity id for reuse. Between constructing and destructing, all needs foralloc_at
are resolved. If you want to keep the id for custom reuse, just destruct instead of despawn! Despawn, now just destructs the entity and frees it.Destructing a not constructed entity will do nothing. Constructing an already constructed entity will panic. This is to guard against users constructing a manually formed
Entity
that the allocator could later hand out. However, public construction methods have proper error handling for this. Despawning a not constructed entity just frees its id.No more flushing
All places that once needed to reserve and flush entity ids now allocate and construct them instead. This improves performance and simplifies things.
Flow chart
(Thanks @ItsDoot)
Testing
Showcase
Here's an example of constructing and destructing
Future Work
Entity
doesn't always correspond to a conceptual entity.EntityWorldMut
. There is (and was) a lot of assuming the entity is constructed there (was assuming it was not despawned).Performance
Benchmarks
This roughly doubles command spawning speed! Despawning also sees a 20-30% improvement. Dummy commands improve by 10-50% (due to not needing an entity flush). Other benchmarks seem to be noise and are negligible. It looks to me like a massive performance win!