mirror of
https://github.com/bevyengine/bevy
synced 2024-11-25 14:10:19 +00:00
Redo State architecture (#1424)
An alternative to StateStages that uses SystemSets. Also includes pop and push operations since this was originally developed for my personal project which needed them.
This commit is contained in:
parent
c3a72e9dc8
commit
284889c64b
8 changed files with 587 additions and 312 deletions
|
@ -7,7 +7,7 @@ use crate::{
|
|||
use bevy_ecs::{
|
||||
component::Component,
|
||||
schedule::{
|
||||
RunOnce, Schedule, Stage, StageLabel, StateStage, SystemDescriptor, SystemSet, SystemStage,
|
||||
RunOnce, Schedule, Stage, StageLabel, State, SystemDescriptor, SystemSet, SystemStage,
|
||||
},
|
||||
system::{IntoExclusiveSystem, IntoSystem},
|
||||
world::{FromWorld, World},
|
||||
|
@ -177,37 +177,18 @@ impl AppBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn on_state_enter<T: Clone + Component>(
|
||||
&mut self,
|
||||
stage: impl StageLabel,
|
||||
state: T,
|
||||
system: impl Into<SystemDescriptor>,
|
||||
) -> &mut Self {
|
||||
self.stage(stage, |stage: &mut StateStage<T>| {
|
||||
stage.on_state_enter(state, system)
|
||||
})
|
||||
pub fn add_state<T: Component + Clone + Eq>(&mut self, initial: T) -> &mut Self {
|
||||
self.insert_resource(State::new(initial))
|
||||
.add_system_set(State::<T>::make_driver())
|
||||
}
|
||||
|
||||
pub fn on_state_update<T: Clone + Component>(
|
||||
pub fn add_state_to_stage<T: Component + Clone + Eq>(
|
||||
&mut self,
|
||||
stage: impl StageLabel,
|
||||
state: T,
|
||||
system: impl Into<SystemDescriptor>,
|
||||
initial: T,
|
||||
) -> &mut Self {
|
||||
self.stage(stage, |stage: &mut StateStage<T>| {
|
||||
stage.on_state_update(state, system)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_state_exit<T: Clone + Component>(
|
||||
&mut self,
|
||||
stage: impl StageLabel,
|
||||
state: T,
|
||||
system: impl Into<SystemDescriptor>,
|
||||
) -> &mut Self {
|
||||
self.stage(stage, |stage: &mut StateStage<T>| {
|
||||
stage.on_state_exit(state, system)
|
||||
})
|
||||
self.insert_resource(State::new(initial))
|
||||
.add_system_set_to_stage(stage, State::<T>::make_driver())
|
||||
}
|
||||
|
||||
pub fn add_default_stages(&mut self) -> &mut Self {
|
||||
|
|
|
@ -19,7 +19,7 @@ pub mod prelude {
|
|||
query::{Added, Changed, Flags, Mutated, Or, QueryState, With, WithBundle, Without},
|
||||
schedule::{
|
||||
AmbiguitySetLabel, ExclusiveSystemDescriptorCoercion, ParallelSystemDescriptorCoercion,
|
||||
Schedule, Stage, StageLabel, State, StateStage, SystemLabel, SystemStage,
|
||||
Schedule, Stage, StageLabel, State, SystemLabel, SystemSet, SystemStage,
|
||||
},
|
||||
system::{
|
||||
Commands, In, IntoChainSystem, IntoExclusiveSystem, IntoSystem, Local, NonSend,
|
||||
|
|
|
@ -1,262 +1,550 @@
|
|||
use crate::{
|
||||
component::Component,
|
||||
schedule::{Stage, SystemDescriptor, SystemStage},
|
||||
world::World,
|
||||
schedule::{ShouldRun, SystemSet},
|
||||
system::{In, IntoChainSystem, IntoSystem, Local, Res, ResMut, System},
|
||||
};
|
||||
use bevy_utils::HashMap;
|
||||
use std::{mem::Discriminant, ops::Deref};
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) struct StateStages {
|
||||
update: Box<dyn Stage>,
|
||||
enter: Box<dyn Stage>,
|
||||
exit: Box<dyn Stage>,
|
||||
/// ### Stack based state machine
|
||||
///
|
||||
/// This state machine has three operations: Push, Pop, and Next.
|
||||
/// * Push pushes a new state to the state stack, pausing the previous state
|
||||
/// * Pop removes the current state, and unpauses the last paused state.
|
||||
/// * Next unwinds the state stack, and replaces the entire stack with a single new state
|
||||
#[derive(Debug)]
|
||||
pub struct State<T: Component + Clone + Eq> {
|
||||
transition: Option<StateTransition<T>>,
|
||||
stack: Vec<T>,
|
||||
scheduled: Option<ScheduledOperation<T>>,
|
||||
end_next_loop: bool,
|
||||
}
|
||||
|
||||
impl Default for StateStages {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enter: Box::new(SystemStage::parallel()),
|
||||
update: Box::new(SystemStage::parallel()),
|
||||
exit: Box::new(SystemStage::parallel()),
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
enum StateTransition<T: Component + Clone + Eq> {
|
||||
PreStartup,
|
||||
Startup,
|
||||
// The parameter order is always (leaving, entering)
|
||||
ExitingToResume(T, T),
|
||||
ExitingFull(T, T),
|
||||
Entering(T, T),
|
||||
Resuming(T, T),
|
||||
Pausing(T, T),
|
||||
}
|
||||
|
||||
pub struct StateStage<T> {
|
||||
stages: HashMap<Discriminant<T>, StateStages>,
|
||||
#[derive(Debug)]
|
||||
enum ScheduledOperation<T: Component + Clone + Eq> {
|
||||
Next(T),
|
||||
Pop,
|
||||
Push(T),
|
||||
}
|
||||
|
||||
impl<T> Default for StateStage<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stages: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::mem_discriminant_non_enum)]
|
||||
impl<T> StateStage<T> {
|
||||
pub fn with_enter_stage<S: Stage>(mut self, state: T, stage: S) -> Self {
|
||||
self.set_enter_stage(state, stage);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_exit_stage<S: Stage>(mut self, state: T, stage: S) -> Self {
|
||||
self.set_exit_stage(state, stage);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_update_stage<S: Stage>(mut self, state: T, stage: S) -> Self {
|
||||
self.set_update_stage(state, stage);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_enter_stage<S: Stage>(&mut self, state: T, stage: S) -> &mut Self {
|
||||
let stages = self.state_stages(state);
|
||||
stages.enter = Box::new(stage);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_exit_stage<S: Stage>(&mut self, state: T, stage: S) -> &mut Self {
|
||||
let stages = self.state_stages(state);
|
||||
stages.exit = Box::new(stage);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_update_stage<S: Stage>(&mut self, state: T, stage: S) -> &mut Self {
|
||||
let stages = self.state_stages(state);
|
||||
stages.update = Box::new(stage);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_state_enter(&mut self, state: T, system: impl Into<SystemDescriptor>) -> &mut Self {
|
||||
self.enter_stage(state, |system_stage: &mut SystemStage| {
|
||||
system_stage.add_system(system)
|
||||
impl<T: Component + Clone + Eq> State<T> {
|
||||
pub fn on_update(s: T) -> impl System<In = (), Out = ShouldRun> {
|
||||
(|state: Res<State<T>>, pred: Local<Option<T>>| {
|
||||
state.stack.last().unwrap() == pred.as_ref().unwrap() && state.transition.is_none()
|
||||
})
|
||||
.system()
|
||||
.config(|(_, pred)| *pred = Some(Some(s)))
|
||||
.chain(should_run_adapter::<T>.system())
|
||||
}
|
||||
|
||||
pub fn on_state_exit(&mut self, state: T, system: impl Into<SystemDescriptor>) -> &mut Self {
|
||||
self.exit_stage(state, |system_stage: &mut SystemStage| {
|
||||
system_stage.add_system(system)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_state_update(&mut self, state: T, system: impl Into<SystemDescriptor>) -> &mut Self {
|
||||
self.update_stage(state, |system_stage: &mut SystemStage| {
|
||||
system_stage.add_system(system)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enter_stage<S: Stage, F: FnOnce(&mut S) -> &mut S>(
|
||||
&mut self,
|
||||
state: T,
|
||||
func: F,
|
||||
) -> &mut Self {
|
||||
let stages = self.state_stages(state);
|
||||
func(
|
||||
stages
|
||||
.enter
|
||||
.downcast_mut()
|
||||
.expect("'Enter' stage does not match the given type"),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn exit_stage<S: Stage, F: FnOnce(&mut S) -> &mut S>(
|
||||
&mut self,
|
||||
state: T,
|
||||
func: F,
|
||||
) -> &mut Self {
|
||||
let stages = self.state_stages(state);
|
||||
func(
|
||||
stages
|
||||
.exit
|
||||
.downcast_mut()
|
||||
.expect("'Exit' stage does not match the given type"),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn update_stage<S: Stage, F: FnOnce(&mut S) -> &mut S>(
|
||||
&mut self,
|
||||
state: T,
|
||||
func: F,
|
||||
) -> &mut Self {
|
||||
let stages = self.state_stages(state);
|
||||
func(
|
||||
stages
|
||||
.update
|
||||
.downcast_mut()
|
||||
.expect("'Update' stage does not match the given type"),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
fn state_stages(&mut self, state: T) -> &mut StateStages {
|
||||
self.stages
|
||||
.entry(std::mem::discriminant(&state))
|
||||
.or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::mem_discriminant_non_enum)]
|
||||
impl<T: Component + Clone> Stage for StateStage<T> {
|
||||
fn run(&mut self, world: &mut World) {
|
||||
let current_stage = loop {
|
||||
let (next_stage, current_stage) = {
|
||||
let mut state = world
|
||||
.get_resource_mut::<State<T>>()
|
||||
.expect("Missing state resource");
|
||||
let result = (
|
||||
state.next.as_ref().map(|next| std::mem::discriminant(next)),
|
||||
std::mem::discriminant(&state.current),
|
||||
);
|
||||
|
||||
state.apply_next();
|
||||
|
||||
result
|
||||
};
|
||||
|
||||
// if next_stage is Some, we just applied a new state
|
||||
if let Some(next_stage) = next_stage {
|
||||
if next_stage != current_stage {
|
||||
if let Some(current_state_stages) = self.stages.get_mut(¤t_stage) {
|
||||
current_state_stages.exit.run(world);
|
||||
}
|
||||
pub fn on_inactive_update(s: T) -> impl System<In = (), Out = ShouldRun> {
|
||||
(|state: Res<State<T>>, mut is_inactive: Local<bool>, pred: Local<Option<T>>| match &state
|
||||
.transition
|
||||
{
|
||||
Some(StateTransition::Pausing(ref relevant, _))
|
||||
| Some(StateTransition::Resuming(_, ref relevant)) => {
|
||||
if relevant == pred.as_ref().unwrap() {
|
||||
*is_inactive = !*is_inactive;
|
||||
}
|
||||
|
||||
if let Some(next_state_stages) = self.stages.get_mut(&next_stage) {
|
||||
next_state_stages.enter.run(world);
|
||||
}
|
||||
} else {
|
||||
break current_stage;
|
||||
false
|
||||
}
|
||||
};
|
||||
Some(_) => false,
|
||||
None => *is_inactive,
|
||||
})
|
||||
.system()
|
||||
.config(|(_, _, pred)| *pred = Some(Some(s)))
|
||||
.chain(should_run_adapter::<T>.system())
|
||||
}
|
||||
|
||||
if let Some(current_state_stages) = self.stages.get_mut(¤t_stage) {
|
||||
current_state_stages.update.run(world);
|
||||
pub fn on_in_stack_update(s: T) -> impl System<In = (), Out = ShouldRun> {
|
||||
(|state: Res<State<T>>, mut is_in_stack: Local<bool>, pred: Local<Option<T>>| match &state
|
||||
.transition
|
||||
{
|
||||
Some(StateTransition::Entering(ref relevant, _))
|
||||
| Some(StateTransition::ExitingToResume(_, ref relevant)) => {
|
||||
if relevant == pred.as_ref().unwrap() {
|
||||
*is_in_stack = !*is_in_stack;
|
||||
}
|
||||
false
|
||||
}
|
||||
Some(StateTransition::ExitingFull(_, ref relevant)) => {
|
||||
if relevant == pred.as_ref().unwrap() {
|
||||
*is_in_stack = !*is_in_stack;
|
||||
}
|
||||
false
|
||||
}
|
||||
Some(StateTransition::Startup) => {
|
||||
if state.stack.last().unwrap() == pred.as_ref().unwrap() {
|
||||
*is_in_stack = !*is_in_stack;
|
||||
}
|
||||
false
|
||||
}
|
||||
Some(_) => false,
|
||||
None => *is_in_stack,
|
||||
})
|
||||
.system()
|
||||
.config(|(_, _, pred)| *pred = Some(Some(s)))
|
||||
.chain(should_run_adapter::<T>.system())
|
||||
}
|
||||
|
||||
pub fn on_enter(s: T) -> impl System<In = (), Out = ShouldRun> {
|
||||
(|state: Res<State<T>>, pred: Local<Option<T>>| {
|
||||
state
|
||||
.transition
|
||||
.as_ref()
|
||||
.map_or(false, |transition| match transition {
|
||||
StateTransition::Entering(_, entering) => entering == pred.as_ref().unwrap(),
|
||||
StateTransition::Startup => {
|
||||
state.stack.last().unwrap() == pred.as_ref().unwrap()
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
.system()
|
||||
.config(|(_, pred)| *pred = Some(Some(s)))
|
||||
.chain(should_run_adapter::<T>.system())
|
||||
}
|
||||
|
||||
pub fn on_exit(s: T) -> impl System<In = (), Out = ShouldRun> {
|
||||
(|state: Res<State<T>>, pred: Local<Option<T>>| {
|
||||
state
|
||||
.transition
|
||||
.as_ref()
|
||||
.map_or(false, |transition| match transition {
|
||||
StateTransition::ExitingToResume(exiting, _)
|
||||
| StateTransition::ExitingFull(exiting, _) => exiting == pred.as_ref().unwrap(),
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
.system()
|
||||
.config(|(_, pred)| *pred = Some(Some(s)))
|
||||
.chain(should_run_adapter::<T>.system())
|
||||
}
|
||||
|
||||
pub fn on_pause(s: T) -> impl System<In = (), Out = ShouldRun> {
|
||||
(|state: Res<State<T>>, pred: Local<Option<T>>| {
|
||||
state
|
||||
.transition
|
||||
.as_ref()
|
||||
.map_or(false, |transition| match transition {
|
||||
StateTransition::Pausing(pausing, _) => pausing == pred.as_ref().unwrap(),
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
.system()
|
||||
.config(|(_, pred)| *pred = Some(Some(s)))
|
||||
.chain(should_run_adapter::<T>.system())
|
||||
}
|
||||
|
||||
pub fn on_resume(s: T) -> impl System<In = (), Out = ShouldRun> {
|
||||
(|state: Res<State<T>>, pred: Local<Option<T>>| {
|
||||
state
|
||||
.transition
|
||||
.as_ref()
|
||||
.map_or(false, |transition| match transition {
|
||||
StateTransition::Resuming(_, resuming) => resuming == pred.as_ref().unwrap(),
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
.system()
|
||||
.config(|(_, pred)| *pred = Some(Some(s)))
|
||||
.chain(should_run_adapter::<T>.system())
|
||||
}
|
||||
|
||||
pub fn on_update_set(s: T) -> SystemSet {
|
||||
SystemSet::new().with_run_criteria(Self::on_update(s))
|
||||
}
|
||||
|
||||
pub fn on_inactive_update_set(s: T) -> SystemSet {
|
||||
SystemSet::new().with_run_criteria(Self::on_inactive_update(s))
|
||||
}
|
||||
|
||||
pub fn on_enter_set(s: T) -> SystemSet {
|
||||
SystemSet::new().with_run_criteria(Self::on_enter(s))
|
||||
}
|
||||
|
||||
pub fn on_exit_set(s: T) -> SystemSet {
|
||||
SystemSet::new().with_run_criteria(Self::on_exit(s))
|
||||
}
|
||||
|
||||
pub fn on_pause_set(s: T) -> SystemSet {
|
||||
SystemSet::new().with_run_criteria(Self::on_pause(s))
|
||||
}
|
||||
|
||||
pub fn on_resume_set(s: T) -> SystemSet {
|
||||
SystemSet::new().with_run_criteria(Self::on_resume(s))
|
||||
}
|
||||
|
||||
/// Creates a driver set for the State.
|
||||
///
|
||||
/// Important note: this set must be inserted **before** all other state-dependant sets to work properly!
|
||||
pub fn make_driver() -> SystemSet {
|
||||
SystemSet::default().with_run_criteria(state_cleaner::<T>.system())
|
||||
}
|
||||
|
||||
pub fn new(initial: T) -> Self {
|
||||
Self {
|
||||
stack: vec![initial],
|
||||
transition: Some(StateTransition::PreStartup),
|
||||
scheduled: None,
|
||||
end_next_loop: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule a state change that replaces the full stack with the given state.
|
||||
/// This will fail if there is a scheduled operation, or if the given `state` matches the current state
|
||||
pub fn set_next(&mut self, state: T) -> Result<(), StateError> {
|
||||
if self.stack.last().unwrap() == &state {
|
||||
return Err(StateError::AlreadyInState);
|
||||
}
|
||||
|
||||
if self.scheduled.is_some() {
|
||||
return Err(StateError::StateAlreadyQueued);
|
||||
}
|
||||
|
||||
self.scheduled = Some(ScheduledOperation::Next(state));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Same as [Self::set_next], but if there is already a next state, it will be overwritten instead of failing
|
||||
pub fn overwrite_next(&mut self, state: T) -> Result<(), StateError> {
|
||||
if self.stack.last().unwrap() == &state {
|
||||
return Err(StateError::AlreadyInState);
|
||||
}
|
||||
|
||||
self.scheduled = Some(ScheduledOperation::Next(state));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Same as [Self::set_next], but does a push operation instead of a next operation
|
||||
pub fn set_push(&mut self, state: T) -> Result<(), StateError> {
|
||||
if self.stack.last().unwrap() == &state {
|
||||
return Err(StateError::AlreadyInState);
|
||||
}
|
||||
|
||||
if self.scheduled.is_some() {
|
||||
return Err(StateError::StateAlreadyQueued);
|
||||
}
|
||||
|
||||
self.scheduled = Some(ScheduledOperation::Push(state));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Same as [Self::set_push], but if there is already a next state, it will be overwritten instead of failing
|
||||
pub fn overwrite_push(&mut self, state: T) -> Result<(), StateError> {
|
||||
if self.stack.last().unwrap() == &state {
|
||||
return Err(StateError::AlreadyInState);
|
||||
}
|
||||
|
||||
self.scheduled = Some(ScheduledOperation::Push(state));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Same as [Self::set_next], but does a pop operation instead of a next operation
|
||||
pub fn set_pop(&mut self) -> Result<(), StateError> {
|
||||
if self.scheduled.is_some() {
|
||||
return Err(StateError::StateAlreadyQueued);
|
||||
}
|
||||
|
||||
if self.stack.len() == 1 {
|
||||
return Err(StateError::StackEmpty);
|
||||
}
|
||||
|
||||
self.scheduled = Some(ScheduledOperation::Pop);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Same as [Self::set_pop], but if there is already a next state, it will be overwritten instead of failing
|
||||
pub fn overwrite_pop(&mut self) -> Result<(), StateError> {
|
||||
if self.stack.len() == 1 {
|
||||
return Err(StateError::StackEmpty);
|
||||
}
|
||||
self.scheduled = Some(ScheduledOperation::Pop);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn current(&self) -> &T {
|
||||
self.stack.last().unwrap()
|
||||
}
|
||||
|
||||
pub fn inactives(&self) -> &[T] {
|
||||
&self.stack[0..self.stack.len() - 2]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum StateError {
|
||||
#[error("Attempted to change the state to the current state.")]
|
||||
AlreadyInState,
|
||||
#[error("Attempted to queue a state change, but there was already a state queued.")]
|
||||
StateAlreadyQueued,
|
||||
#[error("Attempted to queue a pop, but there is nothing to pop.")]
|
||||
StackEmpty,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State<T: Clone> {
|
||||
previous: Option<T>,
|
||||
current: T,
|
||||
next: Option<T>,
|
||||
fn should_run_adapter<T: Component + Clone + Eq>(
|
||||
In(cmp_result): In<bool>,
|
||||
state: Res<State<T>>,
|
||||
) -> ShouldRun {
|
||||
if state.end_next_loop {
|
||||
return ShouldRun::No;
|
||||
}
|
||||
if cmp_result {
|
||||
ShouldRun::YesAndCheckAgain
|
||||
} else {
|
||||
ShouldRun::NoAndCheckAgain
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::mem_discriminant_non_enum)]
|
||||
impl<T: Clone> State<T> {
|
||||
pub fn new(state: T) -> Self {
|
||||
Self {
|
||||
current: state.clone(),
|
||||
previous: None,
|
||||
// add value to queue so that we "enter" the state
|
||||
next: Some(state),
|
||||
fn state_cleaner<T: Component + Clone + Eq>(
|
||||
mut state: ResMut<State<T>>,
|
||||
mut prep_exit: Local<bool>,
|
||||
) -> ShouldRun {
|
||||
if *prep_exit {
|
||||
*prep_exit = false;
|
||||
if state.scheduled.is_none() {
|
||||
state.end_next_loop = true;
|
||||
return ShouldRun::YesAndCheckAgain;
|
||||
}
|
||||
} else if state.end_next_loop {
|
||||
state.end_next_loop = false;
|
||||
return ShouldRun::No;
|
||||
}
|
||||
|
||||
pub fn current(&self) -> &T {
|
||||
&self.current
|
||||
}
|
||||
|
||||
pub fn previous(&self) -> Option<&T> {
|
||||
self.previous.as_ref()
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Option<&T> {
|
||||
self.next.as_ref()
|
||||
}
|
||||
|
||||
/// Queue a state change. This will fail if there is already a state in the queue, or if the
|
||||
/// given `state` matches the current state
|
||||
pub fn set_next(&mut self, state: T) -> Result<(), StateError> {
|
||||
if std::mem::discriminant(&self.current) == std::mem::discriminant(&state) {
|
||||
return Err(StateError::AlreadyInState);
|
||||
}
|
||||
|
||||
if self.next.is_some() {
|
||||
return Err(StateError::StateAlreadyQueued);
|
||||
}
|
||||
|
||||
self.next = Some(state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Same as [Self::set_next], but if there is already a next state, it will be overwritten
|
||||
/// instead of failing
|
||||
pub fn overwrite_next(&mut self, state: T) -> Result<(), StateError> {
|
||||
if std::mem::discriminant(&self.current) == std::mem::discriminant(&state) {
|
||||
return Err(StateError::AlreadyInState);
|
||||
}
|
||||
|
||||
self.next = Some(state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_next(&mut self) {
|
||||
if let Some(next) = self.next.take() {
|
||||
let previous = std::mem::replace(&mut self.current, next);
|
||||
if std::mem::discriminant(&previous) != std::mem::discriminant(&self.current) {
|
||||
self.previous = Some(previous)
|
||||
match state.scheduled.take() {
|
||||
Some(ScheduledOperation::Next(next)) => {
|
||||
if state.stack.len() <= 1 {
|
||||
state.transition = Some(StateTransition::ExitingFull(
|
||||
state.stack.last().unwrap().clone(),
|
||||
next,
|
||||
));
|
||||
} else {
|
||||
state.scheduled = Some(ScheduledOperation::Next(next));
|
||||
match state.transition.take() {
|
||||
Some(StateTransition::ExitingToResume(p, n)) => {
|
||||
state.stack.pop();
|
||||
state.transition = Some(StateTransition::Resuming(p, n));
|
||||
}
|
||||
_ => {
|
||||
state.transition = Some(StateTransition::ExitingToResume(
|
||||
state.stack[state.stack.len() - 1].clone(),
|
||||
state.stack[state.stack.len() - 2].clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(ScheduledOperation::Push(next)) => {
|
||||
let last_type_id = state.stack.last().unwrap().clone();
|
||||
state.transition = Some(StateTransition::Pausing(last_type_id, next));
|
||||
}
|
||||
Some(ScheduledOperation::Pop) => {
|
||||
state.transition = Some(StateTransition::ExitingToResume(
|
||||
state.stack[state.stack.len() - 1].clone(),
|
||||
state.stack[state.stack.len() - 2].clone(),
|
||||
));
|
||||
}
|
||||
None => match state.transition.take() {
|
||||
Some(StateTransition::ExitingFull(p, n)) => {
|
||||
state.transition = Some(StateTransition::Entering(p, n.clone()));
|
||||
state.stack[0] = n;
|
||||
}
|
||||
Some(StateTransition::Pausing(p, n)) => {
|
||||
state.transition = Some(StateTransition::Entering(p, n.clone()));
|
||||
state.stack.push(n);
|
||||
}
|
||||
Some(StateTransition::ExitingToResume(p, n)) => {
|
||||
state.stack.pop();
|
||||
state.transition = Some(StateTransition::Resuming(p, n));
|
||||
}
|
||||
Some(StateTransition::PreStartup) => {
|
||||
state.transition = Some(StateTransition::Startup);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
};
|
||||
if state.transition.is_none() {
|
||||
*prep_exit = true;
|
||||
}
|
||||
|
||||
ShouldRun::YesAndCheckAgain
|
||||
}
|
||||
|
||||
impl<T: Clone> Deref for State<T> {
|
||||
type Target = T;
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.current
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
enum MyState {
|
||||
S1,
|
||||
S2,
|
||||
S3,
|
||||
S4,
|
||||
S5,
|
||||
S6,
|
||||
Final,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_test() {
|
||||
let mut world = World::default();
|
||||
|
||||
world.insert_resource(Vec::<&'static str>::new());
|
||||
world.insert_resource(State::new(MyState::S1));
|
||||
|
||||
let mut stage = SystemStage::parallel();
|
||||
|
||||
stage.add_system_set(State::<MyState>::make_driver());
|
||||
stage
|
||||
.add_system_set(
|
||||
State::on_enter_set(MyState::S1)
|
||||
.with_system((|mut r: ResMut<Vec<&'static str>>| r.push("startup")).system()),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_update_set(MyState::S1).with_system(
|
||||
(|mut r: ResMut<Vec<&'static str>>, mut s: ResMut<State<MyState>>| {
|
||||
r.push("update S1");
|
||||
s.overwrite_next(MyState::S2).unwrap();
|
||||
})
|
||||
.system(),
|
||||
),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_enter_set(MyState::S2)
|
||||
.with_system((|mut r: ResMut<Vec<&'static str>>| r.push("enter S2")).system()),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_update_set(MyState::S2).with_system(
|
||||
(|mut r: ResMut<Vec<&'static str>>, mut s: ResMut<State<MyState>>| {
|
||||
r.push("update S2");
|
||||
s.overwrite_next(MyState::S3).unwrap();
|
||||
})
|
||||
.system(),
|
||||
),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_exit_set(MyState::S2)
|
||||
.with_system((|mut r: ResMut<Vec<&'static str>>| r.push("exit S2")).system()),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_enter_set(MyState::S3)
|
||||
.with_system((|mut r: ResMut<Vec<&'static str>>| r.push("enter S3")).system()),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_update_set(MyState::S3).with_system(
|
||||
(|mut r: ResMut<Vec<&'static str>>, mut s: ResMut<State<MyState>>| {
|
||||
r.push("update S3");
|
||||
s.overwrite_push(MyState::S4).unwrap();
|
||||
})
|
||||
.system(),
|
||||
),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_pause_set(MyState::S3)
|
||||
.with_system((|mut r: ResMut<Vec<&'static str>>| r.push("pause S3")).system()),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_update_set(MyState::S4).with_system(
|
||||
(|mut r: ResMut<Vec<&'static str>>, mut s: ResMut<State<MyState>>| {
|
||||
r.push("update S4");
|
||||
s.overwrite_push(MyState::S5).unwrap();
|
||||
})
|
||||
.system(),
|
||||
),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_inactive_update_set(MyState::S4).with_system(
|
||||
(|mut r: ResMut<Vec<&'static str>>| r.push("inactive S4"))
|
||||
.system()
|
||||
.label("inactive s4"),
|
||||
),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_update_set(MyState::S5).with_system(
|
||||
(|mut r: ResMut<Vec<&'static str>>, mut s: ResMut<State<MyState>>| {
|
||||
r.push("update S5");
|
||||
s.overwrite_push(MyState::S6).unwrap();
|
||||
})
|
||||
.system()
|
||||
.after("inactive s4"),
|
||||
),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_inactive_update_set(MyState::S5).with_system(
|
||||
(|mut r: ResMut<Vec<&'static str>>| r.push("inactive S5"))
|
||||
.system()
|
||||
.label("inactive s5")
|
||||
.after("inactive s4"),
|
||||
),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_update_set(MyState::S6).with_system(
|
||||
(|mut r: ResMut<Vec<&'static str>>, mut s: ResMut<State<MyState>>| {
|
||||
r.push("update S6");
|
||||
s.overwrite_push(MyState::Final).unwrap();
|
||||
})
|
||||
.system()
|
||||
.after("inactive s5"),
|
||||
),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_resume_set(MyState::S4)
|
||||
.with_system((|mut r: ResMut<Vec<&'static str>>| r.push("resume S4")).system()),
|
||||
)
|
||||
.add_system_set(
|
||||
State::on_exit_set(MyState::S5)
|
||||
.with_system((|mut r: ResMut<Vec<&'static str>>| r.push("exit S4")).system()),
|
||||
);
|
||||
|
||||
const EXPECTED: &[&str] = &[
|
||||
//
|
||||
"startup",
|
||||
"update S1",
|
||||
//
|
||||
"enter S2",
|
||||
"update S2",
|
||||
//
|
||||
"exit S2",
|
||||
"enter S3",
|
||||
"update S3",
|
||||
//
|
||||
"pause S3",
|
||||
"update S4",
|
||||
//
|
||||
"inactive S4",
|
||||
"update S5",
|
||||
//
|
||||
"inactive S4",
|
||||
"inactive S5",
|
||||
"update S6",
|
||||
//
|
||||
"inactive S4",
|
||||
"inactive S5",
|
||||
];
|
||||
|
||||
stage.run(&mut world);
|
||||
let mut collected = world.get_resource_mut::<Vec<&'static str>>().unwrap();
|
||||
let mut count = 0;
|
||||
for (found, expected) in collected.drain(..).zip(EXPECTED) {
|
||||
assert_eq!(found, *expected);
|
||||
count += 1;
|
||||
}
|
||||
// If not equal, some elements weren't executed
|
||||
assert_eq!(EXPECTED.len(), count);
|
||||
assert_eq!(
|
||||
world.get_resource::<State<MyState>>().unwrap().current(),
|
||||
&MyState::Final
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
schedule::{RunCriteria, ShouldRun, SystemDescriptor},
|
||||
component::Component,
|
||||
schedule::{RunCriteria, ShouldRun, State, SystemDescriptor},
|
||||
system::System,
|
||||
};
|
||||
|
||||
|
@ -45,4 +46,28 @@ impl SystemSet {
|
|||
self.descriptors.push(system.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_update<T: Component + Clone + Eq>(s: T) -> SystemSet {
|
||||
Self::new().with_run_criteria(State::<T>::on_update(s))
|
||||
}
|
||||
|
||||
pub fn on_inactive_update<T: Component + Clone + Eq>(s: T) -> SystemSet {
|
||||
Self::new().with_run_criteria(State::<T>::on_inactive_update(s))
|
||||
}
|
||||
|
||||
pub fn on_enter<T: Component + Clone + Eq>(s: T) -> SystemSet {
|
||||
Self::new().with_run_criteria(State::<T>::on_enter(s))
|
||||
}
|
||||
|
||||
pub fn on_exit<T: Component + Clone + Eq>(s: T) -> SystemSet {
|
||||
Self::new().with_run_criteria(State::<T>::on_exit(s))
|
||||
}
|
||||
|
||||
pub fn on_pause<T: Component + Clone + Eq>(s: T) -> SystemSet {
|
||||
Self::new().with_run_criteria(State::<T>::on_pause(s))
|
||||
}
|
||||
|
||||
pub fn on_resume<T: Component + Clone + Eq>(s: T) -> SystemSet {
|
||||
Self::new().with_run_criteria(State::<T>::on_resume(s))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,18 +6,14 @@ fn main() {
|
|||
App::build()
|
||||
.init_resource::<RpgSpriteHandles>()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.insert_resource(State::new(AppState::Setup))
|
||||
.add_stage_after(CoreStage::Update, Stage, StateStage::<AppState>::default())
|
||||
.on_state_enter(Stage, AppState::Setup, load_textures.system())
|
||||
.on_state_update(Stage, AppState::Setup, check_textures.system())
|
||||
.on_state_enter(Stage, AppState::Finished, setup.system())
|
||||
.add_state(AppState::Setup)
|
||||
.add_system_set(SystemSet::on_enter(AppState::Setup).with_system(load_textures.system()))
|
||||
.add_system_set(SystemSet::on_update(AppState::Setup).with_system(check_textures.system()))
|
||||
.add_system_set(SystemSet::on_enter(AppState::Finished).with_system(setup.system()))
|
||||
.run();
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)]
|
||||
struct Stage;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
Setup,
|
||||
Finished,
|
||||
|
|
|
@ -6,21 +6,20 @@ fn main() {
|
|||
App::build()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.init_resource::<ButtonMaterials>()
|
||||
.insert_resource(State::new(AppState::Menu))
|
||||
.add_stage_after(CoreStage::Update, Stage, StateStage::<AppState>::default())
|
||||
.on_state_enter(Stage, AppState::Menu, setup_menu.system())
|
||||
.on_state_update(Stage, AppState::Menu, menu.system())
|
||||
.on_state_exit(Stage, AppState::Menu, cleanup_menu.system())
|
||||
.on_state_enter(Stage, AppState::InGame, setup_game.system())
|
||||
.on_state_update(Stage, AppState::InGame, movement.system())
|
||||
.on_state_update(Stage, AppState::InGame, change_color.system())
|
||||
.add_state(AppState::Menu)
|
||||
.add_system_set(SystemSet::on_enter(AppState::Menu).with_system(setup_menu.system()))
|
||||
.add_system_set(SystemSet::on_update(AppState::Menu).with_system(menu.system()))
|
||||
.add_system_set(SystemSet::on_exit(AppState::Menu).with_system(cleanup_menu.system()))
|
||||
.add_system_set(SystemSet::on_enter(AppState::InGame).with_system(setup_game.system()))
|
||||
.add_system_set(
|
||||
SystemSet::on_update(AppState::InGame)
|
||||
.with_system(movement.system())
|
||||
.with_system(change_color.system()),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)]
|
||||
struct Stage;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
enum AppState {
|
||||
Menu,
|
||||
InGame,
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
use bevy::{
|
||||
core::FixedTimestep,
|
||||
ecs::schedule::SystemSet,
|
||||
prelude::*,
|
||||
render::{camera::Camera, render_graph::base::camera::CAMERA_3D},
|
||||
};
|
||||
use rand::Rng;
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)]
|
||||
enum GameStage {
|
||||
InGame,
|
||||
BonusUpdate,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
enum GameState {
|
||||
Playing,
|
||||
GameOver,
|
||||
|
@ -22,38 +17,26 @@ fn main() {
|
|||
.insert_resource(Msaa { samples: 4 })
|
||||
.init_resource::<Game>()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.insert_resource(State::new(GameState::Playing))
|
||||
.add_state(GameState::Playing)
|
||||
.add_startup_system(setup_cameras.system())
|
||||
.add_stage_after(
|
||||
CoreStage::Update,
|
||||
GameStage::InGame,
|
||||
StateStage::<GameState>::default(),
|
||||
.add_system_set(SystemSet::on_enter(GameState::Playing).with_system(setup.system()))
|
||||
.add_system_set(
|
||||
SystemSet::on_update(GameState::Playing)
|
||||
.with_system(move_player.system())
|
||||
.with_system(focus_camera.system())
|
||||
.with_system(rotate_bonus.system())
|
||||
.with_system(scoreboard_system.system()),
|
||||
)
|
||||
.on_state_enter(GameStage::InGame, GameState::Playing, setup.system())
|
||||
.on_state_update(GameStage::InGame, GameState::Playing, move_player.system())
|
||||
.on_state_update(GameStage::InGame, GameState::Playing, focus_camera.system())
|
||||
.on_state_update(GameStage::InGame, GameState::Playing, rotate_bonus.system())
|
||||
.on_state_update(
|
||||
GameStage::InGame,
|
||||
GameState::Playing,
|
||||
scoreboard_system.system(),
|
||||
.add_system_set(SystemSet::on_exit(GameState::Playing).with_system(teardown.system()))
|
||||
.add_system_set(
|
||||
SystemSet::on_enter(GameState::GameOver).with_system(display_score.system()),
|
||||
)
|
||||
.on_state_exit(GameStage::InGame, GameState::Playing, teardown.system())
|
||||
.on_state_enter(
|
||||
GameStage::InGame,
|
||||
GameState::GameOver,
|
||||
display_score.system(),
|
||||
.add_system_set(
|
||||
SystemSet::on_update(GameState::GameOver).with_system(gameover_keyboard.system()),
|
||||
)
|
||||
.on_state_update(
|
||||
GameStage::InGame,
|
||||
GameState::GameOver,
|
||||
gameover_keyboard.system(),
|
||||
)
|
||||
.on_state_exit(GameStage::InGame, GameState::GameOver, teardown.system())
|
||||
.add_stage_after(
|
||||
CoreStage::Update,
|
||||
GameStage::BonusUpdate,
|
||||
SystemStage::parallel()
|
||||
.add_system_set(SystemSet::on_exit(GameState::GameOver).with_system(teardown.system()))
|
||||
.add_system_set(
|
||||
SystemSet::new()
|
||||
.with_run_criteria(FixedTimestep::step(5.0))
|
||||
.with_system(spawn_bonus.system()),
|
||||
)
|
||||
|
|
|
@ -16,11 +16,14 @@ use bevy::{
|
|||
fn main() {
|
||||
App::build()
|
||||
.insert_resource(Msaa { samples: 4 })
|
||||
.insert_resource(State::new(AppState::CreateWindow))
|
||||
.add_state(AppState::CreateWindow)
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_stage_after(CoreStage::Update, Stage, StateStage::<AppState>::default())
|
||||
.on_state_update(Stage, AppState::CreateWindow, setup_window.system())
|
||||
.on_state_enter(Stage, AppState::Setup, setup_pipeline.system())
|
||||
.add_system_set(
|
||||
SystemSet::on_update(AppState::CreateWindow).with_system(setup_window.system()),
|
||||
)
|
||||
.add_system_set(
|
||||
SystemSet::on_enter(AppState::CreateWindow).with_system(setup_pipeline.system()),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
|
@ -29,7 +32,7 @@ pub struct Stage;
|
|||
|
||||
// NOTE: this "state based" approach to multiple windows is a short term workaround.
|
||||
// Future Bevy releases shouldn't require such a strict order of operations.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
enum AppState {
|
||||
CreateWindow,
|
||||
Setup,
|
||||
|
|
Loading…
Reference in a new issue