Menus and Other Game Stuff

Key Points:

Game States and Modes

The kind of structure above works well for arcade-like games where there's only one mode the game is generally in. In many games—to be honest, even in arcade games—this is not generally the case. A typical game might have several modes, even before considering high score entry or any game mechanics per se:

  1. Title screen
  2. Attract mode/demo
  3. Level start screen
  4. Game play
  5. Game over screen

These form a kind of state machine, where certain transitions happen under particular circumstances. Whereas in the skeleton above, our game state was very specific to the game play mode, we might prefer to model game state like a state machine, and run different update and drawing loops depending on the mode. We could achieve this with an enum member of GameState describing the mode, or we could even make GameState itself an enum with variants like TitleScreen, Demo, LevelStart, and so on, each of which might hold state describing the currently live entities. Then you can match in update and display to decide what to do based on the current state.

When things get more complicated, we might want to nest game states in more sophisticated ways. For example, in a role-playing game modes form a hierarchy:

  1. Title Screen
  2. New game/load game menu
  3. Cutscenes
  4. Map navigation
    1. Conversations/in-engine cutscenes
    2. Shops
  5. Battle
  6. Menu screen
  7. Golfing minigame
  8. Whitewater rafting minigame
  9. … and more!

Other modes might overlay these: name select screens, item select screens, dialogue windows, and so on. Game modes also give us the foundation of turn-taking and other game structures. Just like with the character controllers a couple of weeks ago, concurrent and hierarchical state machines can be a good fit for this type of problem. Sometimes we might realize these implicitly (a stack of screens, so things like item select or character menu or battle can be pushed on top of the running game screen) or explicitly as a state machine structure. Some game data will be threaded through these states (progression in the plot, character stats and world position) while others will be local to particular screens (which item is selected in the menu, stats of the enemies in the current battle). You can also devise per-character state machines instead of game-global state machines along very similar lines. Rust enumerations give us a good place to start modeling these situations. Let's take a look:

struct GameState {

}
enum Mode {
    Title,
    Play(PlayMode, GameState),
    EndGame
}
struct BattleState{ /* ... */}
struct MenuState{ /* ... */}
enum PlayMode {
    Map,
    Battle(BattleState),
    Menu(MenuState),
}

type Input = /*...*/;

impl Mode {
    // update consumes self and yields a new state (which might also just be self)
    fn update(self, input:Input) -> Self {
        match self {
            Mode::Title => {
                //...
            },
            Mode::Play(pm, ps) => {
                //...
                // Nested states return Option<Self> from update in case they terminate
                // They can affect the outer state as well
                if let (Some(next),ps) = pm.update(ps,input) {
                    Mode::Play(next,ps)
                } else {
                    Mode::EndGame
                }
            },
            // ...
        }
    }
}
impl PlayMode {
    fn update(self, state:GameState, input:Input) -> (Option<Self>,GameState) {
        // ...
    }
}
pub fn main() {
    let mut state = mode:Mode::Title;
    loop {
        // ...
        state = state.update(input);
    }
}

