A finite-state machine implementation with the following features:
- Supports definition of state machines in declarative style using the builder pattern
- Supports onEntry and onExit actions for states
- Supports registration of state entry and state exit listeners
- Supports onTick actions for states which are triggered by a clock
- States can be implemented as subclasses of the generic State class
- States can have a timer and trigger a transition on timeout
- State transitions can be triggered by combinations of
- conditions (guards)
- event conditions (match transitions by event value/equality or by event class)
- state timeout
- Supports transition actions with information about the event that triggered the transition
- Actions can be implemented by lambda expression or function references
- Tracing of state machine processing to some logger
- Transitions can be annotated (used by export to Graphviz format)
- State graph can be exported into Graphviz ("dot") format
- Drawbacks: No hierarchical states supported
The states are identified by some arbitrary type, normally an enumeration type, string or integer.
A somewhat trivial example is a traffic light controlled using timers.
public class TrafficLight extends StateMachine<Light, Void> {
public enum Light {
OFF, RED, YELLOW, GREEN;
}
public TrafficLight() {
//@formatter:off
super(Light.class);
beginStateMachine()
.description("Traffic Light")
.initialState(OFF)
.states()
.state(OFF)
.state(RED).timeoutAfter(() -> app().clock().sec(3))
.state(YELLOW).timeoutAfter(() -> app().clock().sec(2))
.state(GREEN).timeoutAfter(() -> app().clock().sec(5))
.transitions()
.when(OFF).then(RED).condition(() -> Keyboard.keyPressedOnce(KeyEvent.VK_SPACE))
.when(RED).then(GREEN).onTimeout()
.when(GREEN).then(YELLOW).onTimeout()
.when(YELLOW).then(RED).onTimeout()
.endStateMachine();
//@formatter:off
}
}
In my game library, each application has a lifecycle implemented as a finite-state machine.
class ApplicationLifecycle extends StateMachine<ApplicationState, ApplicationEvent> {
enum ApplicationState {
INITIALIZING, CREATING_UI, RUNNING, PAUSED, CLOSING;
}
enum ApplicationEvent {
PAUSE, RESUME, ENTER_FULLSCREEN_MODE, ENTER_WINDOW_MODE, SHOW_SETTINGS_DIALOG, CLOSE
}
ApplicationLifecycle(Application app, String[] cmdLine) {
super(ApplicationState.class, TransitionMatchStrategy.BY_VALUE);
/*@formatter:off*/
beginStateMachine()
.description(String.format("[%s]", app.getName()))
.initialState(INITIALIZING)
.states()
.state(INITIALIZING)
.onEntry(() -> {
loginfo("Configuring application '%s'", app.getName());
app.configure(app.settings);
app.processCommandLine(cmdLine);
app.printSettings();
app.init();
if (app.settings.muted) {
app.soundManager.muteAll();
}
setState(CREATING_UI);
})
.state(CREATING_UI)
.onEntry(() -> {
SwingUtilities.invokeLater(() -> {
app.createUserInterface();
app.clock.start();
loginfo("Application is running, %d frames/second", app.clock.getTargetFramerate());
});
})
.state(RUNNING)
.onTick(() -> {
app.readInput();
app.controller.update();
app.renderCurrentView();
})
.state(PAUSED)
.onTick(app::renderCurrentView)
.state(CLOSING)
.onEntry(() -> {
loginfo("Closing application '%s'", app.getName());
})
.onTick(() -> {
app.shell.dispose();
// cannot exit in onEntry because CLOSING listeners would not get executed!
System.exit(0);
})
.transitions()
.when(CREATING_UI).then(RUNNING).condition(() -> app.clock.isTicking())
.when(RUNNING).then(PAUSED).on(PAUSE).act(() -> app.soundManager.muteAll())
.when(RUNNING).then(CLOSING).on(CLOSE)
.stay(RUNNING).on(ENTER_FULLSCREEN_MODE).act(() -> app.shell.showFullScreenWindow())
.stay(RUNNING).on(ENTER_WINDOW_MODE).act(() -> app.shell.showWindow())
.stay(RUNNING).on(SHOW_SETTINGS_DIALOG).act(() -> app.shell.showF2Dialog())
.when(PAUSED).then(RUNNING).on(RESUME).act(() -> app.soundManager.unmuteAll())
.when(PAUSED).then(CLOSING).on(CLOSE)
.stay(PAUSED).on(ENTER_FULLSCREEN_MODE).act(() -> app.shell.showFullScreenWindow())
.stay(PAUSED).on(ENTER_WINDOW_MODE).act(() -> app.shell.showWindow())
.stay(PAUSED).on(SHOW_SETTINGS_DIALOG).act(() -> app.shell.showF2Dialog())
.endStateMachine();
/*@formatter:on*/
}
}
Example 3: Menu and controller for Pong game
beginStateMachine()
.description("Pong Menu")
.initialState(Player1_Player2)
.states()
// for clarity, all states are listed, would also work without!
.state(Player1_Player2)
.state(Player1_Computer)
.state(Computer_Player2)
.state(Computer_Computer)
.transitions()
.when(Player1_Player2) .then(Player1_Computer) .condition(this::nextEntrySelected)
.when(Player1_Computer) .then(Computer_Player2) .condition(this::nextEntrySelected)
.when(Computer_Player2) .then(Computer_Computer).condition(this::nextEntrySelected)
.when(Computer_Computer).then(Player1_Player2) .condition(this::nextEntrySelected)
.when(Player1_Player2) .then(Computer_Computer).condition(this::prevEntrySelected)
.when(Computer_Computer).then(Computer_Player2) .condition(this::prevEntrySelected)
.when(Computer_Player2) .then(Player1_Computer) .condition(this::prevEntrySelected)
.when(Player1_Computer) .then(Player1_Player2) .condition(this::prevEntrySelected)
.endStateMachine();
public enum PlayState {
INIT, PLAYING, SERVING, GAME_OVER;
}
beginStateMachine()
.description("Pong")
.initialState(INIT)
.states()
.state(INIT).onEntry(this::initEntities)
.state(SERVING).timeoutAfter(app().clock.sec(2)).onEntry(this::prepareService)
.state(PLAYING).onTick(this::updateEntities)
.state(GAME_OVER)
.transitions()
.when(INIT).then(SERVING).act(this::resetScores)
.when(SERVING).then(PLAYING).onTimeout().act(this::serveBall)
.stay(PLAYING).condition(this::leftPaddleHitsBall).act(this::returnBallWithLeftPaddle)
.stay(PLAYING).condition(this::rightPaddleHitsBall).act(this::returnBallWithRightPaddle)
.when(PLAYING).then(SERVING).condition(this::isBallOutLeft).act(this::assignPointToRightPlayer)
.when(PLAYING).then(SERVING).condition(this::isBallOutRight).act(this::assignPointToLeftPlayer)
.when(PLAYING).then(GAME_OVER).condition(() -> leftPlayerWins() || rightPlayerWins())
.when(GAME_OVER).then(INIT).condition(() -> Keyboard.keyPressedOnce(KeyEvent.VK_SPACE))
.endStateMachine();
I used this state machine library extensively in my Pac-Man game implementation, which in fact was the main motivation for creating this library at all. In that implementation, state machines are used all over the place: for the central game control, for Pac-Man and the ghosts, for their movement, for controlling the animations in the intro view and more.
This is the state machine controlling the behavior of the ghosts:
ai.beginStateMachine()
.description(name + " AI")
.initialState(LOCKED)
.states()
.state(LOCKED)
.onEntry(() -> {
visible = true;
recovering = false;
bounty = 0;
nextState = LOCKED;
placeIntoBed();
})
.onTick(this::move)
.state(LEAVING_HOUSE)
.onTick(this::move)
.onExit(() -> forceMoving(Direction.LEFT))
.state(ENTERING_HOUSE)
.onTick(this::move)
.state(SCATTERING)
.onTick(() -> {
updateMentalHealth();
checkPacManCollision(pacMan);
move();
})
.state(CHASING)
.onTick(() -> {
updateMentalHealth();
checkPacManCollision(pacMan);
move();
})
.state(FRIGHTENED)
.timeoutAfter(this::getFrightenedTicks)
.onTick((state, consumed, remaining) -> {
updateMentalHealth();
checkPacManCollision(pacMan);
move();
recovering = remaining < getFlashTimeTicks();
})
.state(DEAD)
.timeoutAfter(sec(1))
.onEntry(this::computeBounty)
.onTick((s, consumed, remaining) -> {
if (remaining == 0) {
bounty = 0;
move();
}
})
.transitions()
.when(LOCKED).then(LEAVING_HOUSE).on(GhostUnlockedEvent.class)
.when(LEAVING_HOUSE).then(SCATTERING)
.condition(() -> justLeftHouse() && nextState == SCATTERING)
.annotation("Outside house")
.when(LEAVING_HOUSE).then(CHASING)
.condition(() -> justLeftHouse() && nextState == CHASING)
.annotation("Outside house")
.when(LEAVING_HOUSE).then(FRIGHTENED)
.condition(() -> justLeftHouse() && nextState == FRIGHTENED)
.annotation("Outside house")
.when(ENTERING_HOUSE).then(LEAVING_HOUSE)
.condition(() -> getSteering().isComplete())
.annotation("Reached bed")
.when(CHASING).then(FRIGHTENED)
.on(PacManGainsPowerEvent.class)
.act(this::reverseDirection)
.when(CHASING).then(DEAD)
.on(GhostKilledEvent.class)
.when(CHASING).then(SCATTERING)
.condition(() -> nextState == SCATTERING)
.act(this::reverseDirection)
.annotation("Got scattering command")
.when(SCATTERING).then(FRIGHTENED)
.on(PacManGainsPowerEvent.class)
.act(this::reverseDirection)
.when(SCATTERING).then(DEAD)
.on(GhostKilledEvent.class)
.when(SCATTERING).then(CHASING)
.condition(() -> nextState == CHASING)
.act(this::reverseDirection)
.annotation("Got chasing command")
.stay(FRIGHTENED).on(PacManGainsPowerEvent.class)
.act(() -> ai.resetTimer(FRIGHTENED))
.when(FRIGHTENED).then(DEAD)
.on(GhostKilledEvent.class)
.when(FRIGHTENED).then(SCATTERING)
.onTimeout()
.condition(() -> nextState == SCATTERING)
.when(FRIGHTENED).then(CHASING)
.onTimeout()
.condition(() -> nextState == CHASING)
.when(DEAD).then(ENTERING_HOUSE)
.condition(this::isAtHouseEntry)
.annotation("Reached house entry")
.endStateMachine();
/*@formatter:on*/
ai.setMissingTransitionBehavior(MissingTransitionBehavior.LOG);