Skip to content

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

Open
wants to merge 52 commits into
base: main
Choose a base branch
from

Conversation

ElliottjPierce
Copy link
Contributor

@ElliottjPierce ElliottjPierce commented May 31, 2025

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 like Entities::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:

  • Decouple Entities from entity allocation to make room for other allocators and resolve alloc_at issues.
  • Decouple entity allocation from spawning to make reservation a moot point.
  • Introduce constructing and destructing entities, in addition to spawn/despawn.
  • Change reserve and flush patterns to alloc and construct 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. An Entity now exists if its generation matches that of its row. An Entity that has the right generation for its row will claim to exist, even if it is not constructed. This means, for example, an Entity manually constructed with a large index and generation of 0 will exist if it has not been allocated yet.

Entities is separate from the allocator

This pr separates entity allocation from Entities. Entities is now only focused on tracking entity metadata, etc. The new EntitiesAllocator on World manages all allocations. This forces Entities 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 the World, 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, the Entities set the rules for what entities are valid and what entities are not. Now, it has no way of knowing. Instead, interaction with Entities 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 into declare and update.

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 for alloc_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

entity row lifecycle

(Thanks @ItsDoot)

Testing

  • CI
  • Some new tests
  • A few deleted (no longer applicable) tests
  • If you see something you think should have a test case, I'll gladly add it.

Showcase

Here's an example of constructing and destructing

let e4 = world.spawn_null();
world
    .entity_mut(e4)
    .construct((TableStored("junk"), A(0)))
    .unwrap()
    .destruct()
    .construct((TableStored("def"), A(456)))
    .unwrap();

Future Work

  • More expansive docs. This should definitely should be done, but I'd rather do that in a future pr to separate writing review from code review. If you have more ideas for how to introduce users to these concepts, I'd like to see them. As it is, we don't do a very good job of explaining entities to users. Ex: Entity doesn't always correspond to a conceptual entity.
  • Try to remove panics from EntityWorldMut. There is (and was) a lot of assuming the entity is constructed there (was assuming it was not despawned).
  • A lot of names are still centered around spawn/despawn, which is more user-friendly than construct/destruct but less precise. Might be worth changing these over.
  • Making a centralized bundle despawner would make sense now.
  • Of course, build on this for remote reservation and, potentially, for paged entities.

Performance