In the above example, Mode is a state machine enum where some states also have child states. Every tick, the enum is given a chance to update the game and itself, depending on the currently active state (or child states, if any). This requires new code for new states but is pretty extensible, provided that you don't need multiple concurrent states that aren't in a tree structure (then you'd want to have a Vec of presently active modes).

Another alternative for modeling state machines is to use traits and trait objects instead of enumeration types, which is very flexible (and each state is defined separately), but this can make it harder to tell what states are valid at which levels. It might look like this:

struct GameState {
    // ...
}
// Replace the current state with these states
type StateResult = Vec<Box<dyn State>>;
trait State {
    fn update(self, game:&mut GameState, input:Input) -> StateResult;
}
struct Title();
impl State for Title {
    //...
}
struct GamePlay();
impl State for GamePlay {
    fn update(self, game:&mut GameState, input:Input) -> StateResult {
        if game_over() {
            vec![Box::new(EndGame())]
        } else if input.key_pressed(VirtualKeyCode::M) {
            vec![Box::new(self), Box::new(Map())]
        } else {
            //...
        }
    }
}
fn main() {
    let mut state_stack:Vec<Box<dyn State>> = vec![Box::new(Title())];
    let mut game = GameState{};
    while let Some(this_state) = state_stack.pop() {
        // ... get input ...
        state_stack.extend(this_state.update(&mut game, input));
    }
}

We could also use generics and typestates if we were willing to make the game code a bit more aware of which state transitions to expect when.

Finally, we could go fully data-driven. In this example, the state machine structure is described in datatypes like Condition (which itself could be loaded from a file), and we can imagine giving individual Node types update functions which are themselves collections of Actions. This can make a lot of sense for defining character behavior or animation state machines.

struct StateMachine<Node, Condition: StateMachineCondition<Node>> {
    nodes: Vec<Node>, // For hierarchical FSMs, Node could itself be a StateMachine type or a (GameMode, Statemachine<PlayMode, Condition>) pair or something
    edges: Vec<(usize, usize, Condition)>,
    active: usize, // (or Vec<usize> if concurrent state machines are allowed)
}
trait StateMachineCondition<Node> {
    fn is_true(&self, fsm: &StateMachine<Node, Self>, game: &GameState, input:Input) -> bool
    where
        Self: Sized;
}
impl<Node, Condition: StateMachineCondition<Node>> StateMachine<Node, Condition> {
    // ...
    // What transitions are valid right now?
    pub fn available_transitions(&self, game: &GameState, input:Input) -> Vec<Transition> {
        self.edges
            .iter()
            .filter(|(n1, _n2, c)| *n1 == self.active && c.is_true(self, game))
            .map(|(_n1, n2, _c)| Transition {
                target: *n2,
                from: self.active,
            })
            .collect()
    }
    // ...
}
// ...
pub fn main() {
    // This could be loaded from a file!
    let mut fsm = StateMachine::new(
        vec![GameMode::Title, GameMode::Playing, GameMode::EndGame],
        vec![(0, 1, any_key_pressed), (1, 2, game_over)],
        0,
    );
    loop {
        // ...
        let st = fsm.current_state();
        st.update(&mut game_state, input);
        // Just take the first available transition if any
        if let Some(tr) = fsm.available_transitions(&game_state, input).first() {
            fsm.transition(tr, &mut game_state);
        }
    }
}

Mini-Lab: Game States

Make a miniature game (it could be text-based!) with multiple modes (title screen, game play, etc) that have different update and display code, where game state persists across the modes. You can start with the template above and modify it to support states. If you can, try to support nested states (e.g. within game play, there are other modes you can go into and out of like dialogues or shop screens or minigames).

Progression Structures

We often have phenomena in games that are state machine-like, but not well represented by state machines. One important example is story progression, where the state machine representation can be quite verbose or unpleasant to work with. Whether we're looking at small situations like parts of dialogue trees that should only be used once, or larger situations like a town's inhabitants being replaced by statues after a certain plot event or a linear progression of levels, we want to be able to both describe the game's progression structure, store the player's current movement along that structure, and easily query the progression state when loading game levels or beginning dialogues. There are at least three approaches you can use:

  1. Implicit progression. Whenever an event happens, change the game data to reflect the advance in progression state—give the player an item or ability permanently, change an NPC's dialogue ID to a new ID, remove a wall from a level, and so on.
  2. Explicit progression. Define and maintain a partial order (a directed acyclic graph) of game events, each of which has some data (e.g., a bool) reflecting its completion. When deciding which version of a level to load, what capabilities a character has, or what dialogue tree to use, refer to the associated plot flags and present the correct data.
  3. Scripted progression. In cases where progression follows one or more parallel arcs that mostly don't interact with each other, we can simplify (2) to just store the player's position in each of a series of lists of progression events. Conditions on advancing to the next progression point might involve progress in other aspects of the game, but hopefully these can be kept to a minimum.

Advantages of the more complex explicit or scripted progression include the ability to write tools to validate that certain progression sequences are possible and the ability to recognize sequence breaks when they happen; separating out progression from the current state of game data also means that less information has to be saved and loaded.

Other forms of progression systems include character stats and levels or skill trees, which function similarly to the plot progression examples above and tend to operate on scripted or explicit progression. Achievements, side quests, level completion, and other aspects of games can also be thought of as progression systems, often relaxing the acyclic requirement (e.g., sometimes learnable skills are arrayed on a grid and you can unlock a skill if you have unlocked an adjacent skill). Considering whether backwards progress is possible adds another wrinkle to the choice of implementation strategy.

You can imagine all kinds of graphs for representing progression systems; recent attempts at codifying how progression systems operate are the subject of current research in interactive narrative.

Timers

In case you want to run game events on a timer (for instance, that a character should become invulnerable temporarily and then vulnerable again), you have a couple of options:

  1. Use state machines and update their transition tables appropriately;
  2. Give the character a timer variable which ticks down every frame (possibly wrapped in an Option);
  3. Use an event system to enqueue an event in the future where the invulnerability wears off.

For a simple, single-entity case like this (1) or (2) are appropriate, but (3) could be used too; (3) is also maybe better for timers involving game progression (which we'll talk about in a couple of weeks) or coordinating multiple entities, and can be better-performing (we only need to update actually active timers and can even sleep until they're ready).

We can achieve an event queue with some Event types and a priority queue (maybe on top of a BinaryHeap):

pub struct EventQueue<Ev>
{
    timers: BinaryHeap<TimedEventEntry<Ev>>,
    next_id: usize,
    now: usize,
}
struct TimedEventEntry<Ev> {
    id: usize,
    time: usize, // a frame count
    event: Ev,
}
impl<Ev> EventQueue<Ev>
{
    // ...
    pub fn tick(&mut self, now: usize) -> impl Iterator<Item = Ev> {
        self.now = now;
        let mut evs = vec![];
        while self.timers.peek().map(|ev| ev.time >= now).unwrap_or(false) {
            evs.push(self.timers.pop().unwrap().event);
        }
        evs.into_iter()
    }
}
// This impl means event types don't have to implement equality checking
impl<Ev> std::cmp::PartialEq for TimedEventEntry<Ev> {
    fn eq(&self, other: &Self) -> bool {
        self.idx == other.idx && self.at == other.at
    }
}
impl<Ev> std::cmp::Eq for TimedEventEntry<Ev> {}
impl<Ev> std::cmp::Ord for TimedEventEntry<Ev> {
    // Other and self are swapped in the ordering since BinaryHeap is a max heap
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        other.time.cmp(&self.time).then(other.id.cmp(&self.id))
    }
}
impl<Ev> std::cmp::PartialOrd for TimedEventEntry<Ev> {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

Game Resources

At this point we have lots of different moving parts of different types: images, animations, audio, sprites, tiles, tilesets, tilemaps, state machine data, and so on. How can we organize this in a useful, usable way? You may have encountered this question already in working on your own 2D games.

Let's first make a distinction between three types of data:

