bevy/crates
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
..
bevy_a11y Remove accesskit re-export from bevy_a11y (#16257) 2024-11-08 21:01:16 +00:00
bevy_animation Add Immutable Component Support (#16372) 2024-12-05 14:27:48 +00:00
bevy_app Move all_tuples to a new crate (#16161) 2024-12-03 17:41:09 +00:00
bevy_asset Support creating asset directories (#16220) 2024-11-04 22:06:00 +00:00
bevy_audio Update rodio requirement from 0.19 to 0.20 (#16359) 2024-12-03 17:55:42 +00:00
bevy_color Fix MSRVs for standalone crates (#16333) 2024-11-17 09:38:13 +00:00
bevy_core Support on_thread_spawn and on_thread_destroy for TaskPoolPlugin (#13045) 2024-11-11 20:00:01 +00:00
bevy_core_pipeline Add Immutable Component Support (#16372) 2024-12-05 14:27:48 +00:00
bevy_derive move ANDROID_APP to bevy_window (#15585) 2024-10-02 03:01:06 +00:00
bevy_dev_tools Retained Gizmos (#15473) 2024-12-04 21:21:06 +00:00
bevy_diagnostic Revert "Update sysinfo version to 0.32.1 (#16517)" (#16523) 2024-11-26 22:44:23 +00:00
bevy_dylib Generate links to definition in source code pages on docs.rs and dev-docs.bevyengine.org (#12965) 2024-07-29 23:10:16 +00:00
bevy_ecs Add Immutable Component Support (#16372) 2024-12-05 14:27:48 +00:00
bevy_encase_derive Update `glam to 0.29, encase` to 0.10. (#15249) 2024-09-23 19:44:02 +00:00
bevy_gilrs Use Name component for gamepad (#16233) 2024-11-05 00:30:48 +00:00
bevy_gizmos Retained Gizmos (#15473) 2024-12-04 21:21:06 +00:00
bevy_gltf Make bevy_gltf compile without bevy_animation feature (#16551) 2024-11-30 00:04:30 +00:00
bevy_hierarchy Add Immutable Component Support (#16372) 2024-12-05 14:27:48 +00:00
bevy_image switch bevy_image to use wgpu-types wherever possible instead of wgpu (#16620) 2024-12-03 11:46:10 +00:00
bevy_input Move required components doc to type doc (#16575) 2024-12-03 19:45:20 +00:00
bevy_internal Fix the picking backend features not actually disabling the features (#16470) 2024-11-22 18:14:16 +00:00
bevy_log Use en-us locale for typos (#16037) 2024-10-20 18:55:17 +00:00
bevy_macro_utils Modify derive_label to support no_std environments (#15465) 2024-09-27 20:23:26 +00:00
bevy_math Use IntoIterator instead of Into<Vec<..>> in cubic splines interfaces (#16402) 2024-12-03 19:35:14 +00:00
bevy_mesh switch bevy_mesh to use wgpu-types instead of wgpu (#16619) 2024-12-03 19:49:49 +00:00
bevy_mikktspace Use en-us locale for typos (#16037) 2024-10-20 18:55:17 +00:00
bevy_pbr Cluster light probes using conservative spherical bounds. (#13746) 2024-12-05 13:07:10 +00:00
bevy_picking Fix the picking backend features not actually disabling the features (#16470) 2024-11-22 18:14:16 +00:00
bevy_ptr Fix MSRVs for standalone crates (#16333) 2024-11-17 09:38:13 +00:00
bevy_reflect Move all_tuples to a new crate (#16161) 2024-12-03 17:41:09 +00:00
bevy_remote BrpQueryRow has field deserialization fix (#16613) 2024-12-04 18:26:33 +00:00
bevy_render Add Immutable Component Support (#16372) 2024-12-05 14:27:48 +00:00
bevy_scene Move required components doc to type doc (#16575) 2024-12-03 19:45:20 +00:00
bevy_sprite Move required components doc to type doc (#16575) 2024-12-03 19:45:20 +00:00
bevy_state Make StateTransitionSteps public (#16612) 2024-12-03 19:48:00 +00:00
bevy_tasks Support on_thread_spawn and on_thread_destroy for TaskPoolPlugin (#13045) 2024-11-11 20:00:01 +00:00
bevy_text Add Immutable Component Support (#16372) 2024-12-05 14:27:48 +00:00
bevy_time Use en-us locale for typos (#16037) 2024-10-20 18:55:17 +00:00
bevy_transform Move required components doc to type doc (#16575) 2024-12-03 19:45:20 +00:00
bevy_ui Move required components doc to type doc (#16575) 2024-12-03 19:45:20 +00:00
bevy_utils Move all_tuples to a new crate (#16161) 2024-12-03 17:41:09 +00:00
bevy_window Support prefers_home_indicator_hidden (#16005) 2024-10-31 16:09:30 +00:00
bevy_winit bevy_winit(emit raw winit events) (#15884) 2024-12-03 17:20:43 +00:00