machinae

This is the documentation for machinae, a generic state machine intended to be primarily used in game development.

In addition to this book you can find a list of the items in the API documentation.

You'll mostly need the types StateMachine, State and Trans. The state machine stores a stack of states and updates that stack according to the Transitions returned by the current state.

Book structure

This book is split into five chapters, this being the introduction. After this chapter:

Example code

extern crate machinae;

use machinae::*;

#[derive(Debug)]
struct Error;

enum Event {
    WindowQuit,
    KeyPress(char),
}

struct Game {
    // ..
}

enum GameState {
    Loading,
    MainMenu,
    InGame,
}

impl<'a> State<&'a mut Game, Error, Event> for GameState {
    fn start(&mut self, args: &mut Game) -> Result<Trans<Self>, Error> {
        match *self {
            GameState::Loading => {
                args.load("buttons");
                args.load("player");
                
                Trans::None
            }
            GameState::MainMenu => {}
            GameState::InGame => {
                if !args.login() {
                    Trans::None
                } else {
                    eprintln!("Login failed");
                                        
                    Trans::Pop
                }
            }
        }
    }

    // all methods have a default no-op implementation,
    // so you can also leave them out
    fn resume(&mut self, _: &mut Game) {}

    fn pause(&mut self, _: &mut Game) {}

    fn stop(&mut self, args: &mut Game) {
        match *self {
            GameState::Loading => {}
            GameState::MainMenu => {}
            GameState::InGame => args.logout(),
        }
    }

    fn update(&mut self, args: &mut Game) -> Result<Trans<Self>, Error> {
        match *self {
            GameState::Loading => {
                let progress = args.progress();
                args.draw_bar(progress);
                
                if progress == 1.0 {
                    Trans::Switch(GameState::MainMenu)
                } else {
                    Trans::None
                }
            }
            GameState::MainMenu => {
                if args.button("start_game") {
                    Trans::Push(GameState::InGame)
                } else {
                    Trans::None
                }
            }
            GameState::InGame => {
                args.draw("player");
                
                if args.is_alive("player") {
                    Trans::None
                } else {
                    // Don't let the user rage quit
                    Trans::Quit
                }
            },
        }
    }

    fn fixed_update(&mut self, args: &mut Game) -> Result<Trans<Self>, Error> {
        match *self {
            GameState::Loading => {}
            GameState::MainMenu => {}
            GameState::InGame => args.do_physics(),
        }
    }

    fn event(&mut self, args: &mut Game, event: Event) -> Result<Trans<Self>, Error> {
        match event {
            Event::KeyPress('w') => args.translate("player", 3.0, 0.0),
            Event::KeyPress('q') => Trans::Quit,
            Event::WindowQuit => Trans::Quit,
        }
    }
}

fn run() -> Result<(), Error> {
    use std::time::{Duration, Instant};

    let mut machine = StateMachineRef::new(GameState::Loading);

    let mut game = Game { /*..*/ };

    machine.start(&mut game)?;
    machine.fixed_update(&mut game)?;
    
    let mut last_fixed = Instant::now();
    
    while machine.running() {
        for event in window.poll_events() {
            machine.event(&mut game, event)?;
        }        
    
        if last_fixed.elapsed().subsec_nanos() > 4_000_000 {
            machine.fixed_update(&mut game)?;
        }
    
        machine.update(&mut game)?;
    }

    Ok(())
}

fn main() {
    if let Err(e) = run() {
        eprintln!("Error occurred: {:?}", e);
    }
}

Implementing State

This chapter explains how you implement the states. For the state lifecycle see the second chapter.

machinae is designed to be as lightweight as possible. To achieve that, the States in the state machine are strongly typed (in lieu of using trait objects). That means that your state is likely to be an enum. However, for some scenarios you need many different states; that can get quite confusing if you have to match your enum in every method. Because of that, there's a second way to work with this crate: you just create multiple structs and implement DynState for each struct. Every Box<DynState> has an implementation for State automatically.

Working with references

In case your argument to the states is a reference, there are some things you need pay attention to. Chapter 1.3 describes how to do that.

Implementing State for an enum

This is probably the most straightforward way of creating states. First, you create an enum with all your states:

pub enum MyState {
    State1,
    State2,
    State3,
    StateWithData(String),
}

Next, you need to import your argument, error and event types:

use some_module::{Argument, Event, Error};

And then you can finally implement State:

use machinae::{State, Trans};

impl State<Argument, Error, Event> for MyState {}

Then, you'll have to match the enum in every method:

fn start(&mut self, arg: Argument) -> Result<Trans<Self>, Error> {
    match *self {
        State1 => Trans::None,
        State2 => Trans::None,
        State3 => Trans::None,
        StateWithData(_) => Trans::None,
    }
}

Implementing State for multiple types

This chapter describes how to work with machinae if you want to use trait objects for your states. This allows you to better organize the states using multiple types, but it's not as efficient since it uses dynamic dispatch and allocates on the heap.

You'll need to import the following machinae types, plus your argument, error and event types:

use machinae::{DynMachine, DynResult, DynState, Trans};
use some_module::{Argument, Error, Event};

Next, define the structs you want to use as states:

struct State1;

struct State2(i32);

Because we're not using a single type here, we can't use State directly, but rather implement DynState for every struct. A boxed DynState automatically implements State.


# #![allow(unused_variables)]
#fn main() {
impl DynState<Argument, Error, Event> for State1 {
    fn start(&mut self, args: Argument) -> DynResult<i32, Error, Event> {
        Ok(Trans::None)
    }

    fn update(&mut self, args: Argument) -> DynResult<i32, Error, Event> {
        Ok(Trans::Switch(Box::new(State2(42))))
    }
}

impl DynState<i32, Error, Event> for State2 {
    fn start(&mut self, args: Argument) -> DynResult<i32, Error, Event> {
        Ok(Trans::Quit)
    }
}
#}

And finally you can use the DynMachine typedef to create a new state machine:

let mut machine = DynMachine::new(Box::new(State1));

Taking a reference as parameter

If you're taking a reference as parameter, you can mostly follow chapter 1.1 or chapter 1.2, however with one little exception:

Your implementation for State needs to work with every possible lifetime 'a. So it has to look like that:

impl<'a> State<&'a Argument, Error, Event> for MyState {}

It is very important that 'a is not bound on anything. If your state has a lifetime 'b, you have to declare two lifetimes for the State implementation, otherwise a given MyState<'a> would only implement State<'a> (but it has to implement State for every possible lifetime which Rust expresses with for<'a> State<'a>).

State methods and transitions

[TODO]

Using machinae outside of game development

[TODO]

Troubleshooting

[TODO]