Benchmarks
critcmp main pr19451 -t 1
group                                                                                                     main                                     pr19451
-----                                                                                                     ----                                     -------
add_remove/sparse_set                                                                                     1.13    594.7±6.80µs        ? ?/sec      1.00    527.4±8.01µs        ? ?/sec
add_remove/table                                                                                          1.08   799.6±15.53µs        ? ?/sec      1.00   739.7±15.10µs        ? ?/sec
add_remove_big/sparse_set                                                                                 1.10    614.6±6.50µs        ? ?/sec      1.00   557.0±19.04µs        ? ?/sec
add_remove_big/table                                                                                      1.03      2.8±0.01ms        ? ?/sec      1.00      2.7±0.02ms        ? ?/sec
added_archetypes/archetype_count/100                                                                      1.01     30.9±0.50µs        ? ?/sec      1.00     30.5±0.44µs        ? ?/sec
added_archetypes/archetype_count/1000                                                                     1.00   638.0±19.77µs        ? ?/sec      1.03   657.0±73.61µs        ? ?/sec
added_archetypes/archetype_count/10000                                                                    1.02      5.5±0.14ms        ? ?/sec      1.00      5.4±0.09ms        ? ?/sec
all_added_detection/50000_entities_ecs::change_detection::Sparse                                          1.02     47.9±1.22µs        ? ?/sec      1.00     46.8±0.40µs        ? ?/sec
all_added_detection/50000_entities_ecs::change_detection::Table                                           1.02     45.4±1.89µs        ? ?/sec      1.00     44.6±0.78µs        ? ?/sec
build_schedule/1000_schedule                                                                              1.02   942.6±11.53ms        ? ?/sec      1.00   925.2±10.35ms        ? ?/sec
build_schedule/100_schedule                                                                               1.01      5.8±0.12ms        ? ?/sec      1.00      5.7±0.12ms        ? ?/sec
build_schedule/100_schedule_no_constraints                                                                1.03   803.1±28.93µs        ? ?/sec      1.00   781.1±50.11µs        ? ?/sec
build_schedule/500_schedule_no_constraints                                                                1.00      5.6±0.31ms        ? ?/sec      1.08      6.0±0.27ms        ? ?/sec
busy_systems/01x_entities_03_systems                                                                      1.00     24.4±1.35µs        ? ?/sec      1.01     24.7±1.35µs        ? ?/sec
busy_systems/03x_entities_03_systems                                                                      1.00     38.1±1.70µs        ? ?/sec      1.04     39.7±1.49µs        ? ?/sec
busy_systems/03x_entities_09_systems                                                                      1.01    111.4±2.27µs        ? ?/sec      1.00    109.9±2.46µs        ? ?/sec
busy_systems/03x_entities_15_systems                                                                      1.00    174.8±2.56µs        ? ?/sec      1.01    176.6±4.22µs        ? ?/sec
contrived/03x_entities_09_systems                                                                         1.00     59.0±2.92µs        ? ?/sec      1.01     59.8±3.03µs        ? ?/sec
contrived/03x_entities_15_systems                                                                         1.00     97.5±4.87µs        ? ?/sec      1.01     98.8±4.69µs        ? ?/sec
contrived/05x_entities_09_systems                                                                         1.00     75.3±3.76µs        ? ?/sec      1.01     76.4±4.11µs        ? ?/sec
despawn_world/10000_entities                                                                              1.32    344.8±4.47µs        ? ?/sec      1.00    261.4±4.91µs        ? ?/sec
despawn_world/100_entities                                                                                1.22      4.3±0.04µs        ? ?/sec      1.00      3.5±0.54µs        ? ?/sec
despawn_world/1_entities                                                                                  1.01    169.6±7.88ns        ? ?/sec      1.00   167.8±11.45ns        ? ?/sec
despawn_world_recursive/10000_entities                                                                    1.20  1723.0±53.82µs        ? ?/sec      1.00  1437.0±26.11µs        ? ?/sec
despawn_world_recursive/100_entities                                                                      1.16     17.9±0.10µs        ? ?/sec      1.00     15.5±0.16µs        ? ?/sec
despawn_world_recursive/1_entities                                                                        1.01   372.8±15.68ns        ? ?/sec      1.00   367.7±16.90ns        ? ?/sec
ecs::entity_cloning::hierarchy_many/clone                                                                 1.03   227.9±24.67µs 1559.9 KElem/sec    1.00   221.1±29.74µs 1607.8 KElem/sec
ecs::entity_cloning::hierarchy_many/reflect                                                               1.00   406.2±23.46µs 875.2 KElem/sec     1.02   413.9±22.45µs 858.9 KElem/sec
ecs::entity_cloning::hierarchy_tall/clone                                                                 1.01     12.2±0.34µs  4.0 MElem/sec      1.00     12.0±1.41µs  4.1 MElem/sec
ecs::entity_cloning::hierarchy_tall/reflect                                                               1.02     15.3±0.39µs  3.2 MElem/sec      1.00     15.0±2.14µs  3.2 MElem/sec
ecs::entity_cloning::single/clone                                                                         1.02  659.0±100.01ns 1481.8 KElem/sec    1.00  643.3±101.49ns 1517.9 KElem/sec
ecs::entity_cloning::single/reflect                                                                       1.03  1135.2±72.17ns 860.2 KElem/sec     1.00  1098.3±65.99ns 889.1 KElem/sec
empty_archetypes/for_each/10                                                                              1.02      8.1±0.57µs        ? ?/sec      1.00      8.0±0.37µs        ? ?/sec
empty_archetypes/for_each/100                                                                             1.01      8.1±0.34µs        ? ?/sec      1.00      8.1±0.28µs        ? ?/sec
empty_archetypes/for_each/1000                                                                            1.03      8.4±0.25µs        ? ?/sec      1.00      8.2±0.29µs        ? ?/sec
empty_archetypes/iter/100                                                                                 1.01      8.1±0.29µs        ? ?/sec      1.00      8.0±0.34µs        ? ?/sec
empty_archetypes/iter/1000                                                                                1.02      8.5±0.31µs        ? ?/sec      1.00      8.4±0.62µs        ? ?/sec
empty_archetypes/iter/10000                                                                               1.01     10.6±1.22µs        ? ?/sec      1.00     10.5±0.49µs        ? ?/sec
empty_archetypes/par_for_each/10                                                                          1.01      8.8±0.49µs        ? ?/sec      1.00      8.7±0.31µs        ? ?/sec
empty_archetypes/par_for_each/100                                                                         1.00      8.7±0.48µs        ? ?/sec      1.04      9.0±0.34µs        ? ?/sec
empty_archetypes/par_for_each/10000                                                                       1.01     21.2±0.41µs        ? ?/sec      1.00     20.9±0.44µs        ? ?/sec
empty_commands/0_entities                                                                                 1.72      3.7±0.01ns        ? ?/sec      1.00      2.1±0.02ns        ? ?/sec
empty_systems/100_systems                                                                                 1.00     82.9±3.29µs        ? ?/sec      1.07     88.3±3.77µs        ? ?/sec
empty_systems/2_systems                                                                                   1.01      8.2±0.71µs        ? ?/sec      1.00      8.2±0.38µs        ? ?/sec
empty_systems/4_systems                                                                                   1.00      8.2±0.72µs        ? ?/sec      1.03      8.4±0.71µs        ? ?/sec
entity_hash/entity_set_build/10000                                                                        1.10     45.9±1.60µs 207.7 MElem/sec     1.00     41.6±0.39µs 229.0 MElem/sec
entity_hash/entity_set_build/3162                                                                         1.06     12.7±0.77µs 236.7 MElem/sec     1.00     12.0±0.75µs 250.6 MElem/sec
entity_hash/entity_set_lookup_hit/10000                                                                   1.02     14.5±0.30µs 658.3 MElem/sec     1.00     14.2±0.07µs 672.6 MElem/sec
entity_hash/entity_set_lookup_hit/3162                                                                    1.01      4.4±0.03µs 682.7 MElem/sec     1.00      4.4±0.01µs 691.3 MElem/sec
entity_hash/entity_set_lookup_miss_gen/10000                                                              1.01     61.3±4.12µs 155.6 MElem/sec     1.00     60.6±1.47µs 157.3 MElem/sec
entity_hash/entity_set_lookup_miss_gen/3162                                                               1.00      9.5±0.02µs 316.3 MElem/sec     1.01      9.7±0.88µs 311.7 MElem/sec
entity_hash/entity_set_lookup_miss_id/100                                                                 1.00    145.5±1.49ns 655.4 MElem/sec     1.03    149.8±1.59ns 636.7 MElem/sec
entity_hash/entity_set_lookup_miss_id/10000                                                               1.85     63.9±3.57µs 149.3 MElem/sec     1.00     34.6±3.81µs 275.8 MElem/sec
entity_hash/entity_set_lookup_miss_id/316                                                                 1.00    562.0±9.58ns 536.2 MElem/sec     1.02    573.9±1.27ns 525.1 MElem/sec
entity_hash/entity_set_lookup_miss_id/3162                                                                1.03      9.1±0.10µs 330.7 MElem/sec     1.00      8.9±0.24µs 339.0 MElem/sec
event_propagation/four_event_types                                                                        1.12    541.5±3.84µs        ? ?/sec      1.00    482.7±4.64µs        ? ?/sec
event_propagation/single_event_type                                                                       1.07   769.5±10.21µs        ? ?/sec      1.00   715.9±15.16µs        ? ?/sec
event_propagation/single_event_type_no_listeners                                                          1.56    393.4±2.89µs        ? ?/sec      1.00    251.4±3.68µs        ? ?/sec
events_iter/size_16_events_100                                                                            1.01     64.0±0.18ns        ? ?/sec      1.00     63.4±0.23ns        ? ?/sec
events_iter/size_4_events_100                                                                             1.02     64.8±0.90ns        ? ?/sec      1.00     63.4±0.24ns        ? ?/sec
events_iter/size_4_events_1000                                                                            1.01    586.5±8.00ns        ? ?/sec      1.00    579.1±4.93ns        ? ?/sec
events_send/size_16_events_100                                                                            1.00   142.7±24.34ns        ? ?/sec      1.03   147.1±28.36ns        ? ?/sec
events_send/size_16_events_10000                                                                          1.01     12.2±0.13µs        ? ?/sec      1.00     12.1±0.12µs        ? ?/sec
fake_commands/10000_commands                                                                              1.43     63.3±8.21µs        ? ?/sec      1.00     44.1±0.16µs        ? ?/sec
fake_commands/1000_commands                                                                               1.40      6.2±0.01µs        ? ?/sec      1.00      4.4±0.02µs        ? ?/sec
fake_commands/100_commands                                                                                1.38    629.4±1.69ns        ? ?/sec      1.00    457.1±0.84ns        ? ?/sec
few_changed_detection/50000_entities_ecs::change_detection::Table                                         1.00     57.7±0.86µs        ? ?/sec      1.07     61.6±1.19µs        ? ?/sec
few_changed_detection/5000_entities_ecs::change_detection::Sparse                                         1.05      5.4±0.53µs        ? ?/sec      1.00      5.1±0.56µs        ? ?/sec
few_changed_detection/5000_entities_ecs::change_detection::Table                                          1.00      4.3±0.30µs        ? ?/sec      1.18      5.1±0.35µs        ? ?/sec
insert_commands/insert                                                                                    1.11   402.5±10.75µs        ? ?/sec      1.00    363.6±8.07µs        ? ?/sec
insert_commands/insert_batch                                                                              1.00    174.9±3.03µs        ? ?/sec      1.02    177.9±5.74µs        ? ?/sec
insert_simple/base                                                                                        1.04   564.1±23.01µs        ? ?/sec      1.00   544.3±60.70µs        ? ?/sec
insert_simple/unbatched                                                                                   1.32  929.3±180.10µs        ? ?/sec      1.00  704.1±132.88µs        ? ?/sec
iter_fragmented/base                                                                                      1.02    280.0±2.86ns        ? ?/sec      1.00    274.0±4.85ns        ? ?/sec
iter_fragmented/foreach                                                                                   1.00     97.3±0.42ns        ? ?/sec      1.03    100.6±3.44ns        ? ?/sec
iter_fragmented/foreach_wide                                                                              1.04      2.7±0.04µs        ? ?/sec      1.00      2.6±0.03µs        ? ?/sec
iter_fragmented_sparse/base                                                                               1.00      5.6±0.05ns        ? ?/sec      1.04      5.8±0.06ns        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10000_entities_ecs::change_detection::Sparse    1.00   737.7±27.38µs        ? ?/sec      1.01   747.5±30.01µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10000_entities_ecs::change_detection::Table     1.02   678.3±25.13µs        ? ?/sec      1.00   662.1±19.63µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Sparse     1.09     76.0±9.35µs        ? ?/sec      1.00     70.0±3.29µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Table      1.03     64.7±3.40µs        ? ?/sec      1.00     62.8±1.80µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_100_entities_ecs::change_detection::Table       1.02      7.6±0.12µs        ? ?/sec      1.00      7.5±0.16µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10_entities_ecs::change_detection::Sparse       1.00  1003.5±12.38ns        ? ?/sec      1.01  1013.7±32.64ns        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_10_entities_ecs::change_detection::Sparse        1.03   187.1±21.18ns        ? ?/sec      1.00   181.9±22.86ns        ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_10_entities_ecs::change_detection::Sparse         1.00     52.8±8.19ns        ? ?/sec      1.03     54.3±8.06ns        ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_10_entities_ecs::change_detection::Table          1.00     46.8±2.23ns        ? ?/sec      1.03     48.0±2.48ns        ? ?/sec
no_archetypes/system_count/0                                                                              1.00     16.3±0.17ns        ? ?/sec      1.02     16.6±0.16ns        ? ?/sec
no_archetypes/system_count/100                                                                            1.02    851.5±9.32ns        ? ?/sec      1.00    832.9±7.93ns        ? ?/sec
none_changed_detection/5000_entities_ecs::change_detection::Sparse                                        1.00      3.4±0.04µs        ? ?/sec      1.02      3.5±0.05µs        ? ?/sec
nonempty_spawn_commands/10000_entities                                                                    1.89    261.1±6.99µs        ? ?/sec      1.00    137.8±8.47µs        ? ?/sec
nonempty_spawn_commands/1000_entities                                                                     1.90     26.4±3.18µs        ? ?/sec      1.00     13.9±2.38µs        ? ?/sec
nonempty_spawn_commands/100_entities                                                                      1.87      2.6±0.07µs        ? ?/sec      1.00  1388.8±97.31ns        ? ?/sec
observe/trigger_simple                                                                                    1.09    347.5±1.51µs        ? ?/sec      1.00    317.7±2.62µs        ? ?/sec
observe/trigger_targets_simple/10000_entity                                                               1.04   696.5±15.50µs        ? ?/sec      1.00   672.0±13.88µs        ? ?/sec
par_iter_simple/with_0_fragment                                                                           1.01     34.4±0.51µs        ? ?/sec      1.00     33.9±0.53µs        ? ?/sec
par_iter_simple/with_1000_fragment                                                                        1.04     45.5±0.93µs        ? ?/sec      1.00     43.9±1.85µs        ? ?/sec
par_iter_simple/with_100_fragment                                                                         1.03     36.2±0.50µs        ? ?/sec      1.00     35.1±0.44µs        ? ?/sec
par_iter_simple/with_10_fragment                                                                          1.03     37.5±0.97µs        ? ?/sec      1.00     36.5±0.74µs        ? ?/sec
param/combinator_system/8_dyn_params_system                                                               1.00     10.4±0.73µs        ? ?/sec      1.01     10.5±0.79µs        ? ?/sec
param/combinator_system/8_piped_systems                                                                   1.05      8.0±0.65µs        ? ?/sec      1.00      7.6±0.57µs        ? ?/sec
query_get/50000_entities_sparse                                                                           1.06    136.7±0.35µs        ? ?/sec      1.00    128.6±0.44µs        ? ?/sec
query_get_many_10/50000_calls_sparse                                                                      1.02  1649.4±77.80µs        ? ?/sec      1.00  1614.4±78.91µs        ? ?/sec
query_get_many_2/50000_calls_sparse                                                                       1.00    191.3±3.66µs        ? ?/sec      1.01    193.3±0.75µs        ? ?/sec
query_get_many_2/50000_calls_table                                                                        1.00    243.9±0.55µs        ? ?/sec      1.05    257.2±8.62µs        ? ?/sec
query_get_many_5/50000_calls_sparse                                                                       1.00    585.9±7.70µs        ? ?/sec      1.03    600.6±5.99µs        ? ?/sec
query_get_many_5/50000_calls_table                                                                        1.00    673.7±7.44µs        ? ?/sec      1.07   722.3±10.77µs        ? ?/sec
run_condition/no/1000_systems                                                                             1.00     23.7±0.06µs        ? ?/sec      1.06     25.1±0.07µs        ? ?/sec
run_condition/no/100_systems                                                                              1.00   1460.5±4.28ns        ? ?/sec      1.03   1510.1±3.69ns        ? ?/sec
run_condition/no/10_systems                                                                               1.00    201.5±0.53ns        ? ?/sec      1.04    209.1±2.37ns        ? ?/sec
run_condition/yes/1000_systems                                                                            1.00  1225.7±22.58µs        ? ?/sec      1.02  1253.7±24.90µs        ? ?/sec
run_condition/yes/100_systems                                                                             1.02     89.4±3.43µs        ? ?/sec      1.00     88.0±3.96µs        ? ?/sec
run_condition/yes_using_query/1000_systems                                                                1.00  1288.3±26.57µs        ? ?/sec      1.03  1323.0±24.73µs        ? ?/sec
run_condition/yes_using_query/100_systems                                                                 1.00    108.8±2.51µs        ? ?/sec      1.03    112.3±3.09µs        ? ?/sec
run_condition/yes_using_resource/100_systems                                                              1.03     99.0±3.37µs        ? ?/sec      1.00     96.2±4.80µs        ? ?/sec
run_empty_schedule/MultiThreaded                                                                          1.03     15.3±0.10ns        ? ?/sec      1.00     14.9±0.03ns        ? ?/sec
run_empty_schedule/Simple                                                                                 1.01     15.2±0.15ns        ? ?/sec      1.00     15.0±0.25ns        ? ?/sec
sized_commands_0_bytes/10000_commands                                                                     1.57     52.6±0.41µs        ? ?/sec      1.00     33.5±0.10µs        ? ?/sec
sized_commands_0_bytes/1000_commands                                                                      1.57      5.3±0.01µs        ? ?/sec      1.00      3.4±0.00µs        ? ?/sec
sized_commands_0_bytes/100_commands                                                                       1.56    536.5±4.83ns        ? ?/sec      1.00    343.6±1.12ns        ? ?/sec
sized_commands_12_bytes/10000_commands                                                                    1.22     63.0±0.53µs        ? ?/sec      1.00     51.5±6.06µs        ? ?/sec
sized_commands_12_bytes/1000_commands                                                                     1.25      5.7±0.01µs        ? ?/sec      1.00      4.6±0.05µs        ? ?/sec
sized_commands_12_bytes/100_commands                                                                      1.27    579.3±1.28ns        ? ?/sec      1.00    455.4±0.85ns        ? ?/sec
sized_commands_512_bytes/10000_commands                                                                   1.11   248.4±85.81µs        ? ?/sec      1.00   224.3±52.11µs        ? ?/sec
sized_commands_512_bytes/1000_commands                                                                    1.09     22.8±0.18µs        ? ?/sec      1.00     21.0±0.17µs        ? ?/sec
sized_commands_512_bytes/100_commands                                                                     1.13  1852.2±11.21ns        ? ?/sec      1.00   1635.3±4.91ns        ? ?/sec
spawn_commands/10000_entities                                                                             1.04   844.2±11.96µs        ? ?/sec      1.00   811.5±13.25µs        ? ?/sec
spawn_commands/1000_entities                                                                              1.05     84.9±3.66µs        ? ?/sec      1.00     80.5±4.13µs        ? ?/sec
spawn_commands/100_entities                                                                               1.06      8.6±0.12µs        ? ?/sec      1.00      8.1±0.12µs        ? ?/sec
spawn_world/10000_entities                                                                                1.03   413.2±25.20µs        ? ?/sec      1.00   400.9±49.97µs        ? ?/sec
spawn_world/100_entities                                                                                  1.02      4.1±0.62µs        ? ?/sec      1.00      4.1±0.69µs        ? ?/sec
spawn_world/1_entities                                                                                    1.04     42.2±3.23ns        ? ?/sec      1.00     40.6±6.81ns        ? ?/sec
world_entity/50000_entities                                                                               1.18     88.3±0.42µs        ? ?/sec      1.00     74.7±0.16µs        ? ?/sec
world_get/50000_entities_sparse                                                                           1.02    182.2±0.32µs        ? ?/sec      1.00    179.5±0.84µs        ? ?/sec
world_get/50000_entities_table                                                                            1.01    198.3±0.46µs        ? ?/sec      1.00    196.2±0.63µs        ? ?/sec
world_query_for_each/50000_entities_sparse                                                                1.00     32.7±0.12µs        ? ?/sec      1.01     33.1±0.46µs        ? ?/sec

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!

