Path: blob/main/crates/bevy_state/src/state_scoped_events.rs
6849 views
use alloc::vec::Vec;1use core::marker::PhantomData;23use bevy_app::{App, SubApp};4use bevy_ecs::{5message::{Message, MessageReader, Messages},6resource::Resource,7system::Commands,8world::World,9};10use bevy_platform::collections::HashMap;1112use crate::state::{OnEnter, OnExit, StateTransitionEvent, States};1314fn clear_message_queue<M: Message>(w: &mut World) {15if let Some(mut queue) = w.get_resource_mut::<Messages<M>>() {16queue.clear();17}18}1920#[derive(Copy, Clone)]21enum TransitionType {22OnExit,23OnEnter,24}2526#[derive(Resource)]27struct StateScopedMessages<S: States> {28/// Keeps track of which messages need to be reset when the state is exited.29on_exit: HashMap<S, Vec<fn(&mut World)>>,30/// Keeps track of which messages need to be reset when the state is entered.31on_enter: HashMap<S, Vec<fn(&mut World)>>,32}3334impl<S: States> StateScopedMessages<S> {35fn add_message<M: Message>(&mut self, state: S, transition_type: TransitionType) {36let map = match transition_type {37TransitionType::OnExit => &mut self.on_exit,38TransitionType::OnEnter => &mut self.on_enter,39};40map.entry(state).or_default().push(clear_message_queue::<M>);41}4243fn cleanup(&self, w: &mut World, state: S, transition_type: TransitionType) {44let map = match transition_type {45TransitionType::OnExit => &self.on_exit,46TransitionType::OnEnter => &self.on_enter,47};48let Some(fns) = map.get(&state) else {49return;50};51for callback in fns {52(*callback)(w);53}54}55}5657impl<S: States> Default for StateScopedMessages<S> {58fn default() -> Self {59Self {60on_exit: HashMap::default(),61on_enter: HashMap::default(),62}63}64}6566fn clear_messages_on_exit<S: States>(67mut c: Commands,68mut transitions: MessageReader<StateTransitionEvent<S>>,69) {70let Some(transition) = transitions.read().last() else {71return;72};73if transition.entered == transition.exited {74return;75}76let Some(exited) = transition.exited.clone() else {77return;78};7980c.queue(move |w: &mut World| {81w.resource_scope::<StateScopedMessages<S>, ()>(|w, messages| {82messages.cleanup(w, exited, TransitionType::OnExit);83});84});85}8687fn clear_messages_on_enter<S: States>(88mut c: Commands,89mut transitions: MessageReader<StateTransitionEvent<S>>,90) {91let Some(transition) = transitions.read().last() else {92return;93};94if transition.entered == transition.exited {95return;96}97let Some(entered) = transition.entered.clone() else {98return;99};100101c.queue(move |w: &mut World| {102w.resource_scope::<StateScopedMessages<S>, ()>(|w, messages| {103messages.cleanup(w, entered, TransitionType::OnEnter);104});105});106}107108fn clear_messages_on_state_transition<M: Message, S: States>(109app: &mut SubApp,110_p: PhantomData<M>,111state: S,112transition_type: TransitionType,113) {114if !app.world().contains_resource::<StateScopedMessages<S>>() {115app.init_resource::<StateScopedMessages<S>>();116}117app.world_mut()118.resource_mut::<StateScopedMessages<S>>()119.add_message::<M>(state.clone(), transition_type);120match transition_type {121TransitionType::OnExit => app.add_systems(OnExit(state), clear_messages_on_exit::<S>),122TransitionType::OnEnter => app.add_systems(OnEnter(state), clear_messages_on_enter::<S>),123};124}125126/// Extension trait for [`App`] adding methods for registering state scoped messages.127pub trait StateScopedMessagesAppExt {128/// Clears a [`Message`] when exiting the specified `state`.129///130/// Note that message cleanup is ambiguously ordered relative to131/// [`DespawnOnExit`](crate::prelude::DespawnOnExit) entity cleanup,132/// and the [`OnExit`] schedule for the target state.133/// All of these (state scoped entities and messages cleanup, and `OnExit`)134/// occur within schedule [`StateTransition`](crate::prelude::StateTransition)135/// and system set `StateTransitionSystems::ExitSchedules`.136fn clear_messages_on_exit<M: Message>(&mut self, state: impl States) -> &mut Self;137138/// Clears a [`Message`] when entering the specified `state`.139///140/// Note that message cleanup is ambiguously ordered relative to141/// [`DespawnOnEnter`](crate::prelude::DespawnOnEnter) entity cleanup,142/// and the [`OnEnter`] schedule for the target state.143/// All of these (state scoped entities and messages cleanup, and `OnEnter`)144/// occur within schedule [`StateTransition`](crate::prelude::StateTransition)145/// and system set `StateTransitionSystems::EnterSchedules`.146fn clear_messages_on_enter<M: Message>(&mut self, state: impl States) -> &mut Self;147}148149impl StateScopedMessagesAppExt for App {150fn clear_messages_on_exit<M: Message>(&mut self, state: impl States) -> &mut Self {151clear_messages_on_state_transition(152self.main_mut(),153PhantomData::<M>,154state,155TransitionType::OnExit,156);157self158}159160fn clear_messages_on_enter<M: Message>(&mut self, state: impl States) -> &mut Self {161clear_messages_on_state_transition(162self.main_mut(),163PhantomData::<M>,164state,165TransitionType::OnEnter,166);167self168}169}170171impl StateScopedMessagesAppExt for SubApp {172fn clear_messages_on_exit<M: Message>(&mut self, state: impl States) -> &mut Self {173clear_messages_on_state_transition(self, PhantomData::<M>, state, TransitionType::OnExit);174self175}176177fn clear_messages_on_enter<M: Message>(&mut self, state: impl States) -> &mut Self {178clear_messages_on_state_transition(self, PhantomData::<M>, state, TransitionType::OnEnter);179self180}181}182183#[cfg(test)]184mod tests {185use super::*;186use crate::app::StatesPlugin;187use bevy_ecs::message::Message;188use bevy_state::prelude::*;189190#[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)]191enum TestState {192#[default]193A,194B,195}196197#[derive(Message, Debug)]198struct StandardMessage;199200#[derive(Message, Debug)]201struct StateScopedMessage;202203#[test]204fn clear_message_on_exit_state() {205let mut app = App::new();206app.add_plugins(StatesPlugin);207app.init_state::<TestState>();208209app.add_message::<StandardMessage>();210app.add_message::<StateScopedMessage>()211.clear_messages_on_exit::<StateScopedMessage>(TestState::A);212213app.world_mut().write_message(StandardMessage).unwrap();214app.world_mut().write_message(StateScopedMessage).unwrap();215assert!(!app216.world()217.resource::<Messages<StandardMessage>>()218.is_empty());219assert!(!app220.world()221.resource::<Messages<StateScopedMessage>>()222.is_empty());223224app.world_mut()225.resource_mut::<NextState<TestState>>()226.set(TestState::B);227app.update();228229assert!(!app230.world()231.resource::<Messages<StandardMessage>>()232.is_empty());233assert!(app234.world()235.resource::<Messages<StateScopedMessage>>()236.is_empty());237}238239#[test]240fn clear_message_on_enter_state() {241let mut app = App::new();242app.add_plugins(StatesPlugin);243app.init_state::<TestState>();244245app.add_message::<StandardMessage>();246app.add_message::<StateScopedMessage>()247.clear_messages_on_enter::<StateScopedMessage>(TestState::B);248249app.world_mut().write_message(StandardMessage).unwrap();250app.world_mut().write_message(StateScopedMessage).unwrap();251assert!(!app252.world()253.resource::<Messages<StandardMessage>>()254.is_empty());255assert!(!app256.world()257.resource::<Messages<StateScopedMessage>>()258.is_empty());259260app.world_mut()261.resource_mut::<NextState<TestState>>()262.set(TestState::B);263app.update();264265assert!(!app266.world()267.resource::<Messages<StandardMessage>>()268.is_empty());269assert!(app270.world()271.resource::<Messages<StateScopedMessage>>()272.is_empty());273}274}275276277