diff --git a/Cargo.toml b/Cargo.toml index 6fdb2b37f4989..7a1f5fb78b97d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1892,6 +1892,17 @@ description = "Run systems only when one or multiple conditions are met" category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "fallible_params" +path = "examples/ecs/fallible_params.rs" +doc-scrape-examples = true + +[package.metadata.example.fallible_params] +name = "Fallible System Parameters" +description = "Systems are skipped if their parameters cannot be acquired" +category = "ECS (Entity Component System)" +wasm = false + [[example]] name = "startup_system" path = "examples/ecs/startup_system.rs" diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 2c7c6615fce4c..5a3adac96fe20 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -543,11 +543,10 @@ impl<'w> From> for Ticks<'w> { /// /// If you need a unique mutable borrow, use [`ResMut`] instead. /// -/// # Panics +/// This [`SystemParam`](crate::system::SystemParam) fails validation if resource doesn't exist. +/// This will cause systems that use this parameter to be skipped. /// -/// Panics when used as a [`SystemParameter`](crate::system::SystemParam) if the resource does not exist. -/// -/// Use `Option>` instead if the resource might not always exist. +/// Use [`Option>`] instead if the resource might not always exist. pub struct Res<'w, T: ?Sized + Resource> { pub(crate) value: &'w T, pub(crate) ticks: Ticks<'w>, @@ -622,11 +621,10 @@ impl_debug!(Res<'w, T>, Resource); /// /// If you need a shared borrow, use [`Res`] instead. /// -/// # Panics -/// -/// Panics when used as a [`SystemParam`](crate::system::SystemParam) if the resource does not exist. +/// This [`SystemParam`](crate::system::SystemParam) fails validation if resource doesn't exist. +/// This will cause systems that use this parameter to be skipped. /// -/// Use `Option>` instead if the resource might not always exist. +/// Use [`Option>`] instead if the resource might not always exist. pub struct ResMut<'w, T: ?Sized + Resource> { pub(crate) value: &'w mut T, pub(crate) ticks: TicksMut<'w>, @@ -684,11 +682,10 @@ impl<'w, T: Resource> From> for Mut<'w, T> { /// the scheduler to instead run the system on the main thread so that it doesn't send the resource /// over to another thread. /// -/// # Panics -/// -/// Panics when used as a `SystemParameter` if the resource does not exist. +/// This [`SystemParam`](crate::system::SystemParam) fails validation if non-send resource doesn't exist. +/// This will cause systems that use this parameter to be skipped. /// -/// Use `Option>` instead if the resource might not always exist. +/// Use [`Option>`] instead if the resource might not always exist. pub struct NonSendMut<'w, T: ?Sized + 'static> { pub(crate) value: &'w mut T, pub(crate) ticks: TicksMut<'w>, diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 69dfea19f5bda..c7d49dfa3a74e 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -58,8 +58,9 @@ pub mod prelude { }, system::{ Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef, IntoSystem, Local, - NonSend, NonSendMut, ParallelCommands, ParamSet, Query, ReadOnlySystem, Res, ResMut, - Resource, System, SystemIn, SystemInput, SystemParamBuilder, SystemParamFunction, + NonSend, NonSendMut, ParallelCommands, ParamSet, Query, QuerySingle, ReadOnlySystem, + Res, ResMut, Resource, System, SystemIn, SystemInput, SystemParamBuilder, + SystemParamFunction, }, world::{ Command, EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 83681ae5b686f..13155057ff91b 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -8,7 +8,11 @@ use crate::{ }, world::unsafe_world_cell::UnsafeWorldCell, }; -use core::borrow::Borrow; +use core::{ + borrow::Borrow, + marker::PhantomData, + ops::{Deref, DerefMut}, +}; /// [System parameter] that provides selective access to the [`Component`] data stored in a [`World`]. /// @@ -1629,3 +1633,37 @@ impl<'w, 'q, Q: QueryData, F: QueryFilter> From<&'q mut Query<'w, '_, Q, F>> value.transmute_lens_filtered() } } + +/// [System parameter] that provides access to single entity's components, much like [`Query::single`]/[`Query::single_mut`]. +/// +/// This [`SystemParam`](crate::system::SystemParam) fails validation if zero or more than one matching entity exists. +/// This will cause systems that use this parameter to be skipped. +/// +/// Use [`Option>`] instead if zero or one matching entities can exist. +/// +/// See [`Query`] for more details. +pub struct QuerySingle<'w, D: QueryData, F: QueryFilter = ()> { + pub(crate) item: D::Item<'w>, + pub(crate) _filter: PhantomData, +} + +impl<'w, D: QueryData, F: QueryFilter> Deref for QuerySingle<'w, D, F> { + type Target = D::Item<'w>; + + fn deref(&self) -> &Self::Target { + &self.item + } +} + +impl<'w, D: QueryData, F: QueryFilter> DerefMut for QuerySingle<'w, D, F> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.item + } +} + +impl<'w, D: QueryData, F: QueryFilter> QuerySingle<'w, D, F> { + /// Returns the inner item with ownership. + pub fn into_inner(self) -> D::Item<'w> { + self.item + } +} diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 63ef1a16c3d17..7741f3cb80746 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -104,6 +104,10 @@ pub trait System: Send + Sync + 'static { /// is not a strict requirement, both [`System::run`] and [`System::run_unsafe`] /// should provide their own safety mechanism to prevent undefined behavior. /// + /// This method has to be called directly before [`System::run_unsafe`] with no other (relevant) + /// world mutations inbetween. Otherwise, while it won't lead to any undefined behavior, + /// the validity of the param may change. + /// /// # Safety /// /// - The caller must ensure that [`world`](UnsafeWorldCell) has permission to access any world data diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index ba959bb86aca7..fdcf36a018ba3 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -7,10 +7,10 @@ use crate::{ entity::Entities, query::{ Access, AccessConflicts, FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, - QueryState, ReadOnlyQueryData, + QuerySingleError, QueryState, ReadOnlyQueryData, }, storage::{ResourceData, SparseSetIndex}, - system::{Query, SystemMeta}, + system::{Query, QuerySingle, SystemMeta}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, }; use bevy_ecs_macros::impl_param_set; @@ -227,12 +227,21 @@ pub unsafe trait SystemParam: Sized { /// The [`world`](UnsafeWorldCell) can only be used to read param's data /// and world metadata. No data can be written. /// + /// When using system parameters that require `change_tick` you can use + /// [`UnsafeWorldCell::change_tick()`]. Even if this isn't the exact + /// same tick used for [`SystemParam::get_param`], the world access + /// ensures that the queried data will be the same in both calls. + /// + /// This method has to be called directly before [`SystemParam::get_param`] with no other (relevant) + /// world mutations inbetween. Otherwise, while it won't lead to any undefined behavior, + /// the validity of the param may change. + /// /// # Safety /// /// - The passed [`UnsafeWorldCell`] must have read-only access to world data /// registered in [`init_state`](SystemParam::init_state). /// - `world` must be the same [`World`] that was used to initialize [`state`](SystemParam::init_state). - /// - all `world`'s archetypes have been processed by [`new_archetype`](SystemParam::new_archetype). + /// - All `world`'s archetypes have been processed by [`new_archetype`](SystemParam::new_archetype). unsafe fn validate_param( _state: &Self::State, _system_meta: &SystemMeta, @@ -356,6 +365,140 @@ fn assert_component_access_compatibility( panic!("error[B0001]: Query<{query_type}, {filter_type}> in system {system_name} accesses component(s){accesses} in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001"); } +// SAFETY: Relevant query ComponentId and ArchetypeComponentId access is applied to SystemMeta. If +// this Query conflicts with any prior access, a panic will occur. +unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam + for QuerySingle<'a, D, F> +{ + type State = QueryState; + type Item<'w, 's> = QuerySingle<'w, D, F>; + + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + Query::init_state(world, system_meta) + } + + unsafe fn new_archetype( + state: &mut Self::State, + archetype: &Archetype, + system_meta: &mut SystemMeta, + ) { + // SAFETY: Delegate to existing `SystemParam` implementations. + unsafe { Query::new_archetype(state, archetype, system_meta) }; + } + + #[inline] + unsafe fn get_param<'w, 's>( + state: &'s mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + change_tick: Tick, + ) -> Self::Item<'w, 's> { + state.validate_world(world.id()); + // SAFETY: State ensures that the components it accesses are not accessible somewhere elsewhere. + let result = + unsafe { state.get_single_unchecked_manual(world, system_meta.last_run, change_tick) }; + let single = + result.expect("The query was expected to contain exactly one matching entity."); + QuerySingle { + item: single, + _filter: PhantomData, + } + } + + #[inline] + unsafe fn validate_param( + state: &Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell, + ) -> bool { + state.validate_world(world.id()); + // SAFETY: State ensures that the components it accesses are not mutably accessible elsewhere + // and the query is read only. + let result = unsafe { + state.as_readonly().get_single_unchecked_manual( + world, + system_meta.last_run, + world.change_tick(), + ) + }; + result.is_ok() + } +} + +// SAFETY: Relevant query ComponentId and ArchetypeComponentId access is applied to SystemMeta. If +// this Query conflicts with any prior access, a panic will occur. +unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam + for Option> +{ + type State = QueryState; + type Item<'w, 's> = Option>; + + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + QuerySingle::init_state(world, system_meta) + } + + unsafe fn new_archetype( + state: &mut Self::State, + archetype: &Archetype, + system_meta: &mut SystemMeta, + ) { + // SAFETY: Delegate to existing `SystemParam` implementations. + unsafe { QuerySingle::new_archetype(state, archetype, system_meta) }; + } + + #[inline] + unsafe fn get_param<'w, 's>( + state: &'s mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + change_tick: Tick, + ) -> Self::Item<'w, 's> { + state.validate_world(world.id()); + // SAFETY: State ensures that the components it accesses are not accessible elsewhere. + let result = + unsafe { state.get_single_unchecked_manual(world, system_meta.last_run, change_tick) }; + match result { + Ok(single) => Some(QuerySingle { + item: single, + _filter: PhantomData, + }), + Err(QuerySingleError::NoEntities(_)) => None, + Err(QuerySingleError::MultipleEntities(e)) => panic!("{}", e), + } + } + + #[inline] + unsafe fn validate_param( + state: &Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell, + ) -> bool { + state.validate_world(world.id()); + // SAFETY: State ensures that the components it accesses are not mutably accessible elsewhere + // and the query is read only. + let result = unsafe { + state.as_readonly().get_single_unchecked_manual( + world, + system_meta.last_run, + world.change_tick(), + ) + }; + !matches!(result, Err(QuerySingleError::MultipleEntities(_))) + } +} + +// SAFETY: QueryState is constrained to read-only fetches, so it only reads World. +unsafe impl<'a, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam + for QuerySingle<'a, D, F> +{ +} + +// SAFETY: QueryState is constrained to read-only fetches, so it only reads World. +unsafe impl<'a, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam + for Option> +{ +} + /// A collection of potentially conflicting [`SystemParam`]s allowed by disjoint access. /// /// Allows systems to safely access and interact with up to 8 mutually exclusive [`SystemParam`]s, such as @@ -1172,11 +1315,10 @@ unsafe impl SystemParam for Deferred<'_, T> { /// the scheduler to instead run the system on the main thread so that it doesn't send the resource /// over to another thread. /// -/// # Panics -/// -/// Panics when used as a `SystemParameter` if the resource does not exist. +/// This [`SystemParam`] fails validation if non-send resource doesn't exist. +/// This will cause systems that use this parameter to be skipped. /// -/// Use `Option>` instead if the resource might not always exist. +/// Use [`Option>`] instead if the resource might not always exist. pub struct NonSend<'w, T: 'static> { pub(crate) value: &'w T, ticks: ComponentTicks, diff --git a/examples/README.md b/examples/README.md index 616e37648aed0..96b7c577a4417 100644 --- a/examples/README.md +++ b/examples/README.md @@ -288,6 +288,7 @@ Example | Description [Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components [ECS Guide](../examples/ecs/ecs_guide.rs) | Full guide to Bevy's ECS [Event](../examples/ecs/event.rs) | Illustrates event creation, activation, and reception +[Fallible System Parameters](../examples/ecs/fallible_params.rs) | Systems are skipped if their parameters cannot be acquired [Fixed Timestep](../examples/ecs/fixed_timestep.rs) | Shows how to create systems that run every fixed timestep, rather than every tick [Generic System](../examples/ecs/generic_system.rs) | Shows how to create systems that can be reused with different types [Hierarchy](../examples/ecs/hierarchy.rs) | Creates a hierarchy of parents and children entities diff --git a/examples/ecs/fallible_params.rs b/examples/ecs/fallible_params.rs new file mode 100644 index 0000000000000..6965fbcf1d321 --- /dev/null +++ b/examples/ecs/fallible_params.rs @@ -0,0 +1,147 @@ +//! This example demonstrates how fallible parameters can prevent their systems +//! from running if their acquiry conditions aren't met. +//! +//! Fallible parameters include: +//! - [`Res`], [`ResMut`] - If resource doesn't exist. +//! - [`QuerySingle`] - If there is no or more than one entities matching. +//! - [`Option>`] - If there are more than one entities matching. + +use bevy::prelude::*; +use rand::Rng; + +fn main() { + println!(); + println!("Press 'A' to add enemy ships and 'R' to remove them."); + println!("Player ship will wait for enemy ships and track one if it exists,"); + println!("but will stop tracking if there are more than one."); + println!(); + + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + // We add all the systems one after another. + // We don't need to use run conditions here. + .add_systems(Update, (user_input, move_targets, move_pointer).chain()) + .run(); +} + +/// Enemy component stores data for movement in a circle. +#[derive(Component, Default)] +struct Enemy { + origin: Vec2, + radius: f32, + rotation: f32, + rotation_speed: f32, +} + +/// Player component stores data for going after enemies. +#[derive(Component, Default)] +struct Player { + speed: f32, + rotation_speed: f32, + min_follow_radius: f32, +} + +fn setup(mut commands: Commands, asset_server: Res) { + // Spawn 2D camera. + commands.spawn(Camera2dBundle::default()); + + // Spawn player. + let texture = asset_server.load("textures/simplespace/ship_C.png"); + commands.spawn(( + Player { + speed: 100.0, + rotation_speed: 2.0, + min_follow_radius: 50.0, + }, + SpriteBundle { + sprite: Sprite { + color: bevy::color::palettes::tailwind::BLUE_800.into(), + ..default() + }, + transform: Transform::from_translation(Vec3::ZERO), + texture, + ..default() + }, + )); +} + +// System that reads user input. +// If user presses 'A' we spawn a new random enemy. +// If user presses 'R' we remove a random enemy (if any exist). +fn user_input( + mut commands: Commands, + enemies: Query>, + keyboard_input: Res>, + asset_server: Res, +) { + let mut rng = rand::thread_rng(); + if keyboard_input.just_pressed(KeyCode::KeyA) { + let texture = asset_server.load("textures/simplespace/enemy_A.png"); + commands.spawn(( + Enemy { + origin: Vec2::new(rng.gen_range(-200.0..200.0), rng.gen_range(-200.0..200.0)), + radius: rng.gen_range(50.0..150.0), + rotation: rng.gen_range(0.0..std::f32::consts::TAU), + rotation_speed: rng.gen_range(0.5..1.5), + }, + SpriteBundle { + sprite: Sprite { + color: bevy::color::palettes::tailwind::RED_800.into(), + ..default() + }, + transform: Transform::from_translation(Vec3::ZERO), + texture, + ..default() + }, + )); + } + + if keyboard_input.just_pressed(KeyCode::KeyR) { + if let Some(entity) = enemies.iter().next() { + commands.entity(entity).despawn(); + } + } +} + +// System that moves the enemies in a circle. +// TODO: Use [`NonEmptyQuery`] when it exists. +fn move_targets(mut enemies: Query<(&mut Transform, &mut Enemy)>, time: Res