Replies: 7 comments
-
Maybe an example: a small system that checks which gardeners have planted a flower, and updates a tilemap accordingly. fn plant_flower_system(
query: Query<(&Gardener, )>,
mut tilemap: ResMut<TileMap>,
) {
for (gardener,) in query.iter() {
if gardener.has_flower {
tilemap.set_tile(/* ... */);
}
}
} Now I extend this system and want to play an animation when the flower is planted, so I need mutable access to an fn plant_flower_system(
mut query: Query<(&Gardener, &mut Animation)>,
mut tilemap: ResMut<TileMap>,
) {
for (gardener, mut anim) in query.iter_mut() {
if gardener.has_flower {
tilemap.set_tile(/* ... */);
anim.play("plant_flower");
}
}
} As you see, this requires changing the iterator as well as the parameter's mutability.
This is rather repetitive and not the nicest from an ergonomics point of view; I can understand if this is strictly required for safety however. But maybe there's another way? |
Beta Was this translation helpful? Give feedback.
-
Yeah this is a safety issue. We can't allow multiple mutable queries because it could result in aliased mutability. Nesting query iterations is one way this could happen, but there are other more common patters, such as Instead we made it possible to define multiple queries with separate "lifetimes". As long as they don't conflict, you can mutably borrow all of them at the same time. If they do conflict, you need to group them together in QuerySets (to ensure a single borrow can happen at a given time). This is a "runtime check" but it only needs to happen once instead of on every access. There is no better way to do this that I'm aware of without incurring a significant runtime cost. Legion uses "world splitting", which is less ergonomic and harder to reason about (imo). But the general idea is similar to the approach we use now. I'll leave this open for a bit in case anyone has ideas, but I'm inclined to close this as I've already put a lot of thought into the problem. We can open new issues if someone finds a better alternative to the current approach, but I'm skeptical that it will happen. |
Beta Was this translation helpful? Give feedback.
-
Thank you very much for your answer! So, indeed the issue is exclusive/unique access to the components, which can only be statically enforced using It seems like One idea that came to my mind is the type-state pattern, which is an alternative way of modeling mutually exclusive "feature-sets" -- here the read-only and read-write accessors in a query: // Type tags to denote a query "read-only" or "mutable"
trait State {}
struct Mutable;
struct ReadOnly;
impl State for Mutable {}
impl State for ReadOnly {}
pub struct Query<T, S: State> {
_ph: PhantomData<(T, S)>,
}
impl<T, S: State> Query<T, S> {
pub fn new() -> Self { Self { _ph: PhantomData } }
} The query could have distinct implementations, depending on the impl<T> Query<T, Mutable> {
pub fn iter(&mut self) { println!("mutable iter()"); }
}
impl<T> Query<T, ReadOnly> {
pub fn iter(&self) { println!("read-only iter()"); }
} And some metaprogramming could even allow to infer the trait Decide {
type State: State;
}
impl<T> Decide for &T {
type State = ReadOnly;
}
impl<T> Decide for &mut T {
type State = Mutable;
}
// so the declaration becomes:
pub struct Query<T, S: State = <T as Decide>::State> {
_ph: PhantomData<(T, S)>,
} Now applied, this would look as follows: pub fn test() {
let mut qmut = Query::<&mut i32>::new(); // Mutable inferred
let qread = Query::<&i32>::new(); // ReadOnly inferred
qmut.iter(); // prints "mutable iter()"
qread.iter(); // prints "read-only iter()"
} Applied to this specific issue, it would allow a small ergonomic improvement (mostly allow to write |
Beta Was this translation helpful? Give feedback.
-
Personally I think calling But maybe the error messages are a bit easier to understand if you try to call
Instead of something like:
|
Beta Was this translation helpful? Give feedback.
-
As mentioned above, there's already 4 places where "mut" appears in one form or another, so there's enough repetition to not miss it. Furthermore, the important part to convey is which components are mutated by a system -- the query itself is a means to get those components, and the fact that it needs If I have for (spatial, damage, mut health) in query.iter() { ... } , I know immediately that this system mutates health but not any other components. This mutation is additionally expressed twice in the function signature, which is the first thing you see if you look at a system: fn system(
mut query: Query<(&Spatial, &Damage &mut Health)> )
// ^ ^
// mutates anything mutates this component (ok, the top-level Or, to compare it with resources: you specify only at the function signature whether they are |
Beta Was this translation helpful? Give feedback.
-
@NathanSWard, as discussed in #2271, would you like me to assign this to you with the understanding that it will likely be a couple weeks until you get to it? |
Beta Was this translation helpful? Give feedback.
-
I don't think we should assign work until someone has "started" on it. It should be first-come-first serve by default. |
Beta Was this translation helpful? Give feedback.
-
Hello everyone!
On https://bevyengine.org/news/bevy-0-4/, there's this code snippet:
Since
iter_mut()
takes&mut self
, the function parameter should bemut query
, no?Given that, I'll try to fill out the feature template, although it's more of a general API discussion than a concrete request 🙂
What problem does this solve or what need does it fill?
For me, it's not immediately obvious that
Query::iter_mut()
operates on&mut self
, as logically, the query itself is not mutated, but rather the components being queried.Also,
iter()
already provides a specific trait boundReadOnlyFetch
, so it cannot be used for mutable queries:Is the actual reason to enforce unique access (non-aliasing), so that no two mutable queries can run at the same time? The only thing I can imagine here is doing that in a nested loop, and doing so would probably not make sense for
iter()
queries either.What solution would you like?
If compatible with safety, allowing
iter_mut()
on&self
could be considered.If not, we can document the background why
&mut self
is used.What alternative(s) have you considered?
Leaving everything as is, fixing the 0.4 release notes and improving documentation.
Additional context
I looked at #753 (aligning with Rust conventions) and #796 (a bug related to safety, although not directly related to this).
Beta Was this translation helpful? Give feedback.
All reactions