From 92630e7d3195ebc86084dd9c421ee90db34cc6c1 Mon Sep 17 00:00:00 2001 From: Danila Date: Sat, 7 Jun 2025 18:18:08 +0300 Subject: [PATCH 01/13] Add system parameter for relation quieries --- crates/bevy_ecs/src/system/mod.rs | 2 + crates/bevy_ecs/src/system/related_system.rs | 193 +++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 crates/bevy_ecs/src/system/related_system.rs diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 54e5a781ab..4ce5c38adf 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -129,6 +129,7 @@ mod function_system; mod input; mod observer_system; mod query; +mod related_system; mod schedule_system; mod system; mod system_name; @@ -147,6 +148,7 @@ pub use function_system::*; pub use input::*; pub use observer_system::*; pub use query::*; +pub use related_system::*; pub use schedule_system::*; pub use system::*; pub use system_name::*; diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs new file mode 100644 index 0000000000..57a6804e8f --- /dev/null +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -0,0 +1,193 @@ +use std::iter::Map; + +use bevy_ecs::{ + component::Tick, + entity::Entity, + query::{QueryData, QueryFilter, QueryIter, QueryManyIter, QueryState, With}, + relationship::{Relationship, RelationshipTarget}, + system::{Query, SystemMeta, SystemParam}, + world::{unsafe_world_cell::UnsafeWorldCell, World}, +}; + +// SystemParam for combine 2 related queries +pub struct Related<'w, 's, D: QueryData, R: RelationshipTarget, F: QueryFilter> { + data_query: Query<'w, 's, D, With>, + filter_query: QueryIter<'w, 's, &'static R::Relationship, F>, +} + +impl<'w, 's, D: QueryData, R: RelationshipTarget, F: QueryFilter> Related<'w, 's, D, R, F> { + /// Read iterator + pub fn iter( + &self, + ) -> QueryManyIter< + '_, + '_, + ::ReadOnly, + With, + Map< + QueryIter<'w, 's, &'static R::Relationship, F>, + impl FnMut(&'w R::Relationship) -> Entity, + >, + > { + self.data_query + .iter_many(self.filter_query.clone().map(|r| r.get())) + } + /// Mutate iterator + pub fn iter_mut( + &mut self, + ) -> QueryManyIter< + '_, + '_, + D, + With, + Map< + QueryIter<'w, 's, &'static R::Relationship, F>, + impl FnMut(&'w R::Relationship) -> Entity, + >, + > { + self.data_query + .iter_many_mut(self.filter_query.clone().map(|r| r.get())) + } +} + +/// Just make 2 independent queries and then combine them. +unsafe impl<'w, 's, R: RelationshipTarget + 'static, D: QueryData + 'static, F: QueryFilter + 'static> + SystemParam for Related<'w, 's, D, R, F> +{ + type State = ( + QueryState>, + QueryState<&'static R::Relationship, F>, + ); + type Item<'world, 'state> = Related<'world, 'state, D, R, F>; + + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + let data_query = Query::init_state(world, system_meta); + let filter_query = Query::init_state(world, system_meta); + (data_query, filter_query) + } + + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + _: &SystemMeta, + world: UnsafeWorldCell<'world>, + _: Tick, + ) -> Self::Item<'world, 'state> { + let data_query = unsafe { state.0.query_unchecked_manual(world) }; + let filter_query = unsafe { state.1.query_unchecked_manual(world).into_iter() }; + Related { + data_query, + filter_query, + } + } +} + +#[cfg(test)] +mod tests { + use bevy_ecs::{ + children, + component::Component, + entity::Entity, + hierarchy::{ChildOf, Children}, + query::{With, Without}, + spawn::SpawnRelated, + system::Query, + world::World, + }; + + use super::Related; + + #[derive(Component)] + struct Orc; + #[derive(Component)] + struct Human; + #[derive(Component)] + struct Wolf; + #[derive(Component)] + struct Fangs; + #[derive(Component)] + struct Head; + + #[test] + fn world_test() { + let mut world = World::new(); + + let with_head_sys = world.register_system(with_head); + let with_head_and_fangs_sys = world.register_system(with_head_and_fangs); + let with_head_and_without_fangs_sys = world.register_system(with_head_and_without_fangs); + let test_whs = world.register_system(my_with_head); + let test_whafs = world.register_system(my_with_head_and_fangs); + let test_whawf = world.register_system(my_with_head_and_without_fangs); + + let orc_id = world.spawn((Orc, children![(Head, Fangs)])).id(); + let human_id = world.spawn((Human, children![Head])).id(); + let wolf_id = world.spawn((Wolf, children![(Head, Fangs)])).id(); + + let _ = world.run_system(with_head_sys); + let _ = world.run_system(with_head_and_fangs_sys); + let _ = world.run_system(with_head_and_without_fangs_sys); + + let wolf2_id = world.spawn((Wolf, children![(Head, Fangs)])).id(); + assert_eq!( + world.run_system(with_head_sys).unwrap(), + world.run_system(test_whs).unwrap() + ); + assert_eq!( + world.run_system(with_head_and_fangs_sys).unwrap(), + world.run_system(test_whafs).unwrap() + ); + assert_eq!( + world.run_system(with_head_and_without_fangs_sys).unwrap(), + world.run_system(test_whawf).unwrap() + ); + } + + fn my_with_head(q: Related>) -> usize { + q.iter().count() + } + + fn with_head(q: Query>, q2: Query<&ChildOf, With>) -> usize { + q.iter().fold(0, |acc, e| { + if q2.iter().map(|c| c.parent()).find(|c| *c == e).is_some() { + acc + 1 + } else { + acc + } + }) + } + + fn with_head_and_fangs( + q: Query>, + q2: Query<&ChildOf, (With, With)>, + ) -> usize { + q.iter().fold(0, |acc, e| { + if q2.iter().map(|c| c.parent()).find(|c| *c == e).is_some() { + acc + 1 + } else { + acc + } + }) + } + + fn my_with_head_and_fangs(q: Related, With)>) -> usize { + q.iter().count() + } + + fn with_head_and_without_fangs( + q: Query>, + q2: Query<&ChildOf, (With, Without)>, + ) -> usize { + q.iter().fold(0, |acc, e| { + if q2.iter().map(|c| c.parent()).find(|c| *c == e).is_some() { + acc + 1 + } else { + acc + } + }) + } + + fn my_with_head_and_without_fangs( + q: Related, Without)>, + ) -> usize { + q.iter().count() + } +} From e7f5b2d1f4c58f9d299be813b30f5e954fc0a13d Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 16:46:20 +0300 Subject: [PATCH 02/13] add filter for data query --- crates/bevy_ecs/src/system/related_system.rs | 60 ++++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index 57a6804e8f..071724c522 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -10,55 +10,63 @@ use bevy_ecs::{ }; // SystemParam for combine 2 related queries -pub struct Related<'w, 's, D: QueryData, R: RelationshipTarget, F: QueryFilter> { - data_query: Query<'w, 's, D, With>, - filter_query: QueryIter<'w, 's, &'static R::Relationship, F>, +pub struct Related<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilter> { + data_query: Query<'w, 's, D, (F1, With)>, + filter_query: Query<'w, 's, &'static R::Relationship, F2>, } -impl<'w, 's, D: QueryData, R: RelationshipTarget, F: QueryFilter> Related<'w, 's, D, R, F> { +impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilter> + Related<'w, 's, D, F1, R, F2> +{ /// Read iterator pub fn iter( - &self, + &'w self, ) -> QueryManyIter< - '_, - '_, + 'w, + 's, ::ReadOnly, - With, + (F1, With), Map< - QueryIter<'w, 's, &'static R::Relationship, F>, + QueryIter<'w, 's, &'static R::Relationship, F2>, impl FnMut(&'w R::Relationship) -> Entity, >, > { self.data_query - .iter_many(self.filter_query.clone().map(|r| r.get())) + .iter_many(self.filter_query.iter().map(|r| r.get())) } /// Mutate iterator pub fn iter_mut( - &mut self, + &'w mut self, ) -> QueryManyIter< - '_, - '_, + 'w, + 's, D, - With, + (F1, With), Map< - QueryIter<'w, 's, &'static R::Relationship, F>, + QueryIter<'w, 's, &'static R::Relationship, F2>, impl FnMut(&'w R::Relationship) -> Entity, >, > { self.data_query - .iter_many_mut(self.filter_query.clone().map(|r| r.get())) + .iter_many_mut(self.filter_query.iter().map(|r| r.get())) } } /// Just make 2 independent queries and then combine them. -unsafe impl<'w, 's, R: RelationshipTarget + 'static, D: QueryData + 'static, F: QueryFilter + 'static> - SystemParam for Related<'w, 's, D, R, F> +unsafe impl< + 'w, + 's, + R: RelationshipTarget + 'static, + D: QueryData + 'static, + F1: QueryFilter + 'static, + F2: QueryFilter + 'static, + > SystemParam for Related<'w, 's, D, F1, R, F2> { type State = ( - QueryState>, - QueryState<&'static R::Relationship, F>, + QueryState)>, + QueryState<&'static R::Relationship, F2>, ); - type Item<'world, 'state> = Related<'world, 'state, D, R, F>; + type Item<'world, 'state> = Related<'world, 'state, D, F1, R, F2>; fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { let data_query = Query::init_state(world, system_meta); @@ -73,7 +81,7 @@ unsafe impl<'w, 's, R: RelationshipTarget + 'static, D: QueryData + 'static, F: _: Tick, ) -> Self::Item<'world, 'state> { let data_query = unsafe { state.0.query_unchecked_manual(world) }; - let filter_query = unsafe { state.1.query_unchecked_manual(world).into_iter() }; + let filter_query = unsafe { state.1.query_unchecked_manual(world) }; Related { data_query, filter_query, @@ -141,7 +149,7 @@ mod tests { ); } - fn my_with_head(q: Related>) -> usize { + fn my_with_head(q: Related>) -> usize { q.iter().count() } @@ -168,7 +176,9 @@ mod tests { }) } - fn my_with_head_and_fangs(q: Related, With)>) -> usize { + fn my_with_head_and_fangs( + q: Related, With)>, + ) -> usize { q.iter().count() } @@ -186,7 +196,7 @@ mod tests { } fn my_with_head_and_without_fangs( - q: Related, Without)>, + q: Related, Without)>, ) -> usize { q.iter().count() } From 03c7eb8778b99290705c1a29af9a7f2e0162a3ed Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 16:51:25 +0300 Subject: [PATCH 03/13] add safety comments --- crates/bevy_ecs/src/system/related_system.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index 071724c522..9afbcbe13b 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -69,6 +69,7 @@ unsafe impl< type Item<'world, 'state> = Related<'world, 'state, D, F1, R, F2>; fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + // Register all of query's world accesses let data_query = Query::init_state(world, system_meta); let filter_query = Query::init_state(world, system_meta); (data_query, filter_query) @@ -80,6 +81,10 @@ unsafe impl< world: UnsafeWorldCell<'world>, _: Tick, ) -> Self::Item<'world, 'state> { + // SAFETY: We have registered all of the query's world accesses, + // so the caller ensures that `world` has permission to access any + // world data that the query needs. + // The caller ensures the world matches the one used in init_state. let data_query = unsafe { state.0.query_unchecked_manual(world) }; let filter_query = unsafe { state.1.query_unchecked_manual(world) }; Related { From b772630ed4db3d0a92ceaa0069c85e2f233a9bd5 Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 17:30:22 +0300 Subject: [PATCH 04/13] cleaning generic types --- crates/bevy_ecs/src/system/related_system.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index 9afbcbe13b..43382f375c 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -53,14 +53,12 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt } /// Just make 2 independent queries and then combine them. -unsafe impl< - 'w, - 's, - R: RelationshipTarget + 'static, - D: QueryData + 'static, - F1: QueryFilter + 'static, - F2: QueryFilter + 'static, - > SystemParam for Related<'w, 's, D, F1, R, F2> +unsafe impl<'w, 's, R, D, F1, F2> SystemParam for Related<'w, 's, D, F1, R, F2> +where + R: RelationshipTarget, + D: QueryData + 'static, + F1: QueryFilter + 'static, + F2: QueryFilter + 'static, { type State = ( QueryState)>, From afc71b46ae4b5e0fa0fbbd31c763daf0aeafd56b Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 17:50:45 +0300 Subject: [PATCH 05/13] add get method --- crates/bevy_ecs/src/system/related_system.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index 43382f375c..a069c59a9b 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -9,6 +9,8 @@ use bevy_ecs::{ world::{unsafe_world_cell::UnsafeWorldCell, World}, }; +use crate::error::BevyError; + // SystemParam for combine 2 related queries pub struct Related<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilter> { data_query: Query<'w, 's, D, (F1, With)>, @@ -50,6 +52,22 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt self.data_query .iter_many_mut(self.filter_query.iter().map(|r| r.get())) } + + pub fn get( + &'w self, + entity: Entity, + ) -> Result<<::ReadOnly as QueryData>::Item<'w>, BevyError> { + if self + .filter_query + .iter() + .map(|r| r.get()) + .any(|e| e == entity) + { + return Ok(self.data_query.get(entity)?); + } else { + panic!("as"); + } + } } /// Just make 2 independent queries and then combine them. From c17c742d67277a9fa80559bd3dd1d81ba95afdc6 Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 17:59:29 +0300 Subject: [PATCH 06/13] add error on get --- crates/bevy_ecs/src/system/related_system.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index a069c59a9b..28ccb6885b 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -8,9 +8,15 @@ use bevy_ecs::{ system::{Query, SystemMeta, SystemParam}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; +use derive_more::derive::Display; use crate::error::BevyError; +#[derive(Debug, Display, Copy, Clone)] +pub struct RelatedQueryError; + +impl std::error::Error for RelatedQueryError {} + // SystemParam for combine 2 related queries pub struct Related<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilter> { data_query: Query<'w, 's, D, (F1, With)>, @@ -65,7 +71,7 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt { return Ok(self.data_query.get(entity)?); } else { - panic!("as"); + Err(RelatedQueryError.into()) } } } From d2914bfd722a365d2de55a35d77efe8b50dfda93 Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 18:03:54 +0300 Subject: [PATCH 07/13] add get_mut and contains methods --- crates/bevy_ecs/src/system/related_system.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index 28ccb6885b..c0e9aaf47f 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -63,13 +63,25 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt &'w self, entity: Entity, ) -> Result<<::ReadOnly as QueryData>::Item<'w>, BevyError> { - if self + if self.contains(entity) { + Ok(self.data_query.get(entity)?) + } else { + Err(RelatedQueryError.into()) + } + } + + pub fn contains(&self, entity: Entity) -> bool { + return self .filter_query .iter() .map(|r| r.get()) .any(|e| e == entity) - { - return Ok(self.data_query.get(entity)?); + && self.data_query.contains(entity); + } + + pub fn get_mut(&'w mut self, entity: Entity) -> Result<::Item<'w>, BevyError> { + if self.contains(entity) { + Ok(self.data_query.get_mut(entity)?) } else { Err(RelatedQueryError.into()) } From f359c3f8f15fe9086115d5ffb4bc41546cf40989 Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 18:59:12 +0300 Subject: [PATCH 08/13] error refactoring --- crates/bevy_ecs/src/system/related_system.rs | 34 ++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index c0e9aaf47f..f0a94497e7 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -10,12 +10,15 @@ use bevy_ecs::{ }; use derive_more::derive::Display; -use crate::error::BevyError; +use crate::{error::BevyError, query::QueryEntityError}; #[derive(Debug, Display, Copy, Clone)] -pub struct RelatedQueryError; +pub enum RelatedQueryEntityError { + RelationshipEntityError(QueryEntityError), + RelationshipTargetEntityError(Entity), +} -impl std::error::Error for RelatedQueryError {} +impl std::error::Error for RelatedQueryEntityError {} // SystemParam for combine 2 related queries pub struct Related<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilter> { @@ -62,11 +65,16 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt pub fn get( &'w self, entity: Entity, - ) -> Result<<::ReadOnly as QueryData>::Item<'w>, BevyError> { + ) -> Result<<::ReadOnly as QueryData>::Item<'w>, RelatedQueryEntityError> { if self.contains(entity) { - Ok(self.data_query.get(entity)?) + match self.data_query.get(entity) { + Ok(item) => return Ok(item), + Err(err) => return Err(RelatedQueryEntityError::RelationshipEntityError(err)), + } } else { - Err(RelatedQueryError.into()) + return Err(RelatedQueryEntityError::RelationshipTargetEntityError( + entity, + )); } } @@ -79,11 +87,19 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt && self.data_query.contains(entity); } - pub fn get_mut(&'w mut self, entity: Entity) -> Result<::Item<'w>, BevyError> { + pub fn get_mut( + &'w mut self, + entity: Entity, + ) -> Result<::Item<'w>, RelatedQueryEntityError> { if self.contains(entity) { - Ok(self.data_query.get_mut(entity)?) + match self.data_query.get_mut(entity) { + Ok(item) => return Ok(item), + Err(err) => return Err(RelatedQueryEntityError::RelationshipEntityError(err)), + } } else { - Err(RelatedQueryError.into()) + return Err(RelatedQueryEntityError::RelationshipTargetEntityError( + entity, + )); } } } From 2dd3d9ab34ef29842f17f89d939912def3fdd7b9 Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 19:26:47 +0300 Subject: [PATCH 09/13] add doc for related system param --- crates/bevy_ecs/src/system/related_system.rs | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index f0a94497e7..d69183c8d4 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -20,7 +20,28 @@ pub enum RelatedQueryEntityError { impl std::error::Error for RelatedQueryEntityError {} -// SystemParam for combine 2 related queries +/// A Query like [system parameter] that provides selective access to the [`Component`] of data stored in a [`World`], +/// where source of relationship match filter. +/// +/// `Related` is a generic data structure that accepts four type parameters: +/// +/// - **`D` (query data)**: +/// The type of data fetched by the query, which will be returned as the query item. +/// Only entities that match the requested data will generate an item. +/// Must implement the [`QueryData`] trait. +/// - **`F1` (query filter)**: +/// The set of conditions that determine whether query items should be kept or discarded from +/// entities with source of relationship query. +/// Must implement the [`QueryFilter`] trait. +/// - **`R` (relationship target) +/// The target of the relationship, in relation to which the filtering by `F2` will be performed. +/// If the related entity contains as the target of the relationship an entity satisfying `F2`, +/// then it will correspond to the query. +/// Must implement the [`RelationshipTarget`] trait. +/// - **`F2` (query filter) +/// The set of conditions that determine whether query items should be kept or discarded +/// for relationship target query. +/// Must implement the [`QueryFilter`] trait. pub struct Related<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilter> { data_query: Query<'w, 's, D, (F1, With)>, filter_query: Query<'w, 's, &'static R::Relationship, F2>, From 3ef67902516d5dff3d1bcfa53679caa6f111d87e Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 19:35:12 +0300 Subject: [PATCH 10/13] add related error doc --- crates/bevy_ecs/src/system/related_system.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index d69183c8d4..18cdcf3276 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -12,9 +12,12 @@ use derive_more::derive::Display; use crate::{error::BevyError, query::QueryEntityError}; -#[derive(Debug, Display, Copy, Clone)] +/// An error that occurs when retrieving a specific Entity’s query result from [`Related`]. +#[derive(Debug, Display, Copy, Clone, PartialEq, Eq)] pub enum RelatedQueryEntityError { + /// Error retrieving data by source relationship entity. RelationshipEntityError(QueryEntityError), + /// [`Entity`] does not have a relationship target that satisfies `F2`. RelationshipTargetEntityError(Entity), } From 9632a4e29e1b5f92f61e4d84071a022c91536d43 Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 19:43:22 +0300 Subject: [PATCH 11/13] add related methods doc --- crates/bevy_ecs/src/system/related_system.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index 18cdcf3276..6bc00c3d83 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -10,7 +10,7 @@ use bevy_ecs::{ }; use derive_more::derive::Display; -use crate::{error::BevyError, query::QueryEntityError}; +use crate::query::QueryEntityError; /// An error that occurs when retrieving a specific Entity’s query result from [`Related`]. #[derive(Debug, Display, Copy, Clone, PartialEq, Eq)] @@ -53,7 +53,7 @@ pub struct Related<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilter> Related<'w, 's, D, F1, R, F2> { - /// Read iterator + /// Returns an [`Iterator`] over the read-only items. pub fn iter( &'w self, ) -> QueryManyIter< @@ -69,7 +69,7 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt self.data_query .iter_many(self.filter_query.iter().map(|r| r.get())) } - /// Mutate iterator + /// Returns an [`Iterator`] over items for mutation. pub fn iter_mut( &'w mut self, ) -> QueryManyIter< @@ -86,6 +86,7 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt .iter_many_mut(self.filter_query.iter().map(|r| r.get())) } + /// Returns the read-only item for the given [`Entity`]. pub fn get( &'w self, entity: Entity, @@ -102,6 +103,7 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt } } + /// Returns `true` if the given [`Entity`] matches the relative query. pub fn contains(&self, entity: Entity) -> bool { return self .filter_query @@ -111,6 +113,7 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt && self.data_query.contains(entity); } + /// Returns the mutating item for the given [`Entity`]. pub fn get_mut( &'w mut self, entity: Entity, From 030425d666b0cc17688898ec4b80a8ceed028fe8 Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 20:05:31 +0300 Subject: [PATCH 12/13] fix test --- crates/bevy_ecs/src/system/related_system.rs | 24 ++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index 6bc00c3d83..c47a201825 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -208,15 +208,25 @@ mod tests { let test_whafs = world.register_system(my_with_head_and_fangs); let test_whawf = world.register_system(my_with_head_and_without_fangs); - let orc_id = world.spawn((Orc, children![(Head, Fangs)])).id(); - let human_id = world.spawn((Human, children![Head])).id(); - let wolf_id = world.spawn((Wolf, children![(Head, Fangs)])).id(); + let _orc_id = world.spawn((Orc, children![(Head, Fangs)])).id(); + let _human_id = world.spawn((Human, children![Head])).id(); + let _wolf_id = world.spawn((Wolf, children![(Head, Fangs)])).id(); - let _ = world.run_system(with_head_sys); - let _ = world.run_system(with_head_and_fangs_sys); - let _ = world.run_system(with_head_and_without_fangs_sys); + assert_eq!( + world.run_system(with_head_sys).unwrap(), + world.run_system(test_whs).unwrap() + ); + assert_eq!( + world.run_system(with_head_and_fangs_sys).unwrap(), + world.run_system(test_whafs).unwrap() + ); + assert_eq!( + world.run_system(with_head_and_without_fangs_sys).unwrap(), + world.run_system(test_whawf).unwrap() + ); + + let _wolf2_id = world.spawn((Wolf, children![(Head, Fangs)])).id(); - let wolf2_id = world.spawn((Wolf, children![(Head, Fangs)])).id(); assert_eq!( world.run_system(with_head_sys).unwrap(), world.run_system(test_whs).unwrap() From cf6100133264ce3c7b407690a13e3dce0d61f61c Mon Sep 17 00:00:00 2001 From: Danila Date: Sun, 8 Jun 2025 21:23:25 +0300 Subject: [PATCH 13/13] fix ci tests --- crates/bevy_ecs/src/system/related_system.rs | 42 ++++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/crates/bevy_ecs/src/system/related_system.rs b/crates/bevy_ecs/src/system/related_system.rs index c47a201825..3c1e6e2d4c 100644 --- a/crates/bevy_ecs/src/system/related_system.rs +++ b/crates/bevy_ecs/src/system/related_system.rs @@ -1,4 +1,4 @@ -use std::iter::Map; +use core::iter::Map; use bevy_ecs::{ component::Tick, @@ -21,14 +21,17 @@ pub enum RelatedQueryEntityError { RelationshipTargetEntityError(Entity), } -impl std::error::Error for RelatedQueryEntityError {} +impl core::error::Error for RelatedQueryEntityError {} /// A Query like [system parameter] that provides selective access to the [`Component`] of data stored in a [`World`], /// where source of relationship match filter. /// +/// [system parameter]: crate::system::SystemParam +/// [`Component`]: crate::component::Component +/// /// `Related` is a generic data structure that accepts four type parameters: /// -/// - **`D` (query data)**: +/// - **`D` (query data)**: /// The type of data fetched by the query, which will be returned as the query item. /// Only entities that match the requested data will generate an item. /// Must implement the [`QueryData`] trait. @@ -67,7 +70,7 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt >, > { self.data_query - .iter_many(self.filter_query.iter().map(|r| r.get())) + .iter_many(self.filter_query.iter().map(Relationship::get)) } /// Returns an [`Iterator`] over items for mutation. pub fn iter_mut( @@ -83,7 +86,7 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt >, > { self.data_query - .iter_many_mut(self.filter_query.iter().map(|r| r.get())) + .iter_many_mut(self.filter_query.iter().map(Relationship::get)) } /// Returns the read-only item for the given [`Entity`]. @@ -93,13 +96,13 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt ) -> Result<<::ReadOnly as QueryData>::Item<'w>, RelatedQueryEntityError> { if self.contains(entity) { match self.data_query.get(entity) { - Ok(item) => return Ok(item), - Err(err) => return Err(RelatedQueryEntityError::RelationshipEntityError(err)), + Ok(item) => Ok(item), + Err(err) => Err(RelatedQueryEntityError::RelationshipEntityError(err)), } } else { - return Err(RelatedQueryEntityError::RelationshipTargetEntityError( + Err(RelatedQueryEntityError::RelationshipTargetEntityError( entity, - )); + )) } } @@ -108,7 +111,7 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt return self .filter_query .iter() - .map(|r| r.get()) + .map(Relationship::get) .any(|e| e == entity) && self.data_query.contains(entity); } @@ -120,18 +123,19 @@ impl<'w, 's, D: QueryData, F1: QueryFilter, R: RelationshipTarget, F2: QueryFilt ) -> Result<::Item<'w>, RelatedQueryEntityError> { if self.contains(entity) { match self.data_query.get_mut(entity) { - Ok(item) => return Ok(item), - Err(err) => return Err(RelatedQueryEntityError::RelationshipEntityError(err)), + Ok(item) => Ok(item), + Err(err) => Err(RelatedQueryEntityError::RelationshipEntityError(err)), } } else { - return Err(RelatedQueryEntityError::RelationshipTargetEntityError( + Err(RelatedQueryEntityError::RelationshipTargetEntityError( entity, - )); + )) } } } /// Just make 2 independent queries and then combine them. +/// SAFETY: delegates safety to [`Query`] for `ComponentId` and `ArchetypeComponentId` access. unsafe impl<'w, 's, R, D, F1, F2> SystemParam for Related<'w, 's, D, F1, R, F2> where R: RelationshipTarget, @@ -163,6 +167,10 @@ where // world data that the query needs. // The caller ensures the world matches the one used in init_state. let data_query = unsafe { state.0.query_unchecked_manual(world) }; + // SAFETY: We have registered all of the query's world accesses, + // so the caller ensures that `world` has permission to access any + // world data that the query needs. + // The caller ensures the world matches the one used in init_state. let filter_query = unsafe { state.1.query_unchecked_manual(world) }; Related { data_query, @@ -247,7 +255,7 @@ mod tests { fn with_head(q: Query>, q2: Query<&ChildOf, With>) -> usize { q.iter().fold(0, |acc, e| { - if q2.iter().map(|c| c.parent()).find(|c| *c == e).is_some() { + if q2.iter().map(ChildOf::parent).any(|c| c == e) { acc + 1 } else { acc @@ -260,7 +268,7 @@ mod tests { q2: Query<&ChildOf, (With, With)>, ) -> usize { q.iter().fold(0, |acc, e| { - if q2.iter().map(|c| c.parent()).find(|c| *c == e).is_some() { + if q2.iter().map(ChildOf::parent).any(|c| c == e) { acc + 1 } else { acc @@ -279,7 +287,7 @@ mod tests { q2: Query<&ChildOf, (With, Without)>, ) -> usize { q.iter().fold(0, |acc, e| { - if q2.iter().map(|c| c.parent()).find(|c| *c == e).is_some() { + if q2.iter().map(ChildOf::parent).any(|c| c == e) { acc + 1 } else { acc