Game State Management with Key Events

Hello all,

I’m working on a Roguelike dungeon crawler and I am learning new game development/coding techniques along the way. I am having trouble creating a finite state machine that switches between states with user input as seen in the diagram below:

The issue I’m having is with using the same key for entering and leaving a state. (Eg. Normal Gameplay --> Full Map View --> Normal Gameplay). The method I am currently using just rapidly switches between states while the key is pressed.

I’m really struggling to come up with a logical way of keeping track of the user input for this.

I’m currently using a booean[256] array inside keyPressed() and keyReleased() to set each character either pressed (true) or not pressed (false).

Here is the relevant code:


// start, saveExit, death, win;
// 1, 2, 3, 4

int MAXKEYS = 256;
boolean[] keys = new boolean[MAXKEYS];

GameStates states;
void setup() {
  states = GameStates.TitleScreen;
  println(states.toString());
}

String prevString;
void draw() {
  prevString = states.toString();
  states = states.nextState(keys, keys['1'], keys['2'], keys['3'], keys['4']);
  if (!prevString.equals(states.toString())) {
    println(states.toString());
  }
}

void keyPressed() {
  updateKeys(true);
  if (key == ESC) {
    key = 0;
  }
}

void keyReleased() {
  updateKeys(false);
  if (key == ESC) {
    key = 0;
  }
}

void updateKeys(boolean b) {
  if (key < MAXKEYS && keys[key] != b) {
    keys[key] = b;
  }
}

public enum GameStates {

  TitleScreen {
    @Override
      GameStates nextState(boolean[] input, boolean start, boolean saveExit, boolean death, boolean win) {
      if (start) {
        return NormalGameplay;
      } else {
        return TitleScreen;
      }
    }
  }
  , 
    NormalGameplay {
    @Override
      GameStates nextState(boolean[] input, boolean start, boolean saveExit, boolean death, boolean win) {
      if (death) {
        return DeathScreen;
      } else if (win) {
        return EndGame;
      } else if (input[ESC]) {
        return MenuScreen;
      } else if (input['m']) {
        return FullMapView;
      } else if (input['c']) {
        return CharacterInformationView;
      } else {
        return NormalGameplay;
      }
    }
  }
  , 
    MenuScreen {
    @Override
      GameStates nextState(boolean[] input, boolean start, boolean saveExit, boolean death, boolean win) {
      if (saveExit) {
        return TitleScreen;
      } else if (input[ESC]) {
        return NormalGameplay;
      } else {
        return MenuScreen;
      }
    }
  }
  , 
    DeathScreen {
    @Override
      GameStates nextState(boolean[] input, boolean start, boolean saveExit, boolean death, boolean win) {
      int todo; // stats
      return TitleScreen;
    }
  }
  , 
    FullMapView {
    @Override
      GameStates nextState(boolean[] input, boolean start, boolean saveExit, boolean death, boolean win) {
      if (input['m']) {
        return NormalGameplay;
      } else if (input['c']) {
        return CharacterInformationView;
      } else {
        return FullMapView;
      }
    }
  }
  , 
    CharacterInformationView {
    @Override
      GameStates nextState(boolean[] input, boolean start, boolean saveExit, boolean death, boolean win) {
      if (input['c']) {
        return NormalGameplay;
      } else if (input['m']) {
        return FullMapView;
      } else {
        return CharacterInformationView;
      }
    }
  }
  , 
    EndGame {
    @Override
      GameStates nextState(boolean[] input, boolean start, boolean saveExit, boolean death, boolean win) {
      int todo; // credits
      return TitleScreen;
    }
  }; 

  abstract GameStates nextState(boolean[] input, boolean start, boolean saveExit, boolean death, boolean win);
}
2 Likes

For anyone with the same issue in the future, this post is how I solved it. I’m not 100% happy with how I had to do it, but at least it’s working the way I originally intended it to now!

I’m now keeping track of both keyPressed() and keyReleased() with the keys[][] boolean array. The first column keeps track of keyPressed() and the second column keeps track of keyReleased().

I modified the game state switching so that it runs off of keyReleased() events rather than keyPressed() events as keyReleased only occurs once on most operating systems.


// start, saveExit, death, win;
// 1, 2, 3, 4

int MAXKEYS = 256;
boolean[][] keys = new boolean[MAXKEYS][2];

GameStates states;
void setup() {
  states = GameStates.TitleScreen;
  println(states.toString());
}

String prevString;
void draw() {
  prevString = states.toString();
  states = states.nextState(keys, keys['1'][0], keys['2'][0], keys['3'][0], keys['4'][0]);
  if (!prevString.equals(states.toString())) {
    println(states.toString());
  }
}


void keyPressed() {
  updateKeys(key, true, false);
  if (key == ESC) {
    key = 0;
  }
}

void keyReleased() {
  updateKeys(key, false, true);
  if (key == ESC) {
    key = 0;
  }
}

// keys[pressed][first press]
void updateKeys(int k, boolean p, boolean r) {
  if (k < MAXKEYS) {
    keys[k][0] = p;
    keys[k][1] = r;
  }

  // arrow keys etc...
}

// keep track of pressed and released as boolean variables
// reset released boolean after variable is used.


public enum GameStates {

  TitleScreen {
    @Override
      GameStates nextState(boolean[][] input, boolean start, boolean saveExit, boolean death, boolean win) {
      if (start) {
        return NormalGameplay;
      } else {
        return TitleScreen;
      }
    }
  }
  , 
    NormalGameplay {
    @Override
      GameStates nextState(boolean[][] input, boolean start, boolean saveExit, boolean death, boolean win) {
      if (death) {
        return DeathScreen;
      } else if (win) {
        return EndGame;
      } else if (input[ESC][1]) {
        input[ESC][1] = false;
        return MenuScreen;
      } else if (input['m'][1]) {
        input['m'][1] = false;
        return FullMapView;
      } else if (input['c'][1]) {
        input['c'][1] = false;
        return CharacterInformationView;
      } else {
        return NormalGameplay;
      }
    }
  }
  , 
    MenuScreen {
    @Override
      GameStates nextState(boolean[][] input, boolean start, boolean saveExit, boolean death, boolean win) {
      if (saveExit) {
        return TitleScreen;
      } else if (input[ESC][1]) {
        input[ESC][1] = false;
        return NormalGameplay;
      } else {
        return MenuScreen;
      }
    }
  }
  , 
    DeathScreen {
    @Override
      GameStates nextState(boolean[][] input, boolean start, boolean saveExit, boolean death, boolean win) {
      int todo; // stats
      return TitleScreen;
    }
  }
  , 
    FullMapView {
    @Override
      GameStates nextState(boolean[][] input, boolean start, boolean saveExit, boolean death, boolean win) {
      if (input['m'][1]) {
        input['m'][1] = false;
        return NormalGameplay;
      } else if (input['c'][1]) {
        input['c'][1] = false;
        return CharacterInformationView;
      } else {
        return FullMapView;
      }
    }
  }
  , 
    CharacterInformationView {
    @Override
      GameStates nextState(boolean[][] input, boolean start, boolean saveExit, boolean death, boolean win) {
      if (input['c'][1]) {
        input['c'][1] = false;
        return NormalGameplay;
      } else if (input['m'][1]) {
        input['m'][1] = false;
        return FullMapView;
      } else {
        return CharacterInformationView;
      }
    }
  }
  , 
    EndGame {
    @Override
      GameStates nextState(boolean[][] input, boolean start, boolean saveExit, boolean death, boolean win) {
      int todo; // credits
      return TitleScreen;
    }
  }; 

  abstract GameStates nextState(boolean[][] input, boolean start, boolean saveExit, boolean death, boolean win);
}
2 Likes

Thank you for sharing this approach!

You might also be interested in some simple state machine Java libraries. They all use the builder pattern.

1 Like