bevy/examples/ecs/dynamic.rs
Zachary Harrold a35811d088
Add Immutable Component Support (#16372)
# Objective

- Fixes #16208

## Solution

- Added an associated type to `Component`, `Mutability`, which flags
whether a component is mutable, or immutable. If `Mutability= Mutable`,
the component is mutable. If `Mutability= Immutable`, the component is
immutable.
- Updated `derive_component` to default to mutable unless an
`#[component(immutable)]` attribute is added.
- Updated `ReflectComponent` to check if a component is mutable and, if
not, panic when attempting to mutate.

## Testing

- CI
- `immutable_components` example.

---

## Showcase

Users can now mark a component as `#[component(immutable)]` to prevent
safe mutation of a component while it is attached to an entity:

```rust
#[derive(Component)]
#[component(immutable)]
struct Foo {
    // ...
}
```

This prevents creating an exclusive reference to the component while it
is attached to an entity. This is particularly powerful when combined
with component hooks, as you can now fully track a component's value,
ensuring whatever invariants you desire are upheld. Before this would be
done my making a component private, and manually creating a `QueryData`
implementation which only permitted read access.

<details>
  <summary>Using immutable components as an index</summary>
  
```rust
/// This is an example of a component like [`Name`](bevy::prelude::Name), but immutable.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Component)]
#[component(
    immutable,
    on_insert = on_insert_name,
    on_replace = on_replace_name,
)]
pub struct Name(pub &'static str);

/// This index allows for O(1) lookups of an [`Entity`] by its [`Name`].
#[derive(Resource, Default)]
struct NameIndex {
    name_to_entity: HashMap<Name, Entity>,
}

impl NameIndex {
    fn get_entity(&self, name: &'static str) -> Option<Entity> {
        self.name_to_entity.get(&Name(name)).copied()
    }
}

fn on_insert_name(mut world: DeferredWorld<'_>, entity: Entity, _component: ComponentId) {
    let Some(&name) = world.entity(entity).get::<Name>() else {
        unreachable!()
    };
    let Some(mut index) = world.get_resource_mut::<NameIndex>() else {
        return;
    };

    index.name_to_entity.insert(name, entity);
}

fn on_replace_name(mut world: DeferredWorld<'_>, entity: Entity, _component: ComponentId) {
    let Some(&name) = world.entity(entity).get::<Name>() else {
        unreachable!()
    };
    let Some(mut index) = world.get_resource_mut::<NameIndex>() else {
        return;
    };

    index.name_to_entity.remove(&name);
}

// Setup our name index
world.init_resource::<NameIndex>();

// Spawn some entities!
let alyssa = world.spawn(Name("Alyssa")).id();
let javier = world.spawn(Name("Javier")).id();

// Check our index
let index = world.resource::<NameIndex>();

assert_eq!(index.get_entity("Alyssa"), Some(alyssa));
assert_eq!(index.get_entity("Javier"), Some(javier));

// Changing the name of an entity is also fully capture by our index
world.entity_mut(javier).insert(Name("Steven"));

// Javier changed their name to Steven
let steven = javier;

// Check our index
let index = world.resource::<NameIndex>();

assert_eq!(index.get_entity("Javier"), None);
assert_eq!(index.get_entity("Steven"), Some(steven));
```
  
</details>

Additionally, users can use `Component<Mutability = ...>` in trait
bounds to enforce that a component _is_ mutable or _is_ immutable. When
using `Component` as a trait bound without specifying `Mutability`, any
component is applicable. However, methods which only work on mutable or
immutable components are unavailable, since the compiler must be
pessimistic about the type.

## Migration Guide

- When implementing `Component` manually, you must now provide a type
for `Mutability`. The type `Mutable` provides equivalent behaviour to
earlier versions of `Component`:
```rust
impl Component for Foo {
    type Mutability = Mutable;
    // ...
}
```
- When working with generic components, you may need to specify that
your generic parameter implements `Component<Mutability = Mutable>`
rather than `Component` if you require mutable access to said component.
- The entity entry API has had to have some changes made to minimise
friction when working with immutable components. Methods which
previously returned a `Mut<T>` will now typically return an
`OccupiedEntry<T>` instead, requiring you to add an `into_mut()` to get
the `Mut<T>` item again.

## Draft Release Notes

Components can now be made immutable while stored within the ECS.

Components are the fundamental unit of data within an ECS, and Bevy
provides a number of ways to work with them that align with Rust's rules
around ownership and borrowing. One part of this is hooks, which allow
for defining custom behavior at key points in a component's lifecycle,
such as addition and removal. However, there is currently no way to
respond to _mutation_ of a component using hooks. The reasons for this
are quite technical, but to summarize, their addition poses a
significant challenge to Bevy's core promises around performance.
Without mutation hooks, it's relatively trivial to modify a component in
such a way that breaks invariants it intends to uphold. For example, you
can use `core::mem::swap` to swap the components of two entities,
bypassing the insertion and removal hooks.

This means the only way to react to this modification is via change
detection in a system, which then begs the question of what happens
_between_ that alteration and the next run of that system?
Alternatively, you could make your component private to prevent
mutation, but now you need to provide commands and a custom `QueryData`
implementation to allow users to interact with your component at all.

Immutable components solve this problem by preventing the creation of an
exclusive reference to the component entirely. Without an exclusive
reference, the only way to modify an immutable component is via removal
or replacement, which is fully captured by component hooks. To make a
component immutable, simply add `#[component(immutable)]`:

```rust
#[derive(Component)]
#[component(immutable)]
struct Foo {
    // ...
}
```

When implementing `Component` manually, there is an associated type
`Mutability` which controls this behavior:

```rust
impl Component for Foo {
    type Mutability = Mutable;
    // ...
}
```

Note that this means when working with generic components, you may need
to specify that a component is mutable to gain access to certain
methods:

```rust
// Before
fn bar<C: Component>() {
    // ...
}

// After
fn bar<C: Component<Mutability = Mutable>>() {
    // ...
}
```

With this new tool, creating index components, or caching data on an
entity should be more user friendly, allowing libraries to provide APIs
relying on components and hooks to uphold their invariants.

## Notes

- ~~I've done my best to implement this feature, but I'm not happy with
how reflection has turned out. If any reflection SMEs know a way to
improve this situation I'd greatly appreciate it.~~ There is an
outstanding issue around the fallibility of mutable methods on
`ReflectComponent`, but the DX is largely unchanged from `main` now.
- I've attempted to prevent all safe mutable access to a component that
does not implement `Component<Mutability = Mutable>`, but there may
still be some methods I have missed. Please indicate so and I will
address them, as they are bugs.
- Unsafe is an escape hatch I am _not_ attempting to prevent. Whatever
you do with unsafe is between you and your compiler.
- I am marking this PR as ready, but I suspect it will undergo fairly
major revisions based on SME feedback.
- I've marked this PR as _Uncontroversial_ based on the feature, not the
implementation.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Benjamin Brienen <benjamin.brienen@outlook.com>
Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
Co-authored-by: Nuutti Kotivuori <naked@iki.fi>
2024-12-05 14:27:48 +00:00

274 lines
9.8 KiB
Rust

#![allow(unsafe_code)]
//! 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, io::Write, ptr::NonNull};
use bevy::{
ecs::{
component::{ComponentDescriptor, ComponentId, ComponentInfo, StorageType},
query::QueryData,
world::FilteredEntityMut,
},
prelude::*,
ptr::{Aligned, OwningPtr},
utils::HashMap,
};
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,
)
});
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| {
#[allow(deprecated)]
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));
});
}
});
}