-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Improved Entity Lifecycle: remove flushing, support manual spawning and despawning #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?
Improved Entity Lifecycle: remove flushing, support manual spawning and despawning #19451
Conversation
it's not exact, but it should be good enough.
| @@ -1,40 +1,174 @@ | |||
| //! Entity handling types. | |||
| //! This module contains all entity types and utilities for interacting with their ids. | |||
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 not sold on the Construct / Destruct vs Spawn / Despawn terminology split. I think we should embrace Spawn / Despawn everywhere in the interest of simplicity and clarity:
- I'm not seeing anywhere where this would break down. We "allocate" entities prior to spawning them. An "allocated" entity does not exist in the world, does not show up in queries, and does not have a location. A "spawned" entity exists in the world, shows up in queries, and has a location. "Despawning" an entity removes it from the world and frees up the location (and by default also "frees" the entity id). I see "despawn" as the one break in the term flattening: with separate terms/operations you could "destruct" without also freeing up the
entityfor future use. We can resolve this by adding adespawn_no_freealternative (name open to discussion), which despawns the entity but does not return the entity to the allocator or rev the generation (meaning we could spawn the entity with the same generation at some future date). - By design, construct / destruct is a public, user-facing API. This is additional complexity that developers in some scenarios will need to deal with. We train people to think about this in terms of "spawning" in the common case, but then they hit this weird "functionality subset" term which is almost exactly but not quiiiiite identical to spawn-ing.
- As already discussed in this PR, this creates a weird terminology "discontinuity" in some cases where we're setting "high level" values (ex: the "spawned at" tick) in a "low level" context (Entities, which uses "constructed" terminology)
- This does away with the "spawn_null" weirdness, which does not actually "spawn" the entity in the world in any meaningful way. We would replace this with
world.entities.alloc()(or perhapsworld.alloc_entity(), although I'm not sure we need a wrapper for what is a pretty niche operation). - This does away with construct / destruct terminology showing up in spawn / despawn errors. By unifying them, we improve the clarity / understandabiltiy of these errors for the layperson.
Unless I am missing something important, we should unify the terminology. Ex: rename instances of construct(entity, bundle) to spawn_at(entity, bundle).
(note that I discuss some of the details of the unification in my next comments, so read those before responding)
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 on the same page with your goals here. The naming split is needless and confusing for most use cases. As you mentioned, there is little difference between spawn vs allocating and constructing. Maybe some power user would want a custom commands implementation, but other than that, they're basically the same thing.
But that may change in the very near future. There is a big difference between spawn vs allocating and constructing when considering the possibility of multiple or even no entity allocators. With entity paging, its quite possible, even probable, that users will use multiple different allocators from time to time. Most things can use the main allocator, sure, but some things may need remote allocation. Other times, an entity id may be communicated over some network to be replicated in a world. In that case, the id isn't allocated at all and just needs to be constructed right away. With these things in the picture, spawn becomes a special case of construct rather than construct being an implementation detail of spawn, as it is now. In fact, in a heavily multiplayer game, calls to construct may outnumber those to spawn. Hopefully that explains where my head was at when I named these. But none of that applies now, not yet, not until entity paging (which is entirely up to you, but I personally think is well worth it).
What does matter now is the possibility of additional entity allocators (which is technically possible today, though its not pretty), custom and other command-like interfaces needing construct (especially from async, like in assets as entities), and internal documentation (just because it isn't common for users doesn't mean we shouldn't give this a unique, precise name). Keeping this separations of concerns between allocating entities and their existence (generation is as expected) vs constructing entities and their construction status (whether their location is Some), I think, will help provide more useful information.
Take ConstructedEntityDoesNotExistError from a Query::get, for example. We could shorten this to something simpler, but then it would loose valuable debug information. If it is EntityDoesNotExistError , you know the entity requested no longer (or never did) exist. It has been despawned. But if it is EntityNotConstructedError , you know the entity does exist; it just hasn't been constructed yet. I remember when I started using bevy, I tried to spawn an entity from commands and then immediately look it up an a query. Obviously, it doesn't work that way; I got an error. I kept thinking, "Of course the entity exists; I just spawned it!" And it did exist; it just hadn't been constructed yet. If we had a better error type, this would not have taken me nearly as long to debug. Obviously, that's a trivial example from someone very unfamiliar with an ecs at that point, but you get the idea.
Because of all that, I think my position is this: We should clearly distinguish spawning vs construction, and we should prioritize precision over simplicity. I know it is a Bevy principal to keep everything simple, but this is one case where this simplicity has given me more headache via ambiguity than the complexity would give me confusion, at least for me. Of course, that is really up to you. I truly don't mind changing the name away from construct. The spawn_null is especially gnarly. But I think we should name them distinctly from spawning, since spawning makes assumptions about the source of the allocated entity.
We could rename construct to spawn_at, destruct to despawn_no_free, and spawn_null to alloc_entity_default_allocator or remove it. But I think this could become confusing with multiple allocators because I would guess people would just collectively refer to it as spawning, even though it was really spawn_at, which could lead to bugs and misunderstandings. Ex: You never spawn and entity to replicate it from a server, you spawn at an id. Or people trying to figure out where the spawn function is for async. It's not there because it's not spawning! It's allocating and constructing.
Ok. I think that explains my whole perspective here. Sorry its so long... Anyway, now you know the why behind my opinion. I don't see a perfect option here, unfortunately, but I'll happily implement whatever you pick.
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 think merging the terms works out if you consider the defition of spawning to be: "making an Entity present in the World" (which is notably how developers already think about it). Also note that I really wanted to say "exist in the world" (see my comment in the other thread).
"Spawn" (as an operation) inherently doesn't need to care about which Entity it is (or where it came from). That all comes down to how you spawn:
// Entity not specified, use the default allocator to create a new one
world.spawn(Player);
// Entity specified, assume it is not yet spawned / valid, and fail if it is invalid / already spawned.
// _Which_ allocator it came from doesn't matter.
world.spawn_at(entity, Player);
// Default allocator
let entity = world.allocator.alloc();
world.spawn_at(entity, Player);
// Custom allocator
let entity = my_network_allocator.alloc();
world.spawn_at(entity, Player);Imo that is a MUCH better API from a user perspective than:
world.spawn(Player);
let entity = world.entities.alloc();
world.construct_at(entity, Player);
let entity = my_network_allocator.alloc();
world.construct_at(entity, Player);Especially when consider that the first two are "identical" semantically, will share error messages, etc. Additionally, we have things like the Spawned filter, which track entities that were just spawned. That won't care about the spawn vs construct designation (nor should it!). We could change it to Constructed, but then theres no clear pairing between the world.spawn() operations and Constructed. We've forced "normal" users to contend with a pretty arbitrary distinction (and made it harder for them to discover functionality, made their code less understandable to others, etc).
just because it isn't common for users doesn't mean we shouldn't give this a unique, precise name
I don't think this is meaningfully more precise, and it has significant UX splash damage.
We could shorten this to something simpler, but then it would loose valuable debug information
Do we? EntityDoesNotExistError and EntityNotSpawnedError seems to do the exact same job as EntityDoesNotExistError and EntityNotConstructedError.
And it did exist; it just hadn't been constructed yet
The confusion here is a matter of commands being deferred. We have not "spawned" a Player entity immediately after commands.spawn(Player) returns. Adding "constructed" into the mix doesn't help developers understand that core piece, and it in fact makes it harder, as it complicates the lifecycle further, and adds a verb to the mix that most developers haven't used or been exposed to.
Developers would hit the same core issue if they did let id = commands.alloc(); commands.construct(id, Player).
That being said, I do think the error message should reflect the "existince" vs "spawned" distinction.
We should clearly distinguish spawning vs construction, and we should prioritize precision over simplicity
I really don't think this "precision" buys us any additional understandability from a user perspective. I think it impedes understandability. It is actually less precise as it makes "spawning" a "lossy" higher level concept that actually has no real representation in the ECS internals, error messages, events, or queries.
spawn_null to alloc_entity_default_allocator or remove it.
We should just remove it in favor of world.allocator.alloc().
Or people trying to figure out where the spawn function is for async.
It seems to me like an let id = async_world.spawn(Player).await; method could exist, where we await world access and then just do a normal spawn operation.
In general I'm still very unconvinced. I think we should unify the terminology.
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 still disagree, but I don't have any other points to make, nor do I see any fundamental issues with what you're saying. I think our disagreement is more philosophical: You want a simpler, unified API; I want a more rigorous terminology to describe entities. Each have their downsides: With the simpler API, it becomes less clear and harder to understand what constitutes "presence", "existence", "validity", etc. With more rigorous terminology, we risk continuous debate over semantic phrasings that mean the same thing in practice (like which of the 50 different C++ cast methods to use, etc.) So I disagree, but I am also content with your decision.
Now I'll try to understand more practically what to do to unify these terms. We need to rename everything such that the phrasing of the names fits within the context of only the spawning concept. Hmmm...
Some things are pretty easy, as you've already shown. Remove spawn_null. Rename construct to spawn_at. Rename destruct to despawn_no_free.
Beyond that is more challenging. Here's my best guess: Rename EntityDoesNotExistError to OutdatedEntityId or maybe InvalidEntityId. Rename EntityNotConstructedError to EntityNotSpawnedError. Rename ConstructedEntityDoesNotExistError to EntityDoesNotExistError. That's my preferred variation at least.
And then in docs, because internal code dealing with the entity lifecycle will still need to describe this, we can call it "allocating", "spawning", "despawning", and "freeing", and we can just deal with the inherent ambiguity of spawn doing both "allocating" and "spawning" and despawn doing both "despawning", and "freeing".
I think our last task here is just finalizing naming, so I'm trying to pull together all the ideas from each thread here into something I can act on. Once we nail the names down, I'll implement it tomorrow and update the migration guide and docs. How do these names sound to you, or do you have a different naming idea?
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.
So I disagree, but I am also content with your decision.
Excellent. Much appreciated!
Some things are pretty easy, as you've already shown. Remove spawn_null. Rename construct to spawn_at. Rename destruct to despawn_no_free.
Yup do those.
Beyond that is more challenging.
Yeah lets nail down terms:
valid entity: an entity in the current identity space, which could be spawned (ex: valid generation, perhaps in the future valid page)
invalid entity: an entity that cannot exist in the current identity space (ex: invalid generation, perhaps in the future invalid page)
alloc / allocate: allocate a new "valid" entity, which is currently "not spawned"
- note that spawn_at doesn't care which allocator an entity came from. This means that a developer could manually spawn entities that conflict with an entity returned by an allocator, resulting in some future failed spawn
free: return a valid "not spawned" entity to an allocator. This usually involves rev-ing the generation
spawn: for some valid entity, make it present in the world (shows up in queries, present in archetypes + tables, etc). Where the valid entity comes from depends on how the spawn operation is invoked (manually provided, allocated with the default allocator, custom allocator, etc). A "spawned" entity is inherently a "valid" entity, as we managed to spawn it!
despawn: for some valid entity, remove it from the world (no longer shows up in queries, no longer present in archetypes + tables, etc). What happens to the despawned entity depends on how the despawn operation was invoked (returned to the default allocator, returned to a custom allocator, nothing / requires manual management, etc)
Any method or error that connects to these contexts should use these terms, and none of these terms should be used to represent any other ECS concept. I think we should do away with "existence" to describe ECS state, as that is identical to the "spawned" state.
I would prefer to avoid using "entity id" terminology generally, as it feels redundant to me.
I think we should have the following errors:
// We did an operation that assumed the entity was valid, but it was not
struct InvalidEntityError { /* .. */ }
// We did an operation that assumed a valid entity was spawned, but it was not spawned.
struct ValidEntityNotSpawnedError { /* .. */ }
// We did an operation that assumed an entity was spawned, but it was not
struct EntityNotSpawnedError {
// The entity is valid but not spawned yet
ValidEntityNotSpawned(ValidEntityNotSpawned),
// The entity is invalid, and therefore could never have been spawned
InvalidEntity(InvalidEntityError)
}I believe those errors should cover pretty much every case. Feel free to add additional errors at your discretion. But ideally we keep the error zoo as small as possible.
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.
In theory, everything is renamed and re-documented.
We didn't discuss every rename, but I've done my best to remove all existence and entity id terminology from names.
I did use those terms in the module docs. I'm still not sure how to avoid it, but you're welcome to give it a read and see if you have any ideas for better phrasing.
Also, removing spawn_null required me to expose more directly access to the EntitiesAllocator, which is fine but worth mentioning.
The only place I changed a little from your names was ValidEntityNotSpawnedError . I called it EntityRowNotSpawnedError because I kept getting confused over whether ValidEntityNotSpawnedError meant "I know this id is valid; is it spawned?" vs "I expect this entity to be both valid and spawned; am I right?". But I also know, at least a while ago, you weren't wild about the "row" naming either, so I'm open to suggestions.
This had a lot of name and docs churn, and I don't think I missed anything, but I might have. Let me know if you see anything.
crates/bevy_ecs/src/bundle/insert.rs
Outdated
| swapped_entity.index(), | ||
| unsafe { entities.get_spawned(swapped_entity).debug_checked_unwrap() }; | ||
| entities.update_existing_location( | ||
| swapped_entity.row(), |
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 do indeed think this should be index and not row. Entities is not a Table functionally. It is an array. Yes every array in a certain light could be considered to be a single column table. But that is not how we approach that in the context of Bevy or Rust. Even in the context of Bevy ECS specifically I find it mismatched, as Table is a specific thing, and Entities is not that thing. I'd prefer to fix this everywhere in this PR, rather than spread the row terminology further.
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.
Ok, I have lots to say here. If you want to unilaterally declare "no rows", I understand and will do that, but before I do that, I just want to explain myself a little bit here, because I much prefer row to index.
You mentioned that using row names makes it hard to untie it from Table storage. That's fair. I don't have a good answer for that.
But, in the very common "ecs as a spreadsheet" example, this makes a lot of sense. For people learning the ecs way, I think this name is much more approachable than index. Index naming ties the high level concept to the implementation of the Entities collection.
And that implementation is not unlikely to change. For example, with entity paging, this "index" becomes a key into a 2-layer array map. You mentioned elsewhere, I assume for the sake of argument, that Entity might become a UUID, and that should be possible without needing to change names. In that case, the EntityRow would be the key in a hashmap, not an index in an array.
I guess what I'm saying is that using the index name ties the name to its implementation details, an index in an array, rather than the high-level concept, a row in a spreadsheet. And that seems contrary to your motivations behind other naming suggestions, most of which I agree with.
Again, I'll happily rename everything to index. I just don't want that to come back to bite us latter if we do entity paging, for example, and people start wondering "Why is this called index? It's not an index into anything.".
What do you think?
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 remain unconvinced. The "ECS is just tables" statement is perhaps a helpful lie, but it is not the truth of the matter. Bevy ECS is partially tables and partially other things, and that will continue to be true. Given that it is partially real tables, pretending everything is tables makes the internals unnecessarily confusing, and it makes communicating how the system actually works to developers more challenging. That being said, even if we were to choose to tell that lie, I'll assert that this naming scheme is doing it wrong.
First: what is a "row" in a table? It is a specific entry in the table. A "row identifier" is a unique name for the row that can be stored (ex: the "first" row might be row A, row 0, or row 1, depending on the naming scheme). In our case, our naming scheme is "array indices". If we are storing the "row identifier", we are storing the array index of the row. If I have a field or type called row, I expect it to be the "row identifier".
Calling it Entity::row, makes no sense because it is not the row identifier. It is a unique index that identifies the entity, not the row it is stored in. The row an entity is stored in can be constantly changing, while the entity's index remains unchanged. That index points to a location in an array that stores arbitrary information about that entity, which includes the archetype it is stored in, the table, etc. There is a world where that entity metadata stores multiple table rows, if we decide to support grouping some sets of components into separate tables (for performance reasons ... Sander and I have discussed this at various points and Flecs might actually already support it).
Even in the "ECS is just tables" / "ECS is just a database" world, Entity::index would be the "primary key" stored as a column in a row, not the "row":
Table
| Entity | ComponentA | ComponentB |
|---|---|---|
| 4 | A("hello") | B("world") |
| 9 | A("foo") | B("bar") |
Sparse Set
| Entity | ComponentC |
|---|---|
| 9 | C("I'm sparsely stored!") |
Where that primary key then has an acceleration structure to find it's locations (filling exactly the same role that Entities fills)
The index name ties the name to its implementation details
Again, I'll happily rename everything to index. I just don't want that to come back to bite us latter if we do entity paging, for example, and people start wondering "Why is this called index? It's not an index into anything.".
I agree that index is an implementation detail. If someone is relying on Entity::index, they are relying on a Bevy ECS implementation detail. If we change that to include paging, or to use a UUID, the consumer of index in many cases will need to contend with that. A contract (and perhaps naming) change is warranted.
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.
Similarly to construct vs spawn_at, I still very much disagree here but am also ok with you're decision.
For one, I think you're conflating tables + sparse sets as implementation and ecs as a database with "think of an ecs as a spreadsheet." The row in the spreadsheet has nothing to do with how the spreadsheet is represented in memory or how it is implemented. The "row in a spreadsheet" is purely allegorical to help users. I think it is the most common mental framework for thinking of an ecs (again, not in implementation but in concept). At least that's how I was introduced to it and still think about ecs philosophy. I think that's very common, and is the typical mental framework that users are coming from. The rest of the ecs is just figuring out how to make reading the spreadsheet and finding the components of a row in the spreadsheet faster than it has any right to be.
I'm also still concerned about tying the names to implementation details instead of higher level concepts, especially when you've argued for the exact opposite for other names. (I don't mean to attack you here. But I am confused.) Those names were more user-focused. Maybe that's the difference? IDK. Maybe I'm not understanding, but what's confusing me a little is that I'm not sure what criteria you're using to determine implementation-centered names vs concept-centered names. And that makes it much harder (for me) to follow your logic.
Anyway, I don't think I'm going to convince you, and that's ok.
If I understand correctly, you want this name in particular to reflect how Entities is implemented. And that means renaming to index for now and then later, with entity paging, maybe renaming again to "key" or "entry" or something.
Assuming that's correct, I'll finish this up either latter tonight or tomorrow. But do let me know otherwise. And I'd also still like some help (just for future reference and my own learning) understanding the criteria you're using to determine implementation-centered names vs concept-centered names. I don't have a good mental picture for that.
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 also still concerned about tying the names to implementation details instead of higher level concepts,
I'd also still like some help (just for future reference and my own learning) understanding the criteria you're using to determine implementation-centered names vs concept-centered names
The distinction is that Entity and Entity::index are at different levels of abstraction, and abstractions have to end somewhere. From my perspective, Entity should be an opaque unique primary key, which people can use as such without worrying about whats going on inside. The internals are an implementation detail, and therefore benefit from being functionally descriptive. People can reach in to look at what is inside, but they are just "internals", and generally should not be depended in user code.
This is of course an art and not a science. We could decide to abstract over the internals of Entity, with the goal to make them stable. But I don't really see the point of doing that. The cost of functional clarity and additional layers of abstraction isn't worth the stability, at that level, as we already have a higher level abstraction providing that stability.
I still very much disagree here but am also ok with you're decision.
Cool lets move forward with the "index' terminology, and cut the "ecs as spreadsheets" section for now. I'm not fully against using the "ECS is like a spreadsheet" angle as a teaching tool, but I still strongly object to the Entity == Row angle, and I really don't want to block this PR on that conversation.
crates/bevy_ecs/src/world/mod.rs
Outdated
| } | ||
| // The caller wants the entity to be left despawned and in the allocator. In this case, we can just skip the despawning part. | ||
| Err(EntityMutableFetchError::NotSpawned(EntityNotSpawnedError::RowNotSpawned(_))) => { | ||
| self.allocator.free(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.
If the entity is not spawned, that might be because it was already despawned elsewhere right? This would double free(), allowing it to be claimed more than once (bad!). Additionally, multiple calls to this would result in multiple free() calls. I think we should only pair free() directly with actual successful despawn operations.
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've added some comments to make this more clear, but I'm pretty sure this is actually correct.
If the entity was previously despawned, its generation would be incremented, so the error variant would be Invalid, not NotSpawned. The only way this can happen is if despawn_no_free is was called, and then this is called to say "despawn it", as in, make it not spawned and freed. In this case, it's already not spawned, but it isn't freed, so we just free it. This will never double free from someone despawning the same entity twice.
The only way this could behave weird is if there is some nasty id aliasing, and something despawns 0vu32::MAX making it 0v0 and then something tries to despawn the original 0v0, which would trigger a double free. I don't think that's a big issue though because id aliasing is really rare, already can trigger lots of bugs, and is heavily documented with ways to avoid it altogether.
So if you want to change the behavior so that if despawn_no_free is called, the id can only be manually freed, I'm happy to do that. But I don't think this implementation is wrong, unless you want to change what it does.
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.
So if you want to change the behavior so that if despawn_no_free is called, the id can only be manually freed, I'm happy to do that
Yeah I think that is the move / that is the correct mental model. despawn is both a "despawn" operation and a "free" operation. The caller is asking to do both. We cannot despawn, so that should result in an error, just like it does in the other case.
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
Worldmust 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_atclunky and slow, leading to its removal, despite some users still having valid need of it.So the goal of this PR is to:
Entitiesfrom entity allocation to make room for other allocators and resolvealloc_atissues.reserveandflushpatterns toallocandconstructpatterns.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
Entityids relate to conceptual entities. AnEntitynow exists if its generation matches that of its row. AnEntitythat has the right generation for its row will claim to exist, even if it is not constructed. This means, for example, anEntitymanually constructed with a large index and generation of 0 will exist if it has not been allocated yet.Entitiesis separate from the allocatorThis pr separates entity allocation from
Entities.Entitiesis now only focused on tracking entity metadata, etc. The newEntitiesAllocatoronWorldmanages all allocations. This forcesEntitiesto 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
Entitiesin a new way. Previously, theEntitiesset the rules for what entities are valid and what entities are not. Now, it has no way of knowing. Instead, interaction withEntitiesare more like declaring some information for it to track than changing some information it was already tracking. To reflect this,sethas been split up intodeclareandupdate.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_atare 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
Entitythat 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
Entitydoesn'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!