@ElliottjPierce ElliottjPierce added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events D-Complex Quite challenging from either a design or technical perspective. Ask for help! X-Controversial There is active debate or serious implications around merging this PR labels May 31, 2025
@ElliottjPierce ElliottjPierce marked this pull request as draft May 31, 2025 19:54
@alice-i-cecile
Copy link
Member

IMO, we it's very valuable to expose construct/destruct as well as spawn/despawn. I think this should be public for two reasons:

Fully agree!

This doesn't introduce any new errors or new ways for an entity to be invalid, not technically. An entity existing but not being constructed is exactly the same error as trying to use a reserved entity before flushing the world. This pr just reframes that existing error from what was Location::INVALID etc paired with an "index out of bounds" error to the new EntityNotConstructedError. I wouldn't be against hiding that error kind inside EntityDoesNotExistError, but I think the distinction would be useful to users, especially if they are making use of construct/destruct directly (ex for multiplayer, consecutive ids for tile map entities, custom entity allocator, etc)

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 very open to name changes. For a lot of things, the functionality tracks construct/destruct, but the names are still spawn/despawn. Do we want names to be precise (change them to construct) or intuitive to new users (keep them spawn based)?

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.

//! 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.
Copy link
Contributor

@Carter0 Carter0 Jun 21, 2025

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!

