Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
This commit is contained in:
commit
866476e851
@ -851,6 +851,8 @@ impl Image {
|
|||||||
|
|
||||||
/// Resizes the image to the new size, by removing information or appending 0 to the `data`.
|
/// Resizes the image to the new size, by removing information or appending 0 to the `data`.
|
||||||
/// Does not properly scale the contents of the image.
|
/// Does not properly scale the contents of the image.
|
||||||
|
///
|
||||||
|
/// If you need to keep pixel data intact, use [`Image::resize_in_place`].
|
||||||
pub fn resize(&mut self, size: Extent3d) {
|
pub fn resize(&mut self, size: Extent3d) {
|
||||||
self.texture_descriptor.size = size;
|
self.texture_descriptor.size = size;
|
||||||
if let Some(ref mut data) = self.data {
|
if let Some(ref mut data) = self.data {
|
||||||
@ -878,6 +880,52 @@ impl Image {
|
|||||||
self.texture_descriptor.size = new_size;
|
self.texture_descriptor.size = new_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resizes the image to the new size, keeping the pixel data intact, anchored at the top-left.
|
||||||
|
/// When growing, the new space is filled with 0. When shrinking, the image is clipped.
|
||||||
|
///
|
||||||
|
/// For faster resizing when keeping pixel data intact is not important, use [`Image::resize`].
|
||||||
|
pub fn resize_in_place(&mut self, new_size: Extent3d) -> Result<(), ResizeError> {
|
||||||
|
let old_size = self.texture_descriptor.size;
|
||||||
|
let pixel_size = self.texture_descriptor.format.pixel_size();
|
||||||
|
let byte_len = self.texture_descriptor.format.pixel_size() * new_size.volume();
|
||||||
|
|
||||||
|
let Some(ref mut data) = self.data else {
|
||||||
|
return Err(ResizeError::ImageWithoutData);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut new: Vec<u8> = vec![0; byte_len];
|
||||||
|
|
||||||
|
let copy_width = old_size.width.min(new_size.width) as usize;
|
||||||
|
let copy_height = old_size.height.min(new_size.height) as usize;
|
||||||
|
let copy_depth = old_size
|
||||||
|
.depth_or_array_layers
|
||||||
|
.min(new_size.depth_or_array_layers) as usize;
|
||||||
|
|
||||||
|
let old_row_stride = old_size.width as usize * pixel_size;
|
||||||
|
let old_layer_stride = old_size.height as usize * old_row_stride;
|
||||||
|
|
||||||
|
let new_row_stride = new_size.width as usize * pixel_size;
|
||||||
|
let new_layer_stride = new_size.height as usize * new_row_stride;
|
||||||
|
|
||||||
|
for z in 0..copy_depth {
|
||||||
|
for y in 0..copy_height {
|
||||||
|
let old_offset = z * old_layer_stride + y * old_row_stride;
|
||||||
|
let new_offset = z * new_layer_stride + y * new_row_stride;
|
||||||
|
|
||||||
|
let old_range = (old_offset)..(old_offset + copy_width * pixel_size);
|
||||||
|
let new_range = (new_offset)..(new_offset + copy_width * pixel_size);
|
||||||
|
|
||||||
|
new[new_range].copy_from_slice(&data[old_range]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.data = Some(new);
|
||||||
|
|
||||||
|
self.texture_descriptor.size = new_size;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Takes a 2D image containing vertically stacked images of the same size, and reinterprets
|
/// Takes a 2D image containing vertically stacked images of the same size, and reinterprets
|
||||||
/// it as a 2D array texture, where each of the stacked images becomes one layer of the
|
/// it as a 2D array texture, where each of the stacked images becomes one layer of the
|
||||||
/// array. This is primarily for use with the `texture2DArray` shader uniform type.
|
/// array. This is primarily for use with the `texture2DArray` shader uniform type.
|
||||||
@ -1540,6 +1588,14 @@ pub enum TextureError {
|
|||||||
IncompleteCubemap,
|
IncompleteCubemap,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An error that occurs when an image cannot be resized.
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ResizeError {
|
||||||
|
/// Failed to resize an Image because it has no data.
|
||||||
|
#[error("resize method requires cpu-side image data but none was present")]
|
||||||
|
ImageWithoutData,
|
||||||
|
}
|
||||||
|
|
||||||
/// The type of a raw image buffer.
|
/// The type of a raw image buffer.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ImageType<'a> {
|
pub enum ImageType<'a> {
|
||||||
@ -1730,4 +1786,173 @@ mod test {
|
|||||||
image.set_color_at_3d(4, 9, 2, Color::WHITE).unwrap();
|
image.set_color_at_3d(4, 9, 2, Color::WHITE).unwrap();
|
||||||
assert!(matches!(image.get_color_at_3d(4, 9, 2), Ok(Color::WHITE)));
|
assert!(matches!(image.get_color_at_3d(4, 9, 2), Ok(Color::WHITE)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_in_place_2d_grow_and_shrink() {
|
||||||
|
use bevy_color::ColorToPacked;
|
||||||
|
|
||||||
|
const INITIAL_FILL: LinearRgba = LinearRgba::BLACK;
|
||||||
|
const GROW_FILL: LinearRgba = LinearRgba::NONE;
|
||||||
|
|
||||||
|
let mut image = Image::new_fill(
|
||||||
|
Extent3d {
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
TextureDimension::D2,
|
||||||
|
&INITIAL_FILL.to_u8_array(),
|
||||||
|
TextureFormat::Rgba8Unorm,
|
||||||
|
RenderAssetUsages::MAIN_WORLD,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a test pattern
|
||||||
|
|
||||||
|
const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [
|
||||||
|
(0, 1, LinearRgba::RED),
|
||||||
|
(1, 1, LinearRgba::GREEN),
|
||||||
|
(1, 0, LinearRgba::BLUE),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (x, y, color) in &TEST_PIXELS {
|
||||||
|
image.set_color_at(*x, *y, Color::from(*color)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow image
|
||||||
|
image
|
||||||
|
.resize_in_place(Extent3d {
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// After growing, the test pattern should be the same.
|
||||||
|
assert!(matches!(
|
||||||
|
image.get_color_at(0, 0),
|
||||||
|
Ok(Color::LinearRgba(INITIAL_FILL))
|
||||||
|
));
|
||||||
|
for (x, y, color) in &TEST_PIXELS {
|
||||||
|
assert_eq!(
|
||||||
|
image.get_color_at(*x, *y).unwrap(),
|
||||||
|
Color::LinearRgba(*color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pixels in the newly added area should get filled with zeroes.
|
||||||
|
assert!(matches!(
|
||||||
|
image.get_color_at(3, 3),
|
||||||
|
Ok(Color::LinearRgba(GROW_FILL))
|
||||||
|
));
|
||||||
|
|
||||||
|
// Shrink
|
||||||
|
image
|
||||||
|
.resize_in_place(Extent3d {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Images outside of the new dimensions should be clipped
|
||||||
|
assert!(image.get_color_at(1, 1).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_in_place_array_grow_and_shrink() {
|
||||||
|
use bevy_color::ColorToPacked;
|
||||||
|
|
||||||
|
const INITIAL_FILL: LinearRgba = LinearRgba::BLACK;
|
||||||
|
const GROW_FILL: LinearRgba = LinearRgba::NONE;
|
||||||
|
const LAYERS: u32 = 4;
|
||||||
|
|
||||||
|
let mut image = Image::new_fill(
|
||||||
|
Extent3d {
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
depth_or_array_layers: LAYERS,
|
||||||
|
},
|
||||||
|
TextureDimension::D2,
|
||||||
|
&INITIAL_FILL.to_u8_array(),
|
||||||
|
TextureFormat::Rgba8Unorm,
|
||||||
|
RenderAssetUsages::MAIN_WORLD,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a test pattern
|
||||||
|
|
||||||
|
const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [
|
||||||
|
(0, 1, LinearRgba::RED),
|
||||||
|
(1, 1, LinearRgba::GREEN),
|
||||||
|
(1, 0, LinearRgba::BLUE),
|
||||||
|
];
|
||||||
|
|
||||||
|
for z in 0..LAYERS {
|
||||||
|
for (x, y, color) in &TEST_PIXELS {
|
||||||
|
image
|
||||||
|
.set_color_at_3d(*x, *y, z, Color::from(*color))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow image
|
||||||
|
image
|
||||||
|
.resize_in_place(Extent3d {
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
depth_or_array_layers: LAYERS + 1,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// After growing, the test pattern should be the same.
|
||||||
|
assert!(matches!(
|
||||||
|
image.get_color_at(0, 0),
|
||||||
|
Ok(Color::LinearRgba(INITIAL_FILL))
|
||||||
|
));
|
||||||
|
for z in 0..LAYERS {
|
||||||
|
for (x, y, color) in &TEST_PIXELS {
|
||||||
|
assert_eq!(
|
||||||
|
image.get_color_at_3d(*x, *y, z).unwrap(),
|
||||||
|
Color::LinearRgba(*color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pixels in the newly added area should get filled with zeroes.
|
||||||
|
for z in 0..(LAYERS + 1) {
|
||||||
|
assert!(matches!(
|
||||||
|
image.get_color_at_3d(3, 3, z),
|
||||||
|
Ok(Color::LinearRgba(GROW_FILL))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shrink
|
||||||
|
image
|
||||||
|
.resize_in_place(Extent3d {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Images outside of the new dimensions should be clipped
|
||||||
|
assert!(image.get_color_at_3d(1, 1, 0).is_err());
|
||||||
|
|
||||||
|
// Higher layers should no longer be present
|
||||||
|
assert!(image.get_color_at_3d(0, 0, 1).is_err());
|
||||||
|
|
||||||
|
// Grow layers
|
||||||
|
image
|
||||||
|
.resize_in_place(Extent3d {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
depth_or_array_layers: 2,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Pixels in the newly added layer should be zeroes.
|
||||||
|
assert!(matches!(
|
||||||
|
image.get_color_at_3d(0, 0, 1),
|
||||||
|
Ok(Color::LinearRgba(GROW_FILL))
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ use bevy_ecs::{
|
|||||||
};
|
};
|
||||||
use bevy_platform::collections::HashMap;
|
use bevy_platform::collections::HashMap;
|
||||||
|
|
||||||
use crate::state::{OnExit, StateTransitionEvent, States};
|
use crate::state::{OnEnter, OnExit, StateTransitionEvent, States};
|
||||||
|
|
||||||
fn clear_event_queue<E: Event>(w: &mut World) {
|
fn clear_event_queue<E: Event>(w: &mut World) {
|
||||||
if let Some(mut queue) = w.get_resource_mut::<Events<E>>() {
|
if let Some(mut queue) = w.get_resource_mut::<Events<E>>() {
|
||||||
@ -18,21 +18,35 @@ fn clear_event_queue<E: Event>(w: &mut World) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
enum TransitionType {
|
||||||
|
OnExit,
|
||||||
|
OnEnter,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct StateScopedEvents<S: States> {
|
struct StateScopedEvents<S: States> {
|
||||||
cleanup_fns: HashMap<S, Vec<fn(&mut World)>>,
|
/// Keeps track of which events need to be reset when the state is exited.
|
||||||
|
on_exit: HashMap<S, Vec<fn(&mut World)>>,
|
||||||
|
/// Keeps track of which events need to be reset when the state is entered.
|
||||||
|
on_enter: HashMap<S, Vec<fn(&mut World)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: States> StateScopedEvents<S> {
|
impl<S: States> StateScopedEvents<S> {
|
||||||
fn add_event<E: Event>(&mut self, state: S) {
|
fn add_event<E: Event>(&mut self, state: S, transition_type: TransitionType) {
|
||||||
self.cleanup_fns
|
let map = match transition_type {
|
||||||
.entry(state)
|
TransitionType::OnExit => &mut self.on_exit,
|
||||||
.or_default()
|
TransitionType::OnEnter => &mut self.on_enter,
|
||||||
.push(clear_event_queue::<E>);
|
};
|
||||||
|
map.entry(state).or_default().push(clear_event_queue::<E>);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cleanup(&self, w: &mut World, state: S) {
|
fn cleanup(&self, w: &mut World, state: S, transition_type: TransitionType) {
|
||||||
let Some(fns) = self.cleanup_fns.get(&state) else {
|
let map = match transition_type {
|
||||||
|
TransitionType::OnExit => &self.on_exit,
|
||||||
|
TransitionType::OnEnter => &self.on_enter,
|
||||||
|
};
|
||||||
|
let Some(fns) = map.get(&state) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
for callback in fns {
|
for callback in fns {
|
||||||
@ -44,12 +58,13 @@ impl<S: States> StateScopedEvents<S> {
|
|||||||
impl<S: States> Default for StateScopedEvents<S> {
|
impl<S: States> Default for StateScopedEvents<S> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
cleanup_fns: HashMap::default(),
|
on_exit: HashMap::default(),
|
||||||
|
on_enter: HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cleanup_state_scoped_event<S: States>(
|
fn clear_events_on_exit_state<S: States>(
|
||||||
mut c: Commands,
|
mut c: Commands,
|
||||||
mut transitions: EventReader<StateTransitionEvent<S>>,
|
mut transitions: EventReader<StateTransitionEvent<S>>,
|
||||||
) {
|
) {
|
||||||
@ -65,48 +80,185 @@ fn cleanup_state_scoped_event<S: States>(
|
|||||||
|
|
||||||
c.queue(move |w: &mut World| {
|
c.queue(move |w: &mut World| {
|
||||||
w.resource_scope::<StateScopedEvents<S>, ()>(|w, events| {
|
w.resource_scope::<StateScopedEvents<S>, ()>(|w, events| {
|
||||||
events.cleanup(w, exited);
|
events.cleanup(w, exited, TransitionType::OnExit);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_state_scoped_event_impl<E: Event, S: States>(
|
fn clear_events_on_enter_state<S: States>(
|
||||||
|
mut c: Commands,
|
||||||
|
mut transitions: EventReader<StateTransitionEvent<S>>,
|
||||||
|
) {
|
||||||
|
let Some(transition) = transitions.read().last() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if transition.entered == transition.exited {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(entered) = transition.entered.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
c.queue(move |w: &mut World| {
|
||||||
|
w.resource_scope::<StateScopedEvents<S>, ()>(|w, events| {
|
||||||
|
events.cleanup(w, entered, TransitionType::OnEnter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_events_on_state_transition<E: Event, S: States>(
|
||||||
app: &mut SubApp,
|
app: &mut SubApp,
|
||||||
_p: PhantomData<E>,
|
_p: PhantomData<E>,
|
||||||
state: S,
|
state: S,
|
||||||
|
transition_type: TransitionType,
|
||||||
) {
|
) {
|
||||||
if !app.world().contains_resource::<StateScopedEvents<S>>() {
|
if !app.world().contains_resource::<StateScopedEvents<S>>() {
|
||||||
app.init_resource::<StateScopedEvents<S>>();
|
app.init_resource::<StateScopedEvents<S>>();
|
||||||
}
|
}
|
||||||
app.add_event::<E>();
|
|
||||||
app.world_mut()
|
app.world_mut()
|
||||||
.resource_mut::<StateScopedEvents<S>>()
|
.resource_mut::<StateScopedEvents<S>>()
|
||||||
.add_event::<E>(state.clone());
|
.add_event::<E>(state.clone(), transition_type);
|
||||||
app.add_systems(OnExit(state), cleanup_state_scoped_event::<S>);
|
match transition_type {
|
||||||
|
TransitionType::OnExit => app.add_systems(OnExit(state), clear_events_on_exit_state::<S>),
|
||||||
|
TransitionType::OnEnter => {
|
||||||
|
app.add_systems(OnEnter(state), clear_events_on_enter_state::<S>)
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extension trait for [`App`] adding methods for registering state scoped events.
|
/// Extension trait for [`App`] adding methods for registering state scoped events.
|
||||||
pub trait StateScopedEventsAppExt {
|
pub trait StateScopedEventsAppExt {
|
||||||
/// Adds an [`Event`] that is automatically cleaned up when leaving the specified `state`.
|
/// Clears an [`Event`] when exiting the specified `state`.
|
||||||
///
|
///
|
||||||
/// Note that event cleanup is ordered ambiguously relative to [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState)
|
/// Note that event cleanup is ambiguously ordered relative to
|
||||||
/// and [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity
|
/// [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity cleanup,
|
||||||
/// cleanup and the [`OnExit`] schedule for the target state. All of these (state scoped
|
/// and the [`OnExit`] schedule for the target state.
|
||||||
/// entities and events cleanup, and `OnExit`) occur within schedule [`StateTransition`](crate::prelude::StateTransition)
|
/// All of these (state scoped entities and events cleanup, and `OnExit`)
|
||||||
|
/// occur within schedule [`StateTransition`](crate::prelude::StateTransition)
|
||||||
/// and system set `StateTransitionSystems::ExitSchedules`.
|
/// and system set `StateTransitionSystems::ExitSchedules`.
|
||||||
fn add_state_scoped_event<E: Event>(&mut self, state: impl States) -> &mut Self;
|
fn clear_events_on_exit_state<E: Event>(&mut self, state: impl States) -> &mut Self;
|
||||||
|
|
||||||
|
/// Clears an [`Event`] when entering the specified `state`.
|
||||||
|
///
|
||||||
|
/// Note that event cleanup is ambiguously ordered relative to
|
||||||
|
/// [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) entity cleanup,
|
||||||
|
/// and the [`OnEnter`] schedule for the target state.
|
||||||
|
/// All of these (state scoped entities and events cleanup, and `OnEnter`)
|
||||||
|
/// occur within schedule [`StateTransition`](crate::prelude::StateTransition)
|
||||||
|
/// and system set `StateTransitionSystems::EnterSchedules`.
|
||||||
|
fn clear_events_on_enter_state<E: Event>(&mut self, state: impl States) -> &mut Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateScopedEventsAppExt for App {
|
impl StateScopedEventsAppExt for App {
|
||||||
fn add_state_scoped_event<E: Event>(&mut self, state: impl States) -> &mut Self {
|
fn clear_events_on_exit_state<E: Event>(&mut self, state: impl States) -> &mut Self {
|
||||||
add_state_scoped_event_impl(self.main_mut(), PhantomData::<E>, state);
|
clear_events_on_state_transition(
|
||||||
|
self.main_mut(),
|
||||||
|
PhantomData::<E>,
|
||||||
|
state,
|
||||||
|
TransitionType::OnExit,
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_events_on_enter_state<E: Event>(&mut self, state: impl States) -> &mut Self {
|
||||||
|
clear_events_on_state_transition(
|
||||||
|
self.main_mut(),
|
||||||
|
PhantomData::<E>,
|
||||||
|
state,
|
||||||
|
TransitionType::OnEnter,
|
||||||
|
);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateScopedEventsAppExt for SubApp {
|
impl StateScopedEventsAppExt for SubApp {
|
||||||
fn add_state_scoped_event<E: Event>(&mut self, state: impl States) -> &mut Self {
|
fn clear_events_on_exit_state<E: Event>(&mut self, state: impl States) -> &mut Self {
|
||||||
add_state_scoped_event_impl(self, PhantomData::<E>, state);
|
clear_events_on_state_transition(self, PhantomData::<E>, state, TransitionType::OnExit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_events_on_enter_state<E: Event>(&mut self, state: impl States) -> &mut Self {
|
||||||
|
clear_events_on_state_transition(self, PhantomData::<E>, state, TransitionType::OnEnter);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::app::StatesPlugin;
|
||||||
|
use bevy_state::prelude::*;
|
||||||
|
|
||||||
|
#[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)]
|
||||||
|
enum TestState {
|
||||||
|
#[default]
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Event, Debug)]
|
||||||
|
struct StandardEvent;
|
||||||
|
|
||||||
|
#[derive(Event, Debug)]
|
||||||
|
struct StateScopedEvent;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_event_on_exit_state() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(StatesPlugin);
|
||||||
|
app.init_state::<TestState>();
|
||||||
|
|
||||||
|
app.add_event::<StandardEvent>();
|
||||||
|
app.add_event::<StateScopedEvent>()
|
||||||
|
.clear_events_on_exit_state::<StateScopedEvent>(TestState::A);
|
||||||
|
|
||||||
|
app.world_mut().send_event(StandardEvent).unwrap();
|
||||||
|
app.world_mut().send_event(StateScopedEvent).unwrap();
|
||||||
|
assert!(!app.world().resource::<Events<StandardEvent>>().is_empty());
|
||||||
|
assert!(!app
|
||||||
|
.world()
|
||||||
|
.resource::<Events<StateScopedEvent>>()
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
|
app.world_mut()
|
||||||
|
.resource_mut::<NextState<TestState>>()
|
||||||
|
.set(TestState::B);
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
assert!(!app.world().resource::<Events<StandardEvent>>().is_empty());
|
||||||
|
assert!(app
|
||||||
|
.world()
|
||||||
|
.resource::<Events<StateScopedEvent>>()
|
||||||
|
.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_event_on_enter_state() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(StatesPlugin);
|
||||||
|
app.init_state::<TestState>();
|
||||||
|
|
||||||
|
app.add_event::<StandardEvent>();
|
||||||
|
app.add_event::<StateScopedEvent>()
|
||||||
|
.clear_events_on_enter_state::<StateScopedEvent>(TestState::B);
|
||||||
|
|
||||||
|
app.world_mut().send_event(StandardEvent).unwrap();
|
||||||
|
app.world_mut().send_event(StateScopedEvent).unwrap();
|
||||||
|
assert!(!app.world().resource::<Events<StandardEvent>>().is_empty());
|
||||||
|
assert!(!app
|
||||||
|
.world()
|
||||||
|
.resource::<Events<StateScopedEvent>>()
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
|
app.world_mut()
|
||||||
|
.resource_mut::<NextState<TestState>>()
|
||||||
|
.set(TestState::B);
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
assert!(!app.world().resource::<Events<StandardEvent>>().is_empty());
|
||||||
|
assert!(app
|
||||||
|
.world()
|
||||||
|
.resource::<Events<StateScopedEvent>>()
|
||||||
|
.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: `StateScoped` renamed to `DespawnOnExitState`
|
|
||||||
pull_requests: [18818]
|
|
||||||
---
|
|
||||||
|
|
||||||
Previously, Bevy provided the `StateScoped` component as a way to despawn an entity when **exiting** a state.
|
|
||||||
|
|
||||||
However, it can also be useful to have the opposite behavior, where an entity is despawned when **entering** a state. This is now possible with the new `DespawnOnEnterState` component.
|
|
||||||
|
|
||||||
To support despawning entities when entering a state, in Bevy 0.17 the `StateScoped` component was renamed to `DespawnOnExitState` and `clear_state_scoped_entities` was renamed to `despawn_entities_on_exit_state`. Replace all references and imports.
|
|
||||||
20
release-content/migration-guides/rename_state_scoped.md
Normal file
20
release-content/migration-guides/rename_state_scoped.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: Renamed state scoped entities and events
|
||||||
|
pull_requests: [18818, 19435]
|
||||||
|
---
|
||||||
|
|
||||||
|
Previously, Bevy provided the `StateScoped` component and `add_state_scoped_event` method
|
||||||
|
as a way to remove entities/events when **exiting** a state.
|
||||||
|
|
||||||
|
However, it can also be useful to have the opposite behavior,
|
||||||
|
where entities/events are removed when **entering** a state.
|
||||||
|
This is now possible with the new `DespawnOnEnterState` component and `clear_events_on_enter_state` method.
|
||||||
|
|
||||||
|
To support this addition, the previous method and component have been renamed.
|
||||||
|
Also, `clear_event_on_exit_state` no longer adds the event automatically, so you must call `App::add_event` manually.
|
||||||
|
|
||||||
|
| Before | After |
|
||||||
|
|-------------------------------|--------------------------------------------|
|
||||||
|
| `StateScoped` | `DespawnOnExitState` |
|
||||||
|
| `clear_state_scoped_entities` | `despawn_entities_on_exit_state` |
|
||||||
|
| `add_state_scoped_event` | `add_event` + `clear_events_on_exit_state` |
|
||||||
Loading…
Reference in New Issue
Block a user