Discussing State Machine Implementations for Games in Rust

State machines are a common concept in game programming, often manifested at a very high level (Main Menu - Level - Ingame Menu - Cut Scene), or a very low level on a per-object basis
(patrolling - waiting - charging - animating). In the following repository, I have discussed five different implementations.

Personally, I also have a clear preference for two of them, depending on the use case (per object basis: old_fashioned, high level: with_enum_map_trait_objects).

What would be your preference, or would you suggest an entirely different approach?

https://github.com/Carbonfreezer/state_machine_games/tree/main

1 Like

Have you compared their respective implementations with Compiler Explorer?

The old-fashioned method the most readable for me, assuming the complexity of what’s done in each state remains about the same as the example, and I have the impression it’s also the one that’ll give the simplest and quickest code. The high-level one with dynamic dispatching is unnecessarily complicated, and I’m not sure to see what justifies the trait or the dynamic dispatching (but I had only a quick glance).

Hi Redglyph,

Thanks for the hint with the Compiler Explorer. Unfortunately, it does not work for external crates. As for readability, you are definitely correct for that sample size. Here, I would go old-fashioned all the way. The other implementations become more interesting if you have

a) more states than two
b) more state-specific data
c) more state logic per state

In this case, the update routine might become lengthy with many match arms, and the object may become bloated with data. Somewhere along the road of complexity is a break-even point.

Take care,
Christoph

Ah, sorry, I didn’t check for those specific crates. I thought most of them were available, by now. Anyway, perhaps it doesn’t matter as much if you have something more complex.

If there’s more code, I’d just put it into methods, but I don’t see exactly how much complex you mean, nor the number of states or whether they’re all related to the same object (I suppose not, unless it’s for sub-parts of an actor).

If you often have specific code when you’re entering or exiting a state, it makes sense to have a specific mechanism for that, maybe, rather than checking with a condition in each state’s code. The most important is to be comfortable with the clarity of the code and how easy it can be maintained/updated. :slight_smile:

One consideration for a game is it's pretty likely you would like to be dynamically scripted at some point, which is pretty obviously a completely different proposition.

Otherwise I would generally recommend avoiding trying to fancy up the logic here too much: enums directly map to states, and match directly map to handling states, you should at least start with the dumbest possible option:

struct Game {
  state: State, // e.g. current
  paused_level: Option<LevelState>,
  ...
}

enum State {
  Menu,
  Level(LevelState),
  ...
}

impl Game {
  fn set_state(&mut self, next: State) {
    // exit ...
    match std::mem::replace(self.state, next) {
      Self::Level(state) => self.paused_level = Some(state);
      ...
    }
    // enter...
    match self.state {
       ...
    }
  }

  fn update(&mut self) {
    match self.state {
      ..
    }
  }
}

Of course you can break things out to methods etc. (though beware the borrow checker!), but the basic idea is having one big "state" solves a lot of issues you borrow with fancier approaches: prefer inherent methods to traits, prefer match to dyn, etc.

Of course this doesn't scale as nicely to 100s of different object types, so at that point you'll want to think about what you actually want to do there; you're probably thinking about ECS / data oriented designs vs dyn at that point, but having done some interpreters with 100s of match arms for each op code it's not actually all that bad, especially with macros to stamp out common patterns.

1 Like

Hi all,

Thanks for the input. Very good points. Starting as simply as possible and eventually scaling up the infrastructure later in the required direction as the project unfolds is a very solid approach.

Christoph