  1. Static game properties like level layouts, entity definitions, and so on.
  2. Resources like textures and tilesets.
  3. Dynamic game state which can change from frame to frame.

If you're making a game engine, you probably want to provide types and functions for defining and storing these types of data. It might make sense to define traits for them and write engine functions which are polymorphic with respect to the specific game state type, or it might make sense to ask game authors to use a given set of data structures and map their own game-specific concepts to and from your engine's types. In fact, engines often define custom compressed file archive formats that act like "virtual filesystems" or databases to manage the thousands of resource files going into a medium- or large-scale game—since resources might be loaded at the beginning of a level or streamed in on-demand, managing the timing of everything (and graceful failure modes) is quite complex.

In any event, we rarely want the game author to be handling all of these types of data simultaneously. Part of the advantage of using an engine is trading away some complexity by opting in to the engine's management of resources, state, and properties. For this reason, you probably want the engine to give out some type of reference—maybe a Rust &reference, an Rc<Something>, or an opaque struct like TileID or SoundHandle—to resources and properties, and possibly even wrap the state completely and provide a query interface ("give me the colliders of the entities within 16 units of this one", "iterate through every entity tagged with enemy"). In this course, you get to decide where the engine-game boundary falls.

You might try to keep the game in charge of defining and owning the game state and properties and the engine in charge of resources. The engine can define types like colliders and entities, but the game code could hold ownership over them and pass them into the engine's functions on an as-needed basis. This way, you can constrain the job of the engine to just processing data and ensuring the right resources are loaded at the right time, and in particular the engine doesn't need to have its own representations of complex game worlds—just knowledge of how to arbitrate collisions, render tilemaps, and so on.

Alternatively, you could let the engine be in charge of game state, properties, and resources! Then the game code just needs to set up the engine the right way and let it run along on its own.