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 Trans
itions returned by the current state.
Book structure
This book is split into five chapters, this being the introduction. After this chapter:
- Implementing State
- State methods and transitions
- Using machinae outside of game development
- Troubleshooting
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 State
s 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]