bevy/examples/ecs/dynamic.rs
Carter Anderson 3c8fae2390
Improved Entity Mapping and Cloning (#17687)
Fixes #17535

Bevy's approach to handling "entity mapping" during spawning and cloning
needs some work. The addition of
[Relations](https://github.com/bevyengine/bevy/pull/17398) both
[introduced a new "duplicate entities" bug when spawning scenes in the
scene system](#17535) and made the weaknesses of the current mapping
system exceedingly clear:

1. Entity mapping requires _a ton_ of boilerplate (implement or derive
VisitEntities and VisitEntitesMut, then register / reflect MapEntities).
Knowing the incantation is challenging and if you forget to do it in
part or in whole, spawning subtly breaks.
2. Entity mapping a spawned component in scenes incurs unnecessary
overhead: look up ReflectMapEntities, create a _brand new temporary
instance_ of the component using FromReflect, map the entities in that
instance, and then apply that on top of the actual component using
reflection. We can do much better.

Additionally, while our new [Entity cloning
system](https://github.com/bevyengine/bevy/pull/16132) is already pretty
great, it has some areas we can make better:

* It doesn't expose semantic info about the clone (ex: ignore or "clone
empty"), meaning we can't key off of that in places where it would be
useful, such as scene spawning. Rather than duplicating this info across
contexts, I think it makes more sense to add that info to the clone
system, especially given that we'd like to use cloning code in some of
our spawning scenarios.
* EntityCloner is currently built in a way that prioritizes a single
entity clone
* EntityCloner's recursive cloning is built to be done "inside out" in a
parallel context (queue commands that each have a clone of
EntityCloner). By making EntityCloner the orchestrator of the clone we
can remove internal arcs, improve the clarity of the code, make
EntityCloner mutable again, and simplify the builder code.
* EntityCloner does not currently take into account entity mapping. This
is necessary to do true "bullet proof" cloning, would allow us to unify
the per-component scene spawning and cloning UX, and ultimately would
allow us to use EntityCloner in place of raw reflection for scenes like
`Scene(World)` (which would give us a nice performance boost: fewer
archetype moves, less reflection overhead).

## Solution

### Improved Entity Mapping

First, components now have first-class "entity visiting and mapping"
behavior:

```rust
#[derive(Component, Reflect)]
#[reflect(Component)]
struct Inventory {
    size: usize,
    #[entities]
    items: Vec<Entity>,
}
```

Any field with the `#[entities]` annotation will be viewable and
mappable when cloning and spawning scenes.

Compare that to what was required before!

```rust
#[derive(Component, Reflect, VisitEntities, VisitEntitiesMut)]
#[reflect(Component, MapEntities)]
struct Inventory {
    #[visit_entities(ignore)]
    size: usize,
    items: Vec<Entity>,
}
```

Additionally, for relationships `#[entities]` is implied, meaning this
"just works" in scenes and cloning:

```rust
#[derive(Component, Reflect)]
#[relationship(relationship_target = Children)]
#[reflect(Component)]
struct ChildOf(pub Entity);
```

Note that Component _does not_ implement `VisitEntities` directly.
Instead, it has `Component::visit_entities` and
`Component::visit_entities_mut` methods. This is for a few reasons:

1. We cannot implement `VisitEntities for C: Component` because that
would conflict with our impl of VisitEntities for anything that
implements `IntoIterator<Item=Entity>`. Preserving that impl is more
important from a UX perspective.
2. We should not implement `Component: VisitEntities` VisitEntities in
the Component derive, as that would increase the burden of manual
Component trait implementors.
3. Making VisitEntitiesMut directly callable for components would make
it easy to invalidate invariants defined by a component author. By
putting it in the `Component` impl, we can make it harder to call
naturally / unavailable to autocomplete using `fn
visit_entities_mut(this: &mut Self, ...)`.

`ReflectComponent::apply_or_insert` is now
`ReflectComponent::apply_or_insert_mapped`. By moving mapping inside
this impl, we remove the need to go through the reflection system to do
entity mapping, meaning we no longer need to create a clone of the
target component, map the entities in that component, and patch those
values on top. This will make spawning mapped entities _much_ faster
(The default `Component::visit_entities_mut` impl is an inlined empty
function, so it will incur no overhead for unmapped entities).

### The Bug Fix

To solve #17535, spawning code now skips entities with the new
`ComponentCloneBehavior::Ignore` and
`ComponentCloneBehavior::RelationshipTarget` variants (note
RelationshipTarget is a temporary "workaround" variant that allows
scenes to skip these components. This is a temporary workaround that can
be removed as these cases should _really_ be using EntityCloner logic,
which should be done in a followup PR. When that is done,
`ComponentCloneBehavior::RelationshipTarget` can be merged into the
normal `ComponentCloneBehavior::Custom`).

### Improved Cloning

* `Option<ComponentCloneHandler>` has been replaced by
`ComponentCloneBehavior`, which encodes additional intent and context
(ex: `Default`, `Ignore`, `Custom`, `RelationshipTarget` (this last one
is temporary)).
* Global per-world entity cloning configuration has been removed. This
felt overly complicated, increased our API surface, and felt too
generic. Each clone context can have different requirements (ex: what a
user wants in a specific system, what a scene spawner wants, etc). I'd
prefer to see how far context-specific EntityCloners get us first.
* EntityCloner's internals have been reworked to remove Arcs and make it
mutable.
* EntityCloner is now directly stored on EntityClonerBuilder,
simplifying the code somewhat
* EntityCloner's "bundle scratch" pattern has been moved into the new
BundleScratch type, improving its usability and making it usable in
other contexts (such as future cross-world cloning code). Currently this
is still private, but with some higher level safe APIs it could be used
externally for making dynamic bundles
* EntityCloner's recursive cloning behavior has been "externalized". It
is now responsible for orchestrating recursive clones, meaning it no
longer needs to be sharable/clone-able across threads / read-only.
* EntityCloner now does entity mapping during clones, like scenes do.
This gives behavior parity and also makes it more generically useful.
* `RelatonshipTarget::RECURSIVE_SPAWN` is now
`RelationshipTarget::LINKED_SPAWN`, and this field is used when cloning
relationship targets to determine if cloning should happen recursively.
The new `LINKED_SPAWN` term was picked to make it more generically
applicable across spawning and cloning scenarios.

## Next Steps

* I think we should adapt EntityCloner to support cross world cloning. I
think this PR helps set the stage for that by making the internals
slightly more generalized. We could have a CrossWorldEntityCloner that
reuses a lot of this infrastructure.
* Once we support cross world cloning, we should use EntityCloner to
spawn `Scene(World)` scenes. This would yield significant performance
benefits (no archetype moves, less reflection overhead).

---------

Co-authored-by: eugineerd <70062110+eugineerd@users.noreply.github.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2025-02-06 22:13:41 +00:00

278 lines
10 KiB
Rust

#![expect(
unsafe_code,
reason = "Unsafe code is needed to work with dynamic components"
)]
//! This example show how you can create components dynamically, spawn entities with those components
//! as well as query for entities with those components.
use std::{alloc::Layout, collections::HashMap, io::Write, ptr::NonNull};
use bevy::{
ecs::{
component::{
ComponentCloneBehavior, ComponentDescriptor, ComponentId, ComponentInfo, StorageType,
},
query::QueryData,
world::FilteredEntityMut,
},
prelude::*,
ptr::{Aligned, OwningPtr},
};
const PROMPT: &str = "
Commands:
comp, c Create new components
spawn, s Spawn entities
query, q Query for entities
Enter a command with no parameters for usage.";
const COMPONENT_PROMPT: &str = "
comp, c Create new components
Enter a comma separated list of type names optionally followed by a size in u64s.
e.g. CompA 3, CompB, CompC 2";
const ENTITY_PROMPT: &str = "
spawn, s Spawn entities
Enter a comma separated list of components optionally followed by values.
e.g. CompA 0 1 0, CompB, CompC 1";
const QUERY_PROMPT: &str = "
query, q Query for entities
Enter a query to fetch and update entities
Components with read or write access will be displayed with their values
Components with write access will have their fields incremented by one
Accesses: 'A' with, '&A' read, '&mut A' write
Operators: '||' or, ',' and, '?' optional
e.g. &A || &B, &mut C, D, ?E";
fn main() {
let mut world = World::new();
let mut lines = std::io::stdin().lines();
let mut component_names = HashMap::<String, ComponentId>::new();
let mut component_info = HashMap::<ComponentId, ComponentInfo>::new();
println!("{PROMPT}");
loop {
print!("\n> ");
let _ = std::io::stdout().flush();
let Some(Ok(line)) = lines.next() else {
return;
};
if line.is_empty() {
return;
};
let Some((first, rest)) = line.trim().split_once(|c: char| c.is_whitespace()) else {
match &line.chars().next() {
Some('c') => println!("{COMPONENT_PROMPT}"),
Some('s') => println!("{ENTITY_PROMPT}"),
Some('q') => println!("{QUERY_PROMPT}"),
_ => println!("{PROMPT}"),
}
continue;
};
match &first[0..1] {
"c" => {
rest.split(',').for_each(|component| {
let mut component = component.split_whitespace();
let Some(name) = component.next() else {
return;
};
let size = match component.next().map(str::parse) {
Some(Ok(size)) => size,
_ => 0,
};
// Register our new component to the world with a layout specified by it's size
// SAFETY: [u64] is Send + Sync
let id = world.register_component_with_descriptor(unsafe {
ComponentDescriptor::new_with_layout(
name.to_string(),
StorageType::Table,
Layout::array::<u64>(size).unwrap(),
None,
true,
ComponentCloneBehavior::Default,
)
});
let Some(info) = world.components().get_info(id) else {
return;
};
component_names.insert(name.to_string(), id);
component_info.insert(id, info.clone());
println!("Component {} created with id: {}", name, id.index());
});
}
"s" => {
let mut to_insert_ids = Vec::new();
let mut to_insert_data = Vec::new();
rest.split(',').for_each(|component| {
let mut component = component.split_whitespace();
let Some(name) = component.next() else {
return;
};
// Get the id for the component with the given name
let Some(&id) = component_names.get(name) else {
println!("Component {name} does not exist");
return;
};
// Calculate the length for the array based on the layout created for this component id
let info = world.components().get_info(id).unwrap();
let len = info.layout().size() / size_of::<u64>();
let mut values: Vec<u64> = component
.take(len)
.filter_map(|value| value.parse::<u64>().ok())
.collect();
values.resize(len, 0);
// Collect the id and array to be inserted onto our entity
to_insert_ids.push(id);
to_insert_data.push(values);
});
let mut entity = world.spawn_empty();
// Construct an `OwningPtr` for each component in `to_insert_data`
let to_insert_ptr = to_owning_ptrs(&mut to_insert_data);
// SAFETY:
// - Component ids have been taken from the same world
// - Each array is created to the layout specified in the world
unsafe {
entity.insert_by_ids(&to_insert_ids, to_insert_ptr.into_iter());
}
println!("Entity spawned with id: {}", entity.id());
}
"q" => {
let mut builder = QueryBuilder::<FilteredEntityMut>::new(&mut world);
parse_query(rest, &mut builder, &component_names);
let mut query = builder.build();
query.iter_mut(&mut world).for_each(|filtered_entity| {
let terms = filtered_entity
.access()
.component_reads_and_writes()
.0
.map(|id| {
let ptr = filtered_entity.get_by_id(id).unwrap();
let info = component_info.get(&id).unwrap();
let len = info.layout().size() / size_of::<u64>();
// SAFETY:
// - All components are created with layout [u64]
// - len is calculated from the component descriptor
let data = unsafe {
std::slice::from_raw_parts_mut(
ptr.assert_unique().as_ptr().cast::<u64>(),
len,
)
};
// If we have write access, increment each value once
if filtered_entity.access().has_component_write(id) {
data.iter_mut().for_each(|data| {
*data += 1;
});
}
format!("{}: {:?}", info.name(), data[0..len].to_vec())
})
.collect::<Vec<_>>()
.join(", ");
println!("{}: {}", filtered_entity.id(), terms);
});
}
_ => continue,
}
}
}
// Constructs `OwningPtr` for each item in `components`
// By sharing the lifetime of `components` with the resulting ptrs we ensure we don't drop the data before use
fn to_owning_ptrs(components: &mut [Vec<u64>]) -> Vec<OwningPtr<Aligned>> {
components
.iter_mut()
.map(|data| {
let ptr = data.as_mut_ptr();
// SAFETY:
// - Pointers are guaranteed to be non-null
// - Memory pointed to won't be dropped until `components` is dropped
unsafe {
let non_null = NonNull::new_unchecked(ptr.cast());
OwningPtr::new(non_null)
}
})
.collect()
}
fn parse_term<Q: QueryData>(
str: &str,
builder: &mut QueryBuilder<Q>,
components: &HashMap<String, ComponentId>,
) {
let mut matched = false;
let str = str.trim();
match str.chars().next() {
// Optional term
Some('?') => {
builder.optional(|b| parse_term(&str[1..], b, components));
matched = true;
}
// Reference term
Some('&') => {
let mut parts = str.split_whitespace();
let first = parts.next().unwrap();
if first == "&mut" {
if let Some(str) = parts.next() {
if let Some(&id) = components.get(str) {
builder.mut_id(id);
matched = true;
}
};
} else if let Some(&id) = components.get(&first[1..]) {
builder.ref_id(id);
matched = true;
}
}
// With term
Some(_) => {
if let Some(&id) = components.get(str) {
builder.with_id(id);
matched = true;
}
}
None => {}
};
if !matched {
println!("Unable to find component: {str}");
}
}
fn parse_query<Q: QueryData>(
str: &str,
builder: &mut QueryBuilder<Q>,
components: &HashMap<String, ComponentId>,
) {
let str = str.split(',');
str.for_each(|term| {
let sub_terms: Vec<_> = term.split("||").collect();
if sub_terms.len() == 1 {
parse_term(sub_terms[0], builder, components);
} else {
builder.or(|b| {
sub_terms
.iter()
.for_each(|term| parse_term(term, b, components));
});
}
});
}