Copy link
Member

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.
Copy link
Contributor

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.
Copy link
Contributor

@Carter0 Carter0 Jun 21, 2025

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.

//! 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*.
Copy link
Contributor

@Carter0 Carter0 Jun 21, 2025

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.
Copy link
Contributor

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.
Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Contributor

@Carter0 Carter0 left a 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

@ElliottjPierce
Copy link
Contributor Author

@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 Entity to EntityId, we could use the term "entity" unambiguously, but as it is, I feel the need to clarify. For example "An Entity may no longer correspond to an entity," is a really confusing sentence. Do you have any ideas for less confusing ways to differentiate the type from the concept/"thing"/game object? (I have been toying with the idea of literally renaming Entity to EntityId, but that's probably too much of a migration headache sadly.)

@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.

@Carter0
Copy link
Contributor

Carter0 commented Jun 22, 2025

@Carter0 Thanks for the review! That feedback was incredibly valuable. I'll work on some improvements sometime tomorrow probably.

Glad it helped!

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 Entity to EntityId, we could use the term "entity" unambiguously, but as it is, I feel the need to clarify. For example "An Entity may no longer correspond to an entity," is a really confusing sentence. Do you have any ideas for less confusing ways to differentiate the type from the concept/"thing"/game object? (I have been toying with the idea of literally renaming Entity to EntityId, but that's probably too much of a migration headache sadly.)

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!

@alice-i-cecile
Copy link
Member

@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)?

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:

  • what is an [Entity], and why is it different from an "entity"
    • link out to the module docs very quickly
  • what does this module contain
  • an entity's lifecycle
  • the gory details of [Entity] as an identifier (generations and indexes)
  • how are entities stored in the World (covering Entities) and linking out to some of our component storage docs

With respect to conceptual entities:

  • this distinction is much clearer when you have spent more words on it!
  • I think the name "conceptual entity" is too abstract and intimidating
  • I think we can use the lens of the entity lifecycle when needed to talk about entities which are e.g. allocated but not spawned

Copy link
Contributor

@ItsDoot ItsDoot left a 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).
Copy link
Contributor

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.

Copy link
Contributor Author

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.");
Copy link
Contributor

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?

Copy link
Contributor Author

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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Controversial There is active debate or serious implications around merging this PR
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

4 participants