SystemParamBuilder - Support dynamic system parameters (#14817)
# Objective
Support building systems with parameters whose types can be determined
at runtime.
## Solution
Create a `DynSystemParam` type that can be built using a
`SystemParamBuilder` of any type and then downcast to the appropriate
type dynamically.
## Example
```rust
let system = (
DynParamBuilder::new(LocalBuilder(3_usize)),
DynParamBuilder:🆕:<Query<()>>(QueryParamBuilder::new(|builder| {
builder.with::<A>();
})),
DynParamBuilder:🆕:<&Entities>(ParamBuilder),
)
.build_state(&mut world)
.build_system(
|mut p0: DynSystemParam, mut p1: DynSystemParam, mut p2: DynSystemParam| {
let local = p0.downcast_mut::<Local<usize>>().unwrap();
let query_count = p1.downcast_mut::<Query<()>>().unwrap();
let entities = p2.downcast_mut::<&Entities>().unwrap();
},
);
```
---------
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Periwink <charlesbour@gmail.com>
This commit is contained in:
parent
94d40d206e
commit
335f2903d9
@ -4,7 +4,7 @@ use crate::{
|
|||||||
prelude::QueryBuilder,
|
prelude::QueryBuilder,
|
||||||
query::{QueryData, QueryFilter, QueryState},
|
query::{QueryData, QueryFilter, QueryState},
|
||||||
system::{
|
system::{
|
||||||
system_param::{Local, ParamSet, SystemParam},
|
system_param::{DynSystemParam, DynSystemParamState, Local, ParamSet, SystemParam},
|
||||||
Query, SystemMeta,
|
Query, SystemMeta,
|
||||||
},
|
},
|
||||||
world::{FromWorld, World},
|
world::{FromWorld, World},
|
||||||
@ -251,6 +251,34 @@ macro_rules! impl_param_set_builder_tuple {
|
|||||||
|
|
||||||
all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B, meta);
|
all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B, meta);
|
||||||
|
|
||||||
|
/// A [`SystemParamBuilder`] for a [`DynSystemParam`].
|
||||||
|
pub struct DynParamBuilder<'a>(
|
||||||
|
Box<dyn FnOnce(&mut World, &mut SystemMeta) -> DynSystemParamState + 'a>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<'a> DynParamBuilder<'a> {
|
||||||
|
/// Creates a new [`DynParamBuilder`] by wrapping a [`SystemParamBuilder`] of any type.
|
||||||
|
/// The built [`DynSystemParam`] can be downcast to `T`.
|
||||||
|
pub fn new<T: SystemParam + 'static>(builder: impl SystemParamBuilder<T> + 'a) -> Self {
|
||||||
|
Self(Box::new(|world, meta| {
|
||||||
|
DynSystemParamState::new::<T>(builder.build(world, meta))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: `DynSystemParam::get_param` will call `get_param` on the boxed `DynSystemParamState`,
|
||||||
|
// and the boxed builder was a valid implementation of `SystemParamBuilder` for that type.
|
||||||
|
// The resulting `DynSystemParam` can only perform access by downcasting to that param type.
|
||||||
|
unsafe impl<'a, 'w, 's> SystemParamBuilder<DynSystemParam<'w, 's>> for DynParamBuilder<'a> {
|
||||||
|
fn build(
|
||||||
|
self,
|
||||||
|
world: &mut World,
|
||||||
|
meta: &mut SystemMeta,
|
||||||
|
) -> <DynSystemParam<'w, 's> as SystemParam>::State {
|
||||||
|
(self.0)(world, meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A [`SystemParamBuilder`] for a [`Local`].
|
/// A [`SystemParamBuilder`] for a [`Local`].
|
||||||
/// The provided value will be used as the initial value of the `Local`.
|
/// The provided value will be used as the initial value of the `Local`.
|
||||||
pub struct LocalBuilder<T>(pub T);
|
pub struct LocalBuilder<T>(pub T);
|
||||||
@ -271,6 +299,7 @@ unsafe impl<'s, T: FromWorld + Send + 'static> SystemParamBuilder<Local<'s, T>>
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate as bevy_ecs;
|
use crate as bevy_ecs;
|
||||||
|
use crate::entity::Entities;
|
||||||
use crate::prelude::{Component, Query};
|
use crate::prelude::{Component, Query};
|
||||||
use crate::system::{Local, RunSystemOnce};
|
use crate::system::{Local, RunSystemOnce};
|
||||||
|
|
||||||
@ -382,4 +411,33 @@ mod tests {
|
|||||||
let result = world.run_system_once(system);
|
let result = world.run_system_once(system);
|
||||||
assert_eq!(result, 5);
|
assert_eq!(result, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dyn_builder() {
|
||||||
|
let mut world = World::new();
|
||||||
|
|
||||||
|
world.spawn(A);
|
||||||
|
world.spawn_empty();
|
||||||
|
|
||||||
|
let system = (
|
||||||
|
DynParamBuilder::new(LocalBuilder(3_usize)),
|
||||||
|
DynParamBuilder::new::<Query<()>>(QueryParamBuilder::new(|builder| {
|
||||||
|
builder.with::<A>();
|
||||||
|
})),
|
||||||
|
DynParamBuilder::new::<&Entities>(ParamBuilder),
|
||||||
|
)
|
||||||
|
.build_state(&mut world)
|
||||||
|
.build_system(
|
||||||
|
|mut p0: DynSystemParam, mut p1: DynSystemParam, mut p2: DynSystemParam| {
|
||||||
|
let local = *p0.downcast_mut::<Local<usize>>().unwrap();
|
||||||
|
let query_count = p1.downcast_mut::<Query<()>>().unwrap().iter().count();
|
||||||
|
let _entities = p2.downcast_mut::<&Entities>().unwrap();
|
||||||
|
assert!(p0.downcast_mut::<Query<()>>().is_none());
|
||||||
|
local + query_count
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = world.run_system_once(system);
|
||||||
|
assert_eq!(result, 4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ use bevy_utils::{all_tuples, synccell::SyncCell};
|
|||||||
#[cfg(feature = "track_change_detection")]
|
#[cfg(feature = "track_change_detection")]
|
||||||
use std::panic::Location;
|
use std::panic::Location;
|
||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
@ -1674,6 +1675,253 @@ unsafe impl<T: ?Sized> SystemParam for PhantomData<T> {
|
|||||||
// SAFETY: No world access.
|
// SAFETY: No world access.
|
||||||
unsafe impl<T: ?Sized> ReadOnlySystemParam for PhantomData<T> {}
|
unsafe impl<T: ?Sized> ReadOnlySystemParam for PhantomData<T> {}
|
||||||
|
|
||||||
|
/// A [`SystemParam`] with a type that can be configured at runtime.
|
||||||
|
/// To be useful, this must be configured using a [`DynParamBuilder`](crate::system::DynParamBuilder) to build the system using a [`SystemParamBuilder`](crate::prelude::SystemParamBuilder).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_ecs::{prelude::*, system::*};
|
||||||
|
/// #
|
||||||
|
/// # #[derive(Default, Resource)]
|
||||||
|
/// # struct A;
|
||||||
|
/// #
|
||||||
|
/// # #[derive(Default, Resource)]
|
||||||
|
/// # struct B;
|
||||||
|
/// #
|
||||||
|
/// let mut world = World::new();
|
||||||
|
/// world.init_resource::<A>();
|
||||||
|
/// world.init_resource::<B>();
|
||||||
|
///
|
||||||
|
/// // If the inner parameter doesn't require any special building, use `ParamBuilder`.
|
||||||
|
/// // Either specify the type parameter on `DynParamBuilder::new()` ...
|
||||||
|
/// let system = (DynParamBuilder::new::<Res<A>>(ParamBuilder),)
|
||||||
|
/// .build_state(&mut world)
|
||||||
|
/// .build_system(expects_res_a);
|
||||||
|
/// world.run_system_once(system);
|
||||||
|
///
|
||||||
|
/// // ... or use a factory method on `ParamBuilder` that returns a specific type.
|
||||||
|
/// let system = (DynParamBuilder::new(ParamBuilder::resource::<A>()),)
|
||||||
|
/// .build_state(&mut world)
|
||||||
|
/// .build_system(expects_res_a);
|
||||||
|
/// world.run_system_once(system);
|
||||||
|
///
|
||||||
|
/// fn expects_res_a(mut param: DynSystemParam) {
|
||||||
|
/// // Use the `downcast` methods to retrieve the inner parameter.
|
||||||
|
/// // They will return `None` if the type does not match.
|
||||||
|
/// assert!(param.is::<Res<A>>());
|
||||||
|
/// assert!(!param.is::<Res<B>>());
|
||||||
|
/// assert!(param.downcast_mut::<Res<B>>().is_none());
|
||||||
|
/// let foo: Res<A> = param.downcast::<Res<A>>().unwrap();
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let system = (
|
||||||
|
/// // If the inner parameter also requires building,
|
||||||
|
/// // pass the appropriate `SystemParamBuilder`.
|
||||||
|
/// DynParamBuilder::new(LocalBuilder(10usize)),
|
||||||
|
/// // `DynSystemParam` is just an ordinary `SystemParam`,
|
||||||
|
/// // and can be combined with other parameters as usual!
|
||||||
|
/// ParamBuilder::query(),
|
||||||
|
/// )
|
||||||
|
/// .build_state(&mut world)
|
||||||
|
/// .build_system(|param: DynSystemParam, query: Query<()>| {
|
||||||
|
/// let local: Local<usize> = param.downcast::<Local<usize>>().unwrap();
|
||||||
|
/// assert_eq!(*local, 10);
|
||||||
|
/// });
|
||||||
|
/// world.run_system_once(system);
|
||||||
|
/// ```
|
||||||
|
pub struct DynSystemParam<'w, 's> {
|
||||||
|
/// A `ParamState<T>` wrapping the state for the underlying system param.
|
||||||
|
state: &'s mut dyn Any,
|
||||||
|
world: UnsafeWorldCell<'w>,
|
||||||
|
system_meta: SystemMeta,
|
||||||
|
change_tick: Tick,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'w, 's> DynSystemParam<'w, 's> {
|
||||||
|
/// # SAFETY
|
||||||
|
/// - `state` must be a `ParamState<T>` for some inner `T: SystemParam`.
|
||||||
|
/// - The passed [`UnsafeWorldCell`] must have access to any world data registered
|
||||||
|
/// in [`init_state`](SystemParam::init_state) for the inner system param.
|
||||||
|
/// - `world` must be the same `World` that was used to initialize
|
||||||
|
/// [`state`](SystemParam::init_state) for the inner system param.
|
||||||
|
unsafe fn new(
|
||||||
|
state: &'s mut dyn Any,
|
||||||
|
world: UnsafeWorldCell<'w>,
|
||||||
|
system_meta: SystemMeta,
|
||||||
|
change_tick: Tick,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
world,
|
||||||
|
system_meta,
|
||||||
|
change_tick,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the inner system param is the same as `T`.
|
||||||
|
pub fn is<T: SystemParam + 'static>(&self) -> bool {
|
||||||
|
self.state.is::<ParamState<T>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner system param if it is the correct type.
|
||||||
|
/// This consumes the dyn param, so the returned param can have its original world and state lifetimes.
|
||||||
|
pub fn downcast<T: SystemParam + 'static>(self) -> Option<T::Item<'w, 's>> {
|
||||||
|
// SAFETY:
|
||||||
|
// - `DynSystemParam::new()` ensures `state` is a `ParamState<T>`, that the world matches,
|
||||||
|
// and that it has access required by the inner system param.
|
||||||
|
// - This consumes the `DynSystemParam`, so it is the only use of `world` with this access and it is available for `'w`.
|
||||||
|
unsafe { downcast::<T>(self.state, &self.system_meta, self.world, self.change_tick) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner system parameter if it is the correct type.
|
||||||
|
/// This borrows the dyn param, so the returned param is only valid for the duration of that borrow.
|
||||||
|
pub fn downcast_mut<T: SystemParam + 'static>(&mut self) -> Option<T::Item<'_, '_>> {
|
||||||
|
// SAFETY:
|
||||||
|
// - `DynSystemParam::new()` ensures `state` is a `ParamState<T>`, that the world matches,
|
||||||
|
// and that it has access required by the inner system param.
|
||||||
|
// - This exclusively borrows the `DynSystemParam` for `'_`, so it is the only use of `world` with this access for `'_`.
|
||||||
|
unsafe { downcast::<T>(self.state, &self.system_meta, self.world, self.change_tick) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner system parameter if it is the correct type.
|
||||||
|
/// This borrows the dyn param, so the returned param is only valid for the duration of that borrow,
|
||||||
|
/// but since it only performs read access it can keep the original world lifetime.
|
||||||
|
/// This can be useful with methods like [`Query::iter_inner()`] or [`Res::into_inner()`]
|
||||||
|
/// to obtain references with the original world lifetime.
|
||||||
|
pub fn downcast_mut_inner<T: ReadOnlySystemParam + 'static>(
|
||||||
|
&mut self,
|
||||||
|
) -> Option<T::Item<'w, '_>> {
|
||||||
|
// SAFETY:
|
||||||
|
// - `DynSystemParam::new()` ensures `state` is a `ParamState<T>`, that the world matches,
|
||||||
|
// and that it has access required by the inner system param.
|
||||||
|
// - The inner system param only performs read access, so it's safe to copy that access for the full `'w` lifetime.
|
||||||
|
unsafe { downcast::<T>(self.state, &self.system_meta, self.world, self.change_tick) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # SAFETY
|
||||||
|
/// - `state` must be a `ParamState<T>` for some inner `T: SystemParam`.
|
||||||
|
/// - The passed [`UnsafeWorldCell`] must have access to any world data registered
|
||||||
|
/// in [`init_state`](SystemParam::init_state) for the inner system param.
|
||||||
|
/// - `world` must be the same `World` that was used to initialize
|
||||||
|
/// [`state`](SystemParam::init_state) for the inner system param.
|
||||||
|
unsafe fn downcast<'w, 's, T: SystemParam + 'static>(
|
||||||
|
state: &'s mut dyn Any,
|
||||||
|
system_meta: &SystemMeta,
|
||||||
|
world: UnsafeWorldCell<'w>,
|
||||||
|
change_tick: Tick,
|
||||||
|
) -> Option<T::Item<'w, 's>> {
|
||||||
|
state.downcast_mut::<ParamState<T>>().map(|state| {
|
||||||
|
// SAFETY:
|
||||||
|
// - The caller ensures the world has access for the underlying system param,
|
||||||
|
// and since the downcast succeeded, the underlying system param is T.
|
||||||
|
// - The caller ensures the `world` matches.
|
||||||
|
unsafe { T::get_param(&mut state.0, system_meta, world, change_tick) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [`SystemParam::State`] for a [`DynSystemParam`].
|
||||||
|
pub struct DynSystemParamState(Box<dyn DynParamState>);
|
||||||
|
|
||||||
|
impl DynSystemParamState {
|
||||||
|
pub(crate) fn new<T: SystemParam + 'static>(state: T::State) -> Self {
|
||||||
|
Self(Box::new(ParamState::<T>(state)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows a [`SystemParam::State`] to be used as a trait object for implementing [`DynSystemParam`].
|
||||||
|
trait DynParamState: Sync + Send {
|
||||||
|
/// Casts the underlying `ParamState<T>` to an `Any` so it can be downcast.
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||||
|
|
||||||
|
/// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable).a
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `archetype` must be from the [`World`] used to initialize `state` in `init_state`.
|
||||||
|
unsafe fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta);
|
||||||
|
|
||||||
|
/// Applies any deferred mutations stored in this [`SystemParam`]'s state.
|
||||||
|
/// This is used to apply [`Commands`] during [`apply_deferred`](crate::prelude::apply_deferred).
|
||||||
|
///
|
||||||
|
/// [`Commands`]: crate::prelude::Commands
|
||||||
|
fn apply(&mut self, system_meta: &SystemMeta, world: &mut World);
|
||||||
|
|
||||||
|
/// Queues any deferred mutations to be applied at the next [`apply_deferred`](crate::prelude::apply_deferred).
|
||||||
|
fn queue(&mut self, system_meta: &SystemMeta, world: DeferredWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around a [`SystemParam::State`] that can be used as a trait object in a [`DynSystemParam`].
|
||||||
|
struct ParamState<T: SystemParam>(T::State);
|
||||||
|
|
||||||
|
impl<T: SystemParam + 'static> DynParamState for ParamState<T> {
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta) {
|
||||||
|
// SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`.
|
||||||
|
unsafe { T::new_archetype(&mut self.0, archetype, system_meta) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&mut self, system_meta: &SystemMeta, world: &mut World) {
|
||||||
|
T::apply(&mut self.0, system_meta, world);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue(&mut self, system_meta: &SystemMeta, world: DeferredWorld) {
|
||||||
|
T::queue(&mut self.0, system_meta, world);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: `init_state` creates a state of (), which performs no access. The interesting safety checks are on the `SystemParamBuilder`.
|
||||||
|
unsafe impl SystemParam for DynSystemParam<'_, '_> {
|
||||||
|
type State = DynSystemParamState;
|
||||||
|
|
||||||
|
type Item<'world, 'state> = DynSystemParam<'world, 'state>;
|
||||||
|
|
||||||
|
fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {
|
||||||
|
DynSystemParamState::new::<()>(())
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_param<'world, 'state>(
|
||||||
|
state: &'state mut Self::State,
|
||||||
|
system_meta: &SystemMeta,
|
||||||
|
world: UnsafeWorldCell<'world>,
|
||||||
|
change_tick: Tick,
|
||||||
|
) -> Self::Item<'world, 'state> {
|
||||||
|
// SAFETY:
|
||||||
|
// - `state.0` is a boxed `ParamState<T>`, and its implementation of `as_any_mut` returns `self`.
|
||||||
|
// - The state was obtained from `SystemParamBuilder::build()`, which registers all [`World`] accesses used
|
||||||
|
// by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta).
|
||||||
|
// - The caller ensures that the provided world is the same and has the required access.
|
||||||
|
unsafe {
|
||||||
|
DynSystemParam::new(
|
||||||
|
state.0.as_any_mut(),
|
||||||
|
world,
|
||||||
|
system_meta.clone(),
|
||||||
|
change_tick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn new_archetype(
|
||||||
|
state: &mut Self::State,
|
||||||
|
archetype: &Archetype,
|
||||||
|
system_meta: &mut SystemMeta,
|
||||||
|
) {
|
||||||
|
// SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`.
|
||||||
|
unsafe { state.0.new_archetype(archetype, system_meta) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {
|
||||||
|
state.0.apply(system_meta, world);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {
|
||||||
|
state.0.queue(system_meta, world);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user