bevy/release-content/release-notes/entity-spawn-ticks.md
urben1680 732b2e0c79
Track spawn Tick of entities, offer methods, query data SpawnDetails and query filter Spawned (#19047)
# Objective

In my own project I was encountering the issue to find out which
entities were spawned after applying commands. I began maintaining a
vector of all entities with generational information before and after
applying the command and diffing it. This was awfully complicated though
and has no constant complexity but grows with the number of entities.

## Solution

Looking at `EntyMeta` it seemed obvious to me that struct can track the
tick just as it does with `MaybeLocation`, updated from the same call.
After that it became almost a given to also introduce query data
`SpawnDetails` which offers methods to get the spawn tick and location,
and query filter `Spawned` that filters entities out that were not
spawned since the last run.

## Testing

I expanded a few tests and added new ones, though maybe I forgot a group
of tests that should be extended too. I basically searched `bevy_ecs`
for mentions of `Changed` and `Added` to see where the tests and docs
are.

Benchmarks of spawn/despawn can be found
[here](https://github.com/bevyengine/bevy/pull/19047#issuecomment-2852181374).

---

## Showcase

From the added docs, systems with equal complexity since the filter is
not archetypal:
```rs
fn system1(q: Query<Entity, Spawned>) {
    for entity in &q { /* entity spawned */ }
}

fn system2(query: Query<(Entity, SpawnDetails)>) {
    for (entity, spawned) in &query {
        if spawned.is_spawned() { /* entity spawned */ }
    }
}
```

`SpawnedDetails` has a few more methods:

```rs
fn print_spawn_details(query: Query<(Entity, SpawnDetails)>) {
    for (entity, spawn_details) in &query {
        if spawn_details.is_spawned() {
            print!("new ");
        }
        println!(
            "entity {:?} spawned at {:?} by {:?}",
            entity,
            spawn_details.spawned_at(),
            spawn_details.spawned_by()
        );        
    }
}
```

## Changes

No public api was changed, I only added to it. That is why I added no
migration guide.

- query data `SpawnDetails`
- query filter `Spawned`
- method `Entities::entity_get_spawned_or_despawned_at`
- method `EntityRef::spawned_at`
- method `EntityMut::spawned_at`
- method `EntityWorldMut::spawned_at`
- method `UnsafeEntityCell::spawned_at`
- method `FilteredEntityRef::spawned_at`
- method `FilteredEntityMut::spawned_at`
- method `EntityRefExcept::spawned_at`
- method `EntityMutExcept::spawned_at`

---------

Co-authored-by: Eagster <79881080+ElliottjPierce@users.noreply.github.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2025-05-08 14:57:33 +00:00

84 lines
3.5 KiB
Markdown

---
title: Entity Spawn Ticks
authors: ["@urben1680"]
pull_requests: [19047]
---
Keeping track which entities have been spawned since the last time a system ran could only be done indirectly by inserting marker components and do your logic on entities that match an `Added<MyMarker>` filter or in `MyMarker`'s `on_add` hook.
This has the issue however that not every add reacts on a spawn but also on insertions at existing entities. Sometimes you cannot even add your marker because the spawn call is hidden in some non-public API.
The new `SpawnDetails` query data and `Spawned` query filter enable you to find recently spawned entities without any marker components.
## `SpawnDetails`
Use this in your query when you want to get information about the entity's spawn. You might want to do that for debug purposes, using the struct's `Debug` implementation.
You can also get specific information via methods. The following example prints the entity id (prefixed with "new" if it showed up for the first time), the `Tick` it spawned at and, if the `track_location` feature is activated, the source code location where it was spawned. Said feature is not enabled by default because it comes with a runtime cost.
```rs
fn print_spawn_details(query: Query<(Entity, SpawnDetails)>) {
for (entity, spawn_details) in &query {
if spawn_details.is_spawned() {
print!("new ");
}
print!(
"entity {:?} spawned at {:?}",
entity,
spawn_details.spawned_at()
);
match spawn_details.spawned_by().into_option() {
Some(location) => println!(" by {:?}", location),
None => println!()
}
}
}
```
## `Spawned`
Use this filter in your query if you are only interested in entities that were spawned after the last time your system ran.
Note that this, like `Added<T>` and `Changed<T>`, is a non-archetypal filter. This means that your query could still go through millions of entities without yielding any recently spawned ones. Unlike filters like `With<T>` which can easily skip all entities that do not have `T` without checking them one-by-one.
Because of this, these systems have roughly the same performance:
```rs
fn system1(q: Query<Entity, Spawned>) {
for entity in &q { /* entity spawned */ }
}
fn system2(query: Query<(Entity, SpawnDetails)>) {
for (entity, spawned) in &query {
if spawned.is_spawned() { /* entity spawned */ }
}
}
```
## Getter methods
Getting around this weakness of non-archetypal filters can be to check only specific entities for their spawn tick: The method `spawned_at` was added to all entity pointer structs, such as `EntityRef`, `EntityMut` and `EntityWorldMut`.
In this example we want to filter for entities that were spawned after a certain `tick`:
```rs
fn filter_spawned_after(
entities: impl IntoIterator<Item = Entity>,
world: &World,
tick: Tick,
) -> impl Iterator<Item = Entity> {
let now = world.last_change_tick();
entities.into_iter().filter(move |entity| world
.entity(*entity)
.spawned_at()
.is_newer_than(tick, now)
)
}
```
---
The tick is stored in `Entities`. It's method `entity_get_spawned_or_despawned_at` not only returns when a living entity spawned at, it also returns when a despawned entity found it's bitter end.
Note however that despawned entities can be replaced by bevy at any following spawn. Then this method returns `None` for the despawned entity. The same is true if the entity is not even spawned yet, only allocated.