Request for comments: Simplified state machine


(John John Tedro) #1

Hey,

So I’m in the process of experimenting with simplifying the approach to architecting games with the existing application/state machine.

What I’ve done so far is create a new module called dynamic. Here I’ve forked the existing state machine and rewritten it.

The differences are:

I’ve also refactored the application to be more opinionated, it now takes care of building a single Dispatcher and automatically installing it to dispatch on update. Effectively GameDataBuilder and ApplicationBuilder has been merged into one.

The result is a much lighter State trait, which practically only needs to worry about implementing the relevant callbacks.

You can find my branch here: udoprog/amethyst
And this is asteroids-amethyst converted to use the enum-based state machine: udoprog/asteroids-amethyst

So I’m looking for feedback before I try to clean up the code: Does this seem like a reasonable direction to move towards?

EDIT:

To add an example of what states look like before and after this change:

pub struct MainState;

impl SimpleState<'a, 'b> for MainState {
  fn handle_event(
    &mut self, 
    _data: StateData<GameData>, 
    event: StateEvent
  ) -> SimpleTrans<'a, 'b> {
    return Trans::Push(Box::new(PauseState));
  }
}

pub struct PauseState;

impl SimpleState<'a, 'b> for PauseState {
  fn update(&mut self, data: StateData<GameData>) -> Trans<State> {
    // Note: current state is not available.
  }
}

fn main() {
  let game_data = GameDataBuilder::default()
    .with_bundle(/* ... */)?;

  let mut game = Application::build(assets_dir, MainState)?
    .with_frame_limit(FrameRateLimitStrategy::SleepAndYield(Duration::from_millis(2)), 144)
    .build(game_data)?;
}

Would become:

#[derive(Clone, PartialEq, Eq, Hash)]
pub enum State {
  Main,
  Pause,
}

pub struct MainState;

impl<E> StateCallback<State, E> for MainState {
  fn handle_event(
    &mut self, 
    world: &mut World,
    event: &E,
  ) -> Trans<State> {
    Trans::Push(State::Push)
  }
}

pub struct PauseState;

impl<E> StateCallback<State, E> for PauseState {
  fn update(&mut self, world: &mut World) -> Trans<State> {
    // Note: current state is available as a resource. This is also available to systems.
    println!("{:?}", world.read_resource::<State>());
  }
}

fn main() {
  let mut app = ApplicationBuilder::new(assets_dir, State::Main)
    .with_bundle(/* ... */)?
    .with_frame_limit(FrameRateLimitStrategy::SleepAndYield(Duration::from_millis(2)), 144)
    // Note: this is both where we map the state to a value, and set up the persistent instance.
    .with_state(State::Main, MainState)?
    .with_state(State::Pause, PauseState)?
    .build()?;
}

Since State is also made available as a resource, this allows us to do interesting stuff, like make use of pausable systems to make certain systems only run for certain states:

pub struct MainBundle;

impl<'a, 'b> SystemBundle<'a, 'b> for MainBundle {
    fn build(self, builder: &mut DispatcherBuilder<'a, 'b>) -> Result<()> {
        builder.add(KillBulletsSystem.pausable(State::Main), "kill_bullets", &[]);
        builder.add(RandomAsteroidSystem::new().pausable(State::Main), "random_asteroids", &[]);
        builder.add(ShipInputSystem.pausable(State::Main), "ship_input_system", &[]);
        builder.add(PhysicsSystem.pausable(State::Main), "physics_system", &[]);
        builder.add(LimitObjectsSystem.pausable(State::Main), "limit_objects", &["physics_system"]);
        builder.add(CollisionSystem.pausable(State::Main), "collisions", &["physics_system"]);
        builder.add(HandleUiSystem.pausable(State::Main), "handle_ui", &[]);
        Ok(())
    }
}

Differences and similarities of states and systems
(Thomas Schaller) split this topic #2

2 posts were split to a new topic: Differences and similarities of states and systems