-
Hey everybody! I'm currently working on laying the foundation for dynamic components in systems in Bevy. I've already started work on that in #623. Now I would like to get some ideas, though, particularly around how to refactor Queries in a way that will support making a query based on a runtime-determined component ID. Let's get some context. Component ID'sThe first step that I did in #623 was I converted all of the uses of The GoalSo the current goal is to be able to define a system without knowing which components it's going to access at compile time. This system needs to be able to make a For instance, a query in Bevy looks like pub trait Query {
type Fetch: for<'a> Fetch<'a>;
} And the pub trait Fetch<'a>: Sized {
type Item;
fn access(archetype: &Archetype) -> Option<Access>;
fn borrow(archetype: &Archetype);
unsafe fn get(archetype: &'a Archetype, offset: usize) -> Option<Self>;
fn release(archetype: &Archetype);
unsafe fn should_skip(&self) -> bool {
false
}
unsafe fn next(&mut self) -> Self::Item;
} So the gist of this is that the This issue is that, when queries need to be instantiated at runtime, we need to be able to implement the Possible SolutionsModify
|
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 7 replies
-
This is hard. I think in the short term a new DynamicQuery specifically for this case is the right approach, even if it means a bit of code duplication. Medium term, theres a chance we'll need to implement a "stateful query" anyway to support 'system execution order independent change tracking'. The design we've discussed so far would look something like this: // state contains a "last change count" for Changed<T>
world.stateful_query::<(Changed<T>, &U)>(state); Not sure if theres a way to reconcile that with the requirements here / if we should. Another thing to consider is that Fetch is a "hot spot" and any change has a good chance of regressing performance. |
Beta Was this translation helpful? Give feedback.
-
OK, I just noticed something really weird. So I decided to mess around with These are the changes I applied: katharostech@efa4ec3 ( Sorry for the noise, but some of those changes are somewhat unrelated. The interesting stuff is all in the According to @cart's
Switching back to the code before the commit and running the benchmarks:
The changes for bevy_foreach and bevy_foreach_entity seem to just be noise according to the runs I've done and how much they can vary without me actually changing the code. Does a change this small even make a difference? I don't know. I'm definitely no performance/benchmarking expert yet so I don't know if I'm missing anything, but I'm kind confused how it is faster when all I did is add another type parameter and a bunch of useless arguments. Anyway, now the |
Beta Was this translation helpful? Give feedback.
-
I was successfully able to introduce state into queries and establish a dynamic system! I've still got to do some cleanup work and I have to do more testing, but the existing bevy queries continue to work without changes and without a performance regression according to ECS bench run before and after my commits. Here is the current branch. I'll be cleaning it up and moving it to #623. Here's the example code that sets up a runtime-created system: use std::time::Duration;
use bytemuck::{Pod, Zeroable};
use bevy::prelude::*;
use bevy_app::ScheduleRunnerPlugin;
use bevy_ecs::{
Access, ComponentId, DynamicComponentAccess, DynamicComponentInfo, DynamicComponentQuery,
};
// Define our componens
#[derive(Debug, Clone, Copy)]
struct Pos {
x: f32,
y: f32,
}
#[derive(Debug, Clone, Copy)]
struct Vel {
x: f32,
y: f32,
}
// Implement `Pod` ( Plain 'ol Data ) and `Zeroable` for our components so that we can cast them
// safely from raw bytes later
unsafe impl Zeroable for Pos {}
unsafe impl Zeroable for Vel {}
unsafe impl Pod for Pos {}
unsafe impl Pod for Vel {}
/// Create a system for spawning the scene
fn spawn_scene(world: &mut World, _resources: &mut Resources) {
#[rustfmt::skip]
world.spawn_batch(vec![
(
Pos {
x: 0.0,
y: 0.0
},
Vel {
x: 0.0,
y: 1.0,
}
),
(
Pos {
x: 0.0,
y: 0.0
},
Vel {
x: 0.0,
y: -1.0,
}
)
]);
}
/// Create a system for printing the status of our entities
fn info(mut query: Query<(&Pos, &Vel)>) {
println!("---");
for (pos, vel) in &mut query.iter() {
println!("{:?}\t\t{:?}", pos, vel);
}
}
fn main() {
// A Dynamic component query which can be constructed at runtime to represent which components
// we want a dynamic system to access.
//
// Notice that the sizes and IDs of the components can be specified at runtime and allow for
// storage of any data as an array of bytes.
let mut query = DynamicComponentQuery::default();
// Set the first element of the query to be write access to our `Pos` component
query[0] = Some(DynamicComponentAccess {
info: DynamicComponentInfo {
id: ComponentId::RustTypeId(std::any::TypeId::of::<Pos>()),
size: std::mem::size_of::<Pos>(),
},
access: Access::Write,
});
// Set the second element of the query to be read access to our `Vel` component
query[1] = Some(DynamicComponentAccess {
info: DynamicComponentInfo {
id: ComponentId::RustTypeId(std::any::TypeId::of::<Vel>()),
size: std::mem::size_of::<Vel>(),
},
access: Access::Read,
});
// Create our dynamic system by specifying the name, the query we created above, and a closure
// that operates on the query
let pos_vel_system = DynamicSystem::new("pos_vel_system".into(), query, |query| {
// Iterate over the query just like you would in a typical query
for mut components in &mut query.iter() {
// `components` will be an array with indexes corresponding to the indexes of our
// DynamicComponentAccess information that we constructed for our query when creating
// the system.
//
// Each item in the array is an optional mutable reference to a byte slice representing
// the component data: Option<&mut [u8]>.
// Here we take the mutable reference to the bytes of our position and velocity
// components
let pos_bytes = components[0].take().unwrap();
let vel_bytes = components[1].take().unwrap();
// Instead of interacting with the raw bytes of our components, we first cast them to
// their Rust structs
let mut pos: &mut Pos = &mut bytemuck::cast_slice_mut(pos_bytes)[0];
let vel: &Vel = &bytemuck::cast_slice(vel_bytes)[0];
// Now we can operate on our components
pos.x += vel.x;
pos.y += vel.y;
}
});
App::build()
.add_plugin(ScheduleRunnerPlugin::run_loop(Duration::from_secs(1)))
.add_startup_system(spawn_scene.thread_local_system())
.add_system(Box::new(pos_vel_system))
.add_system(info.system())
.run();
} |
Beta Was this translation helpful? Give feedback.
I was successfully able to introduce state into queries and establish a dynamic system! I've still got to do some cleanup work and I have to do more testing, but the existing bevy queries continue to work without changes and without a performance regression according to ECS bench run before and after my commits. Here is the current branch. I'll be cleaning it up and moving it to #623.
Here's the example code that sets up a runtime-created system: