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:
TheRawMeatball 2021-03-15 22:12:04 +00:00
parent c3a72e9dc8
commit 284889c64b
8 changed files with 587 additions and 312 deletions

View file

@ -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 {

View file

@ -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,

View file

@ -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(&current_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(&current_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
);
}
}

View file

@ -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))
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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()),
)

View file

@ -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,