diff --git a/Cargo.toml b/Cargo.toml index 6612b1630986e..b0dbc6b82ba21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -761,6 +761,16 @@ description = "Full guide to Bevy's ECS" category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "archetype_invariants" +path = "examples/ecs/archetype_invariants.rs" + +[package.metadata.example.archetype_invariants] +name = "Archetype Invariants" +description = "Assertions about valid combinations of components" +category = "ECS (Entity Component System)" +wasm = false + [[example]] name = "component_change_detection" path = "examples/ecs/component_change_detection.rs" diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index e71cda2229307..111b5eda6726f 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -3,13 +3,13 @@ pub use bevy_derive::AppLabel; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ event::{Event, Events}, - prelude::FromWorld, + prelude::{Bundle, FromWorld}, schedule::{ IntoSystemDescriptor, Schedule, ShouldRun, Stage, StageLabel, State, StateData, SystemSet, SystemStage, }, system::Resource, - world::World, + world::{ArchetypeInvariant, UntypedArchetypeInvariant, World}, }; use bevy_utils::{tracing::debug, HashMap}; use std::fmt::Debug; @@ -141,6 +141,32 @@ impl App { (runner)(app); } + /// Adds a new [`ArchetypeInvariant`] to this app's [`World`]. + /// + /// Whenever a new archetype invariant is added to a world, all existing archetypes are re-checked. + /// This may include empty archetypes- archetypes that contain no entities. + pub fn add_archetype_invariant( + &mut self, + archetype_invariant: ArchetypeInvariant, + ) -> &mut Self { + self.world.add_archetype_invariant(archetype_invariant); + self + } + + /// Inserts a new [`UntypedArchetypeInvariant`] to this app's [`World`]. + /// + /// Whenever a new archetype invariant is added to a world, all existing archetypes are re-checked. + /// This may include empty archetypes- archetypes that contain no entities. + /// Prefer [`add_archetype_invariant`](App::add_archetype_invariant) where possible. + pub fn add_untyped_archetype_invariant( + &mut self, + archetype_invariant: UntypedArchetypeInvariant, + ) -> &mut Self { + self.world + .add_untyped_archetype_invariant(archetype_invariant); + self + } + /// Adds a [`Stage`] with the given `label` to the last position of the app's /// [`Schedule`]. /// diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index cf81b5b633a2e..90d4a866a25aa 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -3,9 +3,10 @@ use crate::{ bundle::BundleId, - component::{ComponentId, StorageType}, + component::{ComponentId, Components, StorageType}, entity::{Entity, EntityLocation}, storage::{Column, SparseArray, SparseSet, SparseSetIndex, TableId}, + world::ArchetypeInvariants, }; use std::{ collections::HashMap, @@ -145,6 +146,7 @@ pub struct Archetype { } impl Archetype { + #[allow(clippy::too_many_arguments)] pub fn new( id: ArchetypeId, table_id: TableId, @@ -152,13 +154,15 @@ impl Archetype { sparse_set_components: Box<[ComponentId]>, table_archetype_components: Vec, sparse_set_archetype_components: Vec, + archetype_invariants: &ArchetypeInvariants, + components: &Components, ) -> Self { - let mut components = + let mut component_set = SparseSet::with_capacity(table_components.len() + sparse_set_components.len()); for (component_id, archetype_component_id) in table_components.iter().zip(table_archetype_components) { - components.insert( + component_set.insert( *component_id, ArchetypeComponentInfo { storage_type: StorageType::Table, @@ -171,7 +175,7 @@ impl Archetype { .iter() .zip(sparse_set_archetype_components) { - components.insert( + component_set.insert( *component_id, ArchetypeComponentInfo { storage_type: StorageType::SparseSet, @@ -179,13 +183,16 @@ impl Archetype { }, ); } + + archetype_invariants.test_archetype(component_set.indices(), components); + Self { id, table_info: TableInfo { id: table_id, entity_rows: Default::default(), }, - components, + components: component_set, table_components, sparse_set_components, unique_components: SparseSet::new(), @@ -391,7 +398,13 @@ impl Default for Archetypes { archetype_ids: Default::default(), archetype_component_count: 0, }; - archetypes.get_id_or_insert(TableId::empty(), Vec::new(), Vec::new()); + archetypes.get_id_or_insert( + TableId::empty(), + Vec::new(), + Vec::new(), + &ArchetypeInvariants::default(), + &Components::default(), + ); // adds the resource archetype. it is "special" in that it is inaccessible via a "hash", // which prevents entities from being added to it @@ -402,6 +415,8 @@ impl Default for Archetypes { Box::new([]), Vec::new(), Vec::new(), + &ArchetypeInvariants::default(), + &Components::default(), )); archetypes } @@ -488,6 +503,8 @@ impl Archetypes { table_id: TableId, table_components: Vec, sparse_set_components: Vec, + archetype_invariants: &ArchetypeInvariants, + components: &Components, ) -> ArchetypeId { let table_components = table_components.into_boxed_slice(); let sparse_set_components = sparse_set_components.into_boxed_slice(); @@ -521,6 +538,8 @@ impl Archetypes { sparse_set_components, table_archetype_components, sparse_set_archetype_components, + archetype_invariants, + components, )); id }) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index d9124f2493b84..ae5200f9005a3 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -9,6 +9,7 @@ use crate::{ component::{Component, ComponentId, ComponentTicks, Components, StorageType}, entity::{Entities, Entity, EntityLocation}, storage::{SparseSetIndex, SparseSets, Storages, Table}, + world::ArchetypeInvariants, }; use bevy_ecs_macros::all_tuples; use bevy_ptr::OwningPtr; @@ -260,6 +261,7 @@ impl BundleInfo { &self.storage_types } + #[allow(clippy::too_many_arguments)] pub(crate) fn get_bundle_inserter<'a, 'b>( &'b self, entities: &'a mut Entities, @@ -268,9 +270,15 @@ impl BundleInfo { storages: &'a mut Storages, archetype_id: ArchetypeId, change_tick: u32, + archetype_invariants: &ArchetypeInvariants, ) -> BundleInserter<'a, 'b> { - let new_archetype_id = - self.add_bundle_to_archetype(archetypes, storages, components, archetype_id); + let new_archetype_id = self.add_bundle_to_archetype( + archetypes, + storages, + components, + archetype_id, + archetype_invariants, + ); let archetypes_ptr = archetypes.archetypes.as_mut_ptr(); if new_archetype_id == archetype_id { let archetype = &mut archetypes[archetype_id]; @@ -327,9 +335,15 @@ impl BundleInfo { components: &mut Components, storages: &'a mut Storages, change_tick: u32, + archetype_invariants: &ArchetypeInvariants, ) -> BundleSpawner<'a, 'b> { - let new_archetype_id = - self.add_bundle_to_archetype(archetypes, storages, components, ArchetypeId::EMPTY); + let new_archetype_id = self.add_bundle_to_archetype( + archetypes, + storages, + components, + ArchetypeId::EMPTY, + archetype_invariants, + ); let (empty_archetype, archetype) = archetypes.get_2_mut(ArchetypeId::EMPTY, new_archetype_id); let table = &mut storages.tables[archetype.table_id()]; @@ -399,6 +413,7 @@ impl BundleInfo { storages: &mut Storages, components: &mut Components, archetype_id: ArchetypeId, + archetype_invariants: &ArchetypeInvariants, ) -> ArchetypeId { if let Some(add_bundle) = archetypes[archetype_id].edges().get_add_bundle(self.id) { return add_bundle.archetype_id; @@ -461,8 +476,13 @@ impl BundleInfo { new_sparse_set_components }; }; - let new_archetype_id = - archetypes.get_id_or_insert(table_id, table_components, sparse_set_components); + let new_archetype_id = archetypes.get_id_or_insert( + table_id, + table_components, + sparse_set_components, + archetype_invariants, + components, + ); // add an edge from the old archetype to the new archetype archetypes[archetype_id].edges_mut().insert_add_bundle( self.id, diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index c59a1e2028737..a5d05c6c659e4 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -7,6 +7,7 @@ use crate::{ }; pub use bevy_ecs_macros::Component; use bevy_ptr::OwningPtr; +use bevy_utils::get_short_name; use std::{ alloc::Layout, any::{Any, TypeId}, @@ -251,6 +252,22 @@ impl SparseSetIndex for ComponentId { } } +/// Returns the shortened type names of the provided [`ComponentId`]s. +/// +/// Uses [`get_short_name`] to strip the module paths of the items, resulting in cleaner lists. +pub fn display_component_id_types<'a, I: Iterator>( + component_ids: I, + components: &Components, +) -> String { + component_ids + .map(|id| match components.get_info(*id) { + Some(info) => get_short_name(info.name()), + None => format!("{:?}", id), + }) + .reduce(|acc, s| format!("{}, {}", acc, s)) + .unwrap_or_default() +} + pub struct ComponentDescriptor { name: Cow<'static, str>, // SAFETY: This must remain private. It must match the statically known StorageType of the @@ -370,6 +387,9 @@ pub struct Components { } impl Components { + /// Adds a new component type to [`Components`]. + /// + /// If the component type is already present, it simply returns its [`ComponentId`]. #[inline] pub fn init_component(&mut self, storages: &mut Storages) -> ComponentId { let type_id = TypeId::of::(); @@ -385,6 +405,7 @@ impl Components { ComponentId(*index) } + /// Adds a new component with the provided [`ComponentDescriptor`] to [`Components`]. pub fn init_component_with_descriptor( &mut self, storages: &mut Storages, diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 6143036737dbb..1b0f8c9fa6096 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -42,7 +42,10 @@ pub mod prelude { NonSendMut, ParallelCommands, ParamSet, Query, RemovedComponents, Res, ResMut, Resource, System, SystemParamFunction, }, - world::{FromWorld, Mut, World}, + world::{ + ArchetypeInvariant, ArchetypeInvariantHelpers, ArchetypeStatement, FromWorld, Mut, + World, + }, }; } diff --git a/crates/bevy_ecs/src/world/archetype_invariants.rs b/crates/bevy_ecs/src/world/archetype_invariants.rs new file mode 100644 index 0000000000000..8dfea1f8d2835 --- /dev/null +++ b/crates/bevy_ecs/src/world/archetype_invariants.rs @@ -0,0 +1,730 @@ +use std::marker::PhantomData; + +use bevy_utils::HashSet; + +use crate::{ + component::{display_component_id_types, ComponentId, Components}, + prelude::Bundle, + world::World, +}; + +/// A rule about which [`Component`](crate::component::Component)s can coexist on entities. +/// +/// These rules must be true at all times for all entities in the [`World`]. +/// The generic [`Bundle`] type `B1` is always used in the `premise`, +/// while `B2` is used in the `consequence`. +/// If only a single generic is provided, these types are the same. +/// +/// When added to the [`World`], archetype invariants behave like [`assert!`]. +/// Archetype invariants are checked each time [`Archetypes`](crate::archetype::Archetypes) is modified; +/// this can occur on component addition, component removal, and entity spawning. +/// +/// Archetypes are only modified when a novel archetype (set of components) is seen for the first time; +/// swapping between existing archetypes will not trigger these checks. +/// +/// Note that archetype invariants are not symmetric by default. +/// For example, `ArchetypeInvariant::::requires_one()` means that `B1` requires `B2`, +/// but not that `B2` requires `B1`. +/// In this case, an entity with just `B2` is completely valid, but an entity with just `B1` is not. +/// If symmetry is desired, repeat the invariant with the order of the types switched. +/// +/// When working with dynamic component types (for non-Rust components), +/// use [`UntypedArchetypeInvariant`] and [`UntypedArchetypeStatement`] instead. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArchetypeInvariant { + /// Defines which entities this invariant applies to. + /// This is the "if" of the if/then clause. + pub premise: ArchetypeStatement, + /// Defines what must be true for the entities that this invariant applies to. + /// This is the "then" of the if/then clause. + pub consequence: ArchetypeStatement, +} + +impl ArchetypeInvariant { + /// Erases the type information of this archetype invariant. + /// + /// Requires mutable world access, since the components might not have been added to the world yet. + #[inline] + pub fn into_untyped(self, world: &mut World) -> UntypedArchetypeInvariant { + UntypedArchetypeInvariant { + premise: self.premise.into_untyped(world), + consequence: self.consequence.into_untyped(world), + } + } +} + +/// A statement about the presence or absence of some subset of components in the given [`Bundle`]. +/// +/// This type is used as part of an [`ArchetypeInvariant`]. +/// +/// When used as a premise, the archetype invariant matches all entities which satisfy the statement. +/// When used as a consequence, then the statment must be true for all entities that were matched by the premise. +/// +/// Statements can also be defined on single components, like so: `ArchetypeStatement::::all_of`. +/// For single component statements, `AllOf` and `AnyOf` are equivalent. +/// Prefer `ArchetypeStatement::::all_of` over `ArchetypeStatement::::any_of` for consistency and clarity. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ArchetypeStatement { + /// Evaluates to true if and only if the entity has all of the components present in the bundle `B`. + AllOf(PhantomData), + /// The entity has at least one component in the bundle `B`. + /// + /// When using a single-component bundle, `AllOf` is preferred by convention. + AnyOf(PhantomData), + /// The entity has zero or one of the components in the bundle `B`, but no more. + /// + /// When using a single-component bundle this is always true. + /// Prefer the much clearer `True` variant. + AtMostOneOf(PhantomData), + /// The entity has none of the components in the bundle `B`. + NoneOf(PhantomData), + /// The entity contains only components from the bundle `B`, and no others. + Only(PhantomData), + /// This statement is always true. + /// + /// Useful for constructing universal invariants. + True, + /// This statement is always false. + /// + /// Useful for constructing universal invariants. + False, +} + +impl ArchetypeStatement { + /// Erases the type information of this archetype statement. + /// + /// Requires mutable world access, since the components might not have been added to the world yet. + pub fn into_untyped(self, world: &mut World) -> UntypedArchetypeStatement { + let mut component_ids = HashSet::new(); + B::component_ids(&mut world.components, &mut world.storages, &mut |id| { + component_ids.insert(id); + }); + + match self { + ArchetypeStatement::AllOf(_) => UntypedArchetypeStatement::AllOf(component_ids), + ArchetypeStatement::AnyOf(_) => UntypedArchetypeStatement::AnyOf(component_ids), + ArchetypeStatement::AtMostOneOf(_) => { + UntypedArchetypeStatement::AtMostOneOf(component_ids) + } + ArchetypeStatement::NoneOf(_) => UntypedArchetypeStatement::NoneOf(component_ids), + ArchetypeStatement::Only(_) => UntypedArchetypeStatement::Only(component_ids), + ArchetypeStatement::True => UntypedArchetypeStatement::True, + ArchetypeStatement::False => UntypedArchetypeStatement::False, + } + } + + /// Constructs a new [`ArchetypeStatement::AllOf`] variant for all components stored in the bundle `B`. + #[inline] + pub const fn all_of() -> Self { + ArchetypeStatement::AllOf(PhantomData) + } + + /// Constructs a new [`ArchetypeStatement::AnyOf`] variant for all components stored in the bundle `B`. + #[inline] + pub const fn any_of() -> Self { + ArchetypeStatement::AnyOf(PhantomData) + } + + /// Constructs a new [`ArchetypeStatement::AtMostOneOf`] variant for all components stored in the bundle `B`. + #[inline] + pub const fn at_most_one_of() -> Self { + ArchetypeStatement::AtMostOneOf(PhantomData) + } + + /// Constructs a new [`ArchetypeStatement::NoneOf`] variant for all components stored in the bundle `B`. + #[inline] + pub const fn none_of() -> Self { + ArchetypeStatement::NoneOf(PhantomData) + } + + /// Constructs a new [`ArchetypeStatement::Only`] variant for all components stored in the bundle `B`. + #[inline] + pub const fn only() -> Self { + ArchetypeStatement::Only(PhantomData) + } +} + +// We must pass in a generic type to all archetype statements; +// we use the empty bundle `()` by convention. +// These helper methods are useful because they improve type inference and consistency in user code. +impl ArchetypeStatement<()> { + /// Constructs a new [`ArchetypeStatement::True`] variant. + #[inline] + pub const fn always_true() -> Self { + ArchetypeStatement::<()>::True + } + + /// Constructs a new [`ArchetypeStatement::False`] variant. + #[inline] + pub const fn always_false() -> Self { + ArchetypeStatement::<()>::False + } +} + +/// Defines helper methods to eaily contruct common archetype invariants. +/// +/// For more details on each method, see the implementation docs. +/// This trait is sealed: it should never be implemented by dependencies. +pub trait ArchetypeInvariantHelpers: private::Sealed { + fn forbids() -> ArchetypeInvariant; + + fn requires() -> ArchetypeInvariant; + + fn requires_one() -> ArchetypeInvariant; + + fn atomic() -> ArchetypeInvariant; + + fn disjoint() -> ArchetypeInvariant; + + fn exclusive() -> ArchetypeInvariant; +} + +impl ArchetypeInvariantHelpers for B { + /// Creates an archetype invariant where any component of `B` forbids every component from `B2`, and vice versa. + /// + /// In other words, if any component from `B` is present, then none of the components from `B2` can be present. + /// Although this appears asymmetric, it actually implies its own converse. + /// This is particularly useful for avoiding query conflicts. + #[inline] + fn forbids() -> ArchetypeInvariant { + ArchetypeInvariant { + premise: ArchetypeStatement::::any_of(), + consequence: ArchetypeStatement::::none_of(), + } + } + + /// Creates an archetype invariant where components of `B` require all the components of `B2`. + /// + /// In other words, if any component from `B` is present, then all of the components from `B2` must be. + #[inline] + fn requires() -> ArchetypeInvariant { + ArchetypeInvariant { + premise: ArchetypeStatement::::any_of(), + consequence: ArchetypeStatement::::all_of(), + } + } + + /// Creates an archetype invariant where components of `B` require at least one component of `B2`. + /// + /// In other words, if any component from `B` is present, then at least one component from `B2` must be. + #[inline] + fn requires_one() -> ArchetypeInvariant { + ArchetypeInvariant { + premise: ArchetypeStatement::::any_of(), + consequence: ArchetypeStatement::::any_of(), + } + } + + /// Creates an archetype invariant where all components of `B` require each other. + /// + /// In other words, if any component of this bundle is present, then all of them must be. + #[inline] + fn atomic() -> ArchetypeInvariant { + ArchetypeInvariant { + premise: ArchetypeStatement::::any_of(), + consequence: ArchetypeStatement::::all_of(), + } + } + + /// Creates an archetype where components of `B` cannot appear with each other. + /// + /// In other words, if any component of this bundle is present, then no others can be. + /// This is particularly useful for creating enum-like groups of components, such as `Dead` and `Alive`. + #[inline] + fn disjoint() -> ArchetypeInvariant { + ArchetypeInvariant { + premise: ArchetypeStatement::::any_of(), + consequence: ArchetypeStatement::::at_most_one_of(), + } + } + + /// Creates an archetype invariant where components of `B` can only appear with each other. + /// + /// In other words, if any component of this bundle is present, then _only_ components from this bundle can be present. + #[inline] + fn exclusive() -> ArchetypeInvariant { + ArchetypeInvariant { + premise: ArchetypeStatement::::any_of(), + consequence: ArchetypeStatement::::only(), + } + } +} + +/// A special module used to prevent [`ArchetypeInvariantHelpers`] from being implemented by any other module. +/// +/// For more information, see: . +mod private { + use crate::prelude::Bundle; + + pub trait Sealed {} + + impl Sealed for B {} +} + +/// A type-erased version of [`ArchetypeInvariant`]. +/// +/// Intended to be used with dynamic components that cannot be represented with Rust types. +/// Prefer [`ArchetypeInvariant`] when possible. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UntypedArchetypeInvariant { + /// Defines which entities this invariant applies to. + /// This is the "if" of the if/then clause. + pub premise: UntypedArchetypeStatement, + /// Defines what must be true for the entities that this invariant applies to. + /// This is the "then" of the if/then clause. + pub consequence: UntypedArchetypeStatement, +} + +impl UntypedArchetypeInvariant { + /// Asserts that the provided iterator of [`ComponentId`]s obeys this archetype invariant. + /// + /// When testing against multiple archetype invariants, [`ArchetypeInvariants::test_archetype`] is preferred, + /// as it can more efficiently cache checks between archetypes. + /// + /// # Panics + /// Panics if the archetype invariant is violated. + pub(crate) fn test_archetype( + &self, + component_ids_of_archetype: impl Iterator, + components: &Components, + ) { + let component_ids_of_archetype: HashSet = component_ids_of_archetype.collect(); + + if self.premise.test(&component_ids_of_archetype) + && !self.consequence.test(&component_ids_of_archetype) + { + let archetype_component_names = + display_component_id_types(component_ids_of_archetype.iter(), components); + + panic!( + "Archetype invariant {} failed for archetype [{}]", + self.display(components), + archetype_component_names + ); + } + } + + /// Returns formatted string describing this archetype invariant + pub fn display(&self, components: &Components) -> String { + format!( + "{{Premise: {}, Consequence: {}}}", + self.premise.display(components), + self.consequence.display(components) + ) + } +} + +/// A type-erased version of [`ArchetypeStatement`]. +/// +/// Intended to be used with dynamic components that cannot be represented with Rust types. +/// Prefer [`ArchetypeStatement`] when possible. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum UntypedArchetypeStatement { + /// Evaluates to true if and only if the entity has all of the components present in the set. + AllOf(HashSet), + /// The entity has at least one component in the set, and may have all of them. + /// + /// When using a single-component set, `AllOf` is preferred. + AnyOf(HashSet), + /// The entity has zero or one of the components in the set, but no more. + /// + /// When using a single-component bundle this is always true. + /// Prefer the much clearer `True` variant. + AtMostOneOf(HashSet), + /// The entity has none of the components in the set. + NoneOf(HashSet), + /// The entity contains only components from the bundle `B`, and no others. + Only(HashSet), + /// This statement is always true. + /// + /// Useful for constructing universal invariants. + True, + /// This statement is always false. + /// + /// Useful for constructing universal invariants. + False, +} + +impl UntypedArchetypeStatement { + /// Returns the set of [`ComponentId`]s affected by this statement. + /// + /// Returns `Some` for all variants other than the static `True` and `False`. + pub fn component_ids(&self) -> Option<&HashSet> { + match self { + UntypedArchetypeStatement::AllOf(set) + | UntypedArchetypeStatement::AnyOf(set) + | UntypedArchetypeStatement::AtMostOneOf(set) + | UntypedArchetypeStatement::NoneOf(set) + | UntypedArchetypeStatement::Only(set) => Some(set), + UntypedArchetypeStatement::True | UntypedArchetypeStatement::False => None, + } + } + + /// Returns formatted string describing this archetype statement. + /// + /// For Rust types, the names should match the type name. + /// If any [`ComponentId`]s in the statement have not been registered in the world, + /// then the raw component id will be returned instead. + pub fn display(&self, components: &Components) -> String { + let component_names = display_component_id_types( + self.component_ids().unwrap_or(&HashSet::new()).iter(), + components, + ); + + match self { + UntypedArchetypeStatement::AllOf(_) => format!("AllOf({component_names})"), + UntypedArchetypeStatement::AnyOf(_) => format!("AnyOf({component_names})"), + UntypedArchetypeStatement::AtMostOneOf(_) => format!("AtMostOneOf({component_names})"), + UntypedArchetypeStatement::NoneOf(_) => format!("NoneOf({component_names})"), + UntypedArchetypeStatement::Only(_) => format!("Only({component_names})"), + UntypedArchetypeStatement::True => "True".to_owned(), + UntypedArchetypeStatement::False => "False".to_owned(), + } + } + + /// Test if this statement is true for the provided set of [`ComponentId`]s. + pub fn test(&self, component_ids: &HashSet) -> bool { + match self { + UntypedArchetypeStatement::AllOf(required_ids) => { + for required_id in required_ids { + if !component_ids.contains(required_id) { + return false; + } + } + true + } + UntypedArchetypeStatement::AnyOf(desired_ids) => { + for desired_id in desired_ids { + if component_ids.contains(desired_id) { + return true; + } + } + false + } + UntypedArchetypeStatement::AtMostOneOf(exclusive_ids) => { + let mut found_previous = false; + for exclusive_id in exclusive_ids { + if component_ids.contains(exclusive_id) { + if found_previous { + return false; + } + found_previous = true; + } + } + true + } + UntypedArchetypeStatement::NoneOf(forbidden_ids) => { + for forbidden_id in forbidden_ids { + if component_ids.contains(forbidden_id) { + return false; + } + } + true + } + UntypedArchetypeStatement::Only(only_ids) => { + for component_id in component_ids { + if !only_ids.contains(component_id) { + return false; + } + } + true + } + UntypedArchetypeStatement::True => true, + UntypedArchetypeStatement::False => false, + } + } +} + +/// A list of [`ArchetypeInvariant`]s, stored on a [`World`]. +/// +/// These store [`UntypedArchetypeInvariant`]s to ensure fast computation +/// and compatiblity with dynamic (non-Rust) component types. +#[derive(Default)] +pub struct ArchetypeInvariants { + /// The list of invariants that must be upheld. + raw_list: Vec, +} + +impl ArchetypeInvariants { + /// Adds a new [`ArchetypeInvariant`] to this set of archetype invariants. + #[inline] + pub fn add(&mut self, archetype_invariant: UntypedArchetypeInvariant) { + self.raw_list.push(archetype_invariant); + } + + /// Returns the raw list of [`UntypedArchetypeInvariant`]s + pub fn raw_list(&self) -> &Vec { + &self.raw_list + } + + /// Asserts that the provided iterator of [`ComponentId`]s obeys all archetype invariants. + /// + /// # Panics + /// + /// Panics if any archetype invariant is violated. + pub(crate) fn test_archetype( + &self, + component_ids_of_archetype: impl Iterator, + components: &Components, + ) { + let component_ids_of_archetype: HashSet = component_ids_of_archetype.collect(); + + for invariant in &self.raw_list { + if invariant.premise.test(&component_ids_of_archetype) + && !invariant.consequence.test(&component_ids_of_archetype) + { + let mut failed_invariants = vec![]; + + for invariant in &self.raw_list { + if invariant.premise.test(&component_ids_of_archetype) + && !invariant.consequence.test(&component_ids_of_archetype) + { + failed_invariants.push(invariant.clone()); + } + } + + let archetype_component_names = + display_component_id_types(component_ids_of_archetype.iter(), components); + + let failed_invariant_names = failed_invariants + .iter() + .map(|invariant| invariant.display(components)) + .reduce(|acc, s| format!("{}\n{}", acc, s)) + .unwrap_or_default(); + + panic!( + "At least one archetype invariant was violated for the archetype [{archetype_component_names}]: \ + \n{failed_invariant_names}" + ) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate as bevy_ecs; + use crate::{ + component::Component, world::archetype_invariants::ArchetypeInvariant, + world::archetype_invariants::ArchetypeInvariantHelpers, + world::archetype_invariants::ArchetypeStatement, world::World, + }; + + #[derive(Component)] + struct A; + + #[derive(Component)] + struct B; + + #[derive(Component)] + struct C; + + #[test] + fn on_insert_happy() { + let mut world = World::new(); + + world.spawn((A, B, C)); + world.add_archetype_invariant(<(A, B, C)>::atomic()); + } + + #[test] + #[should_panic] + fn on_insert_sad() { + let mut world = World::new(); + + world.spawn((A, B)); + world.add_archetype_invariant(<(A, B, C)>::atomic()); + } + + #[test] + fn on_insert_untyped_happy() { + let mut world = World::new(); + + world.spawn((A, B, C)); + let archetype_invariant = <(A, B, C)>::atomic().into_untyped(&mut world); + world.add_untyped_archetype_invariant(archetype_invariant); + } + + #[test] + #[should_panic] + fn on_insert_untyped_sad() { + let mut world = World::new(); + + world.spawn((A, B)); + let archetype_invariant = <(A, B, C)>::atomic().into_untyped(&mut world); + world.add_untyped_archetype_invariant(archetype_invariant); + } + + #[test] + fn tautology() { + let mut world = World::new(); + + // This invariant is a tautology. + world.add_archetype_invariant(ArchetypeInvariant { + premise: ArchetypeStatement::always_true(), + consequence: ArchetypeStatement::always_true(), + }); + // This invariant is also a tautology. + world.add_archetype_invariant(ArchetypeInvariant { + premise: ArchetypeStatement::always_false(), + consequence: ArchetypeStatement::always_false(), + }); + + // Since invariants are only checked when archetypes are created, + // we must add something to trigger the check. + world.spawn(A); + } + + #[test] + #[should_panic] + fn contradiction() { + let mut world = World::new(); + + // This invariant is a contradiction. + world.add_archetype_invariant(ArchetypeInvariant { + premise: ArchetypeStatement::always_true(), + consequence: ArchetypeStatement::always_false(), + }); + + // Since invariants are only checked when archetypes are created, + // we must add something to trigger the check. + world.spawn(A); + } + + #[test] + fn forbids_happy() { + let mut world = World::new(); + + world.add_archetype_invariant(A::forbids::<(B, C)>()); + world.spawn(A); + world.spawn((B, C)); + } + + #[test] + #[should_panic] + fn forbids_sad() { + let mut world = World::new(); + + world.add_archetype_invariant(A::forbids::<(B, C)>()); + world.spawn((A, B)); + } + + #[test] + fn requires_happy() { + let mut world = World::new(); + + world.add_archetype_invariant(A::requires::<(B, C)>()); + world.spawn((A, B, C)); + world.spawn((B, C)); + } + + #[test] + #[should_panic] + fn requires_sad_partial() { + let mut world = World::new(); + + world.add_archetype_invariant(A::requires::<(B, C)>()); + world.spawn((A, B)); + } + + #[test] + #[should_panic] + fn requires_sad_none() { + let mut world = World::new(); + + world.add_archetype_invariant(A::requires::<(B, C)>()); + world.spawn(A); + } + + #[test] + fn requires_one_happy() { + let mut world = World::new(); + + world.add_archetype_invariant(A::requires_one::<(B, C)>()); + world.spawn((A, B, C)); + world.spawn((A, B)); + world.spawn((B, C)); + } + + #[test] + #[should_panic] + fn requires_one_sad() { + let mut world = World::new(); + + world.add_archetype_invariant(A::requires_one::<(B, C)>()); + world.spawn(A); + } + + #[test] + fn atomic_happy() { + let mut world = World::new(); + + world.add_archetype_invariant(<(A, B, C)>::atomic()); + world.spawn((A, B, C)); + } + + #[test] + #[should_panic] + fn atomic_sad() { + let mut world = World::new(); + + world.add_archetype_invariant(<(A, B, C)>::atomic()); + world.spawn((A, B)); + } + + #[test] + fn disjoint_happy() { + let mut world = World::new(); + + world.add_archetype_invariant(<(A, B, C)>::disjoint()); + world.spawn(A); + world.spawn(B); + world.spawn(C); + } + + #[test] + #[should_panic] + fn disjoint_sad_partial() { + let mut world = World::new(); + + world.add_archetype_invariant(<(A, B, C)>::disjoint()); + world.spawn((A, B)); + } + + #[test] + #[should_panic] + fn disjoint_sad_all() { + let mut world = World::new(); + + world.add_archetype_invariant(<(A, B, C)>::disjoint()); + world.spawn((A, B, C)); + } + + #[test] + fn exclusive_happy() { + let mut world = World::new(); + + world.add_archetype_invariant(<(A, B)>::exclusive()); + world.spawn((A, B)); + world.spawn(A); + world.spawn(C); + } + + #[test] + #[should_panic] + fn exclusive_sad_partial() { + let mut world = World::new(); + + world.add_archetype_invariant(<(A, B)>::exclusive()); + world.spawn((A, C)); + } + + #[test] + #[should_panic] + fn exclusive_sad_all() { + let mut world = World::new(); + + world.add_archetype_invariant(<(A, B)>::exclusive()); + world.spawn((A, B, C)); + } +} diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index b532387c577f4..d6f0ea5d2711b 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -10,6 +10,8 @@ use crate::{ use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; use std::{any::TypeId, cell::UnsafeCell}; +use super::archetype_invariants::ArchetypeInvariants; + /// A read-only reference to a particular [`Entity`] and all of its components #[derive(Copy, Clone)] pub struct EntityRef<'w> { @@ -259,6 +261,7 @@ impl<'w> EntityMut<'w> { &mut self.world.storages, self.location.archetype_id, change_tick, + &self.world.archetype_invariants, ); // SAFETY: location matches current entity. `T` matches `bundle_info` unsafe { @@ -286,6 +289,7 @@ impl<'w> EntityMut<'w> { let components = &mut self.world.components; let entities = &mut self.world.entities; let removed_components = &mut self.world.removed_components; + let archetype_invariants = &self.world.archetype_invariants; let bundle_info = self.world.bundles.init_info::(components, storages); let old_location = self.location; @@ -299,6 +303,7 @@ impl<'w> EntityMut<'w> { old_location.archetype_id, bundle_info, false, + archetype_invariants, )? }; @@ -419,6 +424,7 @@ impl<'w> EntityMut<'w> { let components = &mut self.world.components; let entities = &mut self.world.entities; let removed_components = &mut self.world.removed_components; + let archetype_invariants = &self.world.archetype_invariants; let bundle_info = self.world.bundles.init_info::(components, storages); let old_location = self.location; @@ -433,6 +439,7 @@ impl<'w> EntityMut<'w> { old_location.archetype_id, bundle_info, true, + archetype_invariants, ) .expect("intersections should always return a result") }; @@ -785,6 +792,7 @@ unsafe fn remove_bundle_from_archetype( archetype_id: ArchetypeId, bundle_info: &BundleInfo, intersection: bool, + archetype_invariants: &ArchetypeInvariants, ) -> Option { // check the archetype graph to see if the Bundle has been removed from this archetype in the // past @@ -854,6 +862,8 @@ unsafe fn remove_bundle_from_archetype( next_table_id, next_table_components, next_sparse_set_components, + archetype_invariants, + components, ); Some(new_archetype_id) }; diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 128c5e8dcf094..bb38b212a9bb8 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,8 +1,10 @@ +mod archetype_invariants; mod entity_ref; mod spawn_batch; mod world_cell; pub use crate::change_detection::Mut; +pub use archetype_invariants::*; pub use entity_ref::*; pub use spawn_batch::*; pub use world_cell::*; @@ -55,6 +57,7 @@ pub struct World { pub(crate) entities: Entities, pub(crate) components: Components, pub(crate) archetypes: Archetypes, + pub(crate) archetype_invariants: ArchetypeInvariants, pub(crate) storages: Storages, pub(crate) bundles: Bundles, pub(crate) removed_components: SparseSet>, @@ -72,6 +75,7 @@ impl Default for World { entities: Default::default(), components: Default::default(), archetypes: Default::default(), + archetype_invariants: Default::default(), storages: Default::default(), bundles: Default::default(), removed_components: Default::default(), @@ -125,6 +129,12 @@ impl World { &self.archetypes } + /// Retrieves this world's [`ArchetypeInvariants`] collection. + #[inline] + pub fn archetype_invariants(&self) -> &ArchetypeInvariants { + &self.archetype_invariants + } + /// Retrieves this world's [Components] collection #[inline] pub fn components(&self) -> &Components { @@ -450,6 +460,7 @@ impl World { &mut self.components, &mut self.storages, *self.change_tick.get_mut(), + &self.archetype_invariants, ); // SAFETY: bundle's type matches `bundle_info`, entity is allocated but non-existent @@ -702,6 +713,43 @@ impl World { } } + /// Inserts a new [`ArchetypeInvariant`] to the world. + /// + /// Whenever a new archetype invariant is added, all existing archetypes are re-checked. + /// This may include empty archetypes- archetypes that contain no entities. + #[inline] + pub fn add_archetype_invariant( + &mut self, + archetype_invariant: ArchetypeInvariant, + ) { + let untyped_invariant = archetype_invariant.into_untyped(self); + + for archetype in &self.archetypes.archetypes { + let archetype_components = archetype.components.indices(); + untyped_invariant.test_archetype(archetype_components, self.components()); + } + + self.archetype_invariants.add(untyped_invariant); + } + + /// Inserts a new [`UntypedArchetypeInvariant`] to the world. + /// + /// Whenever a new archetype invariant is added, all existing archetypes are re-checked. + /// This may include empty archetypes- archetypes that contain no entities. + /// Prefer [`add_archetype_invariant`](World::add_archetype_invariant) where possible. + #[inline] + pub fn add_untyped_archetype_invariant( + &mut self, + archetype_invariant: UntypedArchetypeInvariant, + ) { + for archetype in &self.archetypes.archetypes { + let archetype_components = archetype.components.indices(); + archetype_invariant.test_archetype(archetype_components, self.components()); + } + + self.archetype_invariants.add(archetype_invariant); + } + /// Inserts a new resource with standard starting values. /// /// If the resource already exists, nothing happens. @@ -1065,6 +1113,7 @@ impl World { &mut self.components, &mut self.storages, change_tick, + &self.archetype_invariants, )); let mut invalid_entities = Vec::new(); @@ -1089,6 +1138,7 @@ impl World { &mut self.storages, location.archetype_id, change_tick, + &self.archetype_invariants, ); // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter unsafe { inserter.insert(entity, location.index, bundle) }; @@ -1108,6 +1158,7 @@ impl World { &mut self.components, &mut self.storages, change_tick, + &self.archetype_invariants, ); // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter unsafe { spawner.spawn_non_existent(entity, bundle) }; diff --git a/crates/bevy_ecs/src/world/spawn_batch.rs b/crates/bevy_ecs/src/world/spawn_batch.rs index f5e1bd2792e2b..968097c93c9c8 100644 --- a/crates/bevy_ecs/src/world/spawn_batch.rs +++ b/crates/bevy_ecs/src/world/spawn_batch.rs @@ -38,6 +38,7 @@ where &mut world.components, &mut world.storages, *world.change_tick.get_mut(), + &world.archetype_invariants, ); spawner.reserve_storage(length); diff --git a/examples/README.md b/examples/README.md index 18853ac19db1a..a6dab7a43d2f7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -187,6 +187,7 @@ Example | Description Example | Description --- | --- +[Archetype Invariants](../examples/ecs/archetype_invariants.rs) | Assertions about valid combinations of components [Component Change Detection](../examples/ecs/component_change_detection.rs) | Change detection on components [Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type [ECS Guide](../examples/ecs/ecs_guide.rs) | Full guide to Bevy's ECS diff --git a/examples/ecs/archetype_invariants.rs b/examples/ecs/archetype_invariants.rs new file mode 100644 index 0000000000000..e1ff64ce5c502 --- /dev/null +++ b/examples/ecs/archetype_invariants.rs @@ -0,0 +1,88 @@ +//! Archetype invariants are rules about which combinations of components can coexist. +//! +//! An archetype (in the sense that Bevy uses it) is the "unique set of components" that belong to an entity. +//! These are useful to codify your assumptions about the composition of your entities. +//! For example, an entity can never have a `Player` component with a `Camera` together, +//! or a `GlobalTransform` may only be valid in association with a `Transform`. +//! By constructing `ArchetypeInvariant`s out of `ArchetypeStatement`s, +//! we can encode this logic into our app. +//! +//! Archetype invariants are guaranteed to hold at *all* points during the app's lifecycle; +//! this is automtically checked on component insertion and removal, including when entities are spawned. +//! Make sure to test thoroughly when using archetype invariants in production though; +//! any violations will result in a panic! +//! +//! There are many helper methods provided on `ArchetypeInvariant` to help easily construct common invariant patterns, +//! but we will only be showcasing some of them here. +//! For a full list, see the docs for [`ArchetypeInvariant`]. +use bevy::prelude::*; + +fn main() { + App::new() + // Archetype invariants are constructed in terms of either bundles or single components. + // This invariant ensures that Player and Camera can never be found together on the same entity. + .add_archetype_invariant(Player::forbids::()) + // This invariant ensures that the `GlobalTransform` component is always found with the `Transform` component, and vice versa. + .add_archetype_invariant(<(GlobalTransform, Transform)>::atomic()) + // This invariant ensures that the `Player` component is always found with the `Life` component. + // This requirement is only in one direction: it is possible to have entities which have `Life`, but not `Player` (like enemies). + .add_archetype_invariant(Player::requires::()) + // The `disjoint` invariant ensures that at most one component from the bundle is present on a given entity. + // This way, an entity never belongs to more than one RPG class at once. + // This is useful for creating groups of components that behave similarly to an enum. + .add_archetype_invariant(<(Archer, Swordsman, Mage)>::disjoint()) + // This invariant indicates that any entity with the `Player` component always has + // at least one component in the `(Archer, Swordsman, Mage)` bundle. + // We could use a type alias to improve clarity and avoid errors caused by duplication: + // type Class = (Archer, Swordsman, Mage); + .add_archetype_invariant(Player::requires_one::<(Archer, Swordsman, Mage)>()) + // You can also specify custom invariants by constructing `ArchetypeInvariant` directly. + // This invariant specifies that the `Node` component cannot appear on any entity in our world. + // We're not using bevy_ui in our App, so this component should never show up. + .add_archetype_invariant(ArchetypeInvariant { + premise: ArchetypeStatement::::all_of(), + consequence: ArchetypeStatement::always_false(), + }) + .add_startup_system(spawn_player) + .add_system(position_player) + .run(); +} + +#[derive(Component)] +struct Player; + +#[derive(Component)] +struct Life; + +#[derive(Component)] +struct Archer; + +#[derive(Component)] +struct Swordsman; + +#[derive(Component)] +struct Mage; + +fn spawn_player(mut commands: Commands) { + commands.spawn((Player, Mage, Life)); +} + +fn position_player(mut commands: Commands, query: Query>) { + let player_entity = query.single(); + + // Because of our invariants, these components need to be added together. + // Adding them separately (as in the broken code below) will cause the entity to briefly enter an invalid state, + // where it has only one of the two components. + commands + .entity(player_entity) + .insert((GlobalTransform::default(), Transform::default())); + + // Adding the components one at a time panics. + // Track this limitation at https://github.com/bevyengine/bevy/issues/5074. + /* + commands + .entity(player_entity) + .insert(GlobalTransform::default()) + .insert(Transform::default()); + */ +}