Finite State Machines (FSMs) are fundamental to game development for managing character behavior, game states, and complex workflows. This comprehensive guide covers FSM patterns, implementations, and advanced techniques for Java games.
FSM Architecture Overview
FSM Core Concepts: ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ States │ │ Transitions │ │ Actions │ │ • Idle │ -> │ • Conditions │ -> │ • On Enter │ │ • Moving │ │ • Triggers │ │ • On Exit │ │ • Attacking │ │ • Guards │ │ • On Update │ └─────────────────┘ └──────────────────┘ └─────────────────┘
Basic FSM Implementation
1. Core FSM Interface
package com.game.fsm;
public interface State {
void enter();
void update(float deltaTime);
void exit();
String getName();
}
public interface StateMachine {
void addState(State state);
void setInitialState(String stateName);
void changeState(String stateName);
State getCurrentState();
void update(float deltaTime);
}
2. Generic State Machine Implementation
package com.game.fsm;
import java.util.HashMap;
import java.util.Map;
public class GenericStateMachine implements StateMachine {
private final Map<String, State> states;
private State currentState;
private State previousState;
public GenericStateMachine() {
this.states = new HashMap<>();
this.currentState = null;
this.previousState = null;
}
@Override
public void addState(State state) {
states.put(state.getName(), state);
}
@Override
public void setInitialState(String stateName) {
State initialState = states.get(stateName);
if (initialState != null) {
currentState = initialState;
currentState.enter();
} else {
throw new IllegalArgumentException("Initial state not found: " + stateName);
}
}
@Override
public void changeState(String stateName) {
State nextState = states.get(stateName);
if (nextState == null) {
throw new IllegalArgumentException("State not found: " + stateName);
}
if (currentState != null) {
currentState.exit();
previousState = currentState;
}
currentState = nextState;
currentState.enter();
System.out.println("State changed: " +
(previousState != null ? previousState.getName() : "null") +
" -> " + currentState.getName());
}
@Override
public State getCurrentState() {
return currentState;
}
@Override
public void update(float deltaTime) {
if (currentState != null) {
currentState.update(deltaTime);
}
}
public void revertToPreviousState() {
if (previousState != null) {
changeState(previousState.getName());
}
}
public boolean isInState(String stateName) {
return currentState != null && currentState.getName().equals(stateName);
}
public Map<String, State> getStates() {
return new HashMap<>(states);
}
}
Game Character State Management
1. Enemy AI State Machine
package com.game.fsm.enemy;
import com.game.fsm.State;
import com.game.fsm.GenericStateMachine;
public class EnemyAI {
private final GenericStateMachine stateMachine;
private final Enemy enemy;
private float detectionRange = 10.0f;
private float attackRange = 2.0f;
public EnemyAI(Enemy enemy) {
this.enemy = enemy;
this.stateMachine = new GenericStateMachine();
initializeStates();
}
private void initializeStates() {
// Add all enemy states
stateMachine.addState(new IdleState());
stateMachine.addState(new PatrolState());
stateMachine.addState(new ChaseState());
stateMachine.addState(new AttackState());
stateMachine.addState(new FleeState());
stateMachine.addState(new DeadState());
// Set initial state
stateMachine.setInitialState("IDLE");
}
public void update(float deltaTime) {
stateMachine.update(deltaTime);
}
// Enemy States
private class IdleState implements State {
private float idleTime = 0;
private final float maxIdleTime = 3.0f;
@Override
public void enter() {
idleTime = 0;
enemy.setAnimation("idle");
System.out.println(enemy.getName() + " is now idle");
}
@Override
public void update(float deltaTime) {
idleTime += deltaTime;
// Check for player detection
if (enemy.canSeePlayer(detectionRange)) {
stateMachine.changeState("CHASE");
return;
}
// Transition to patrol after idle time
if (idleTime >= maxIdleTime) {
stateMachine.changeState("PATROL");
}
}
@Override
public void exit() {
idleTime = 0;
}
@Override
public String getName() {
return "IDLE";
}
}
private class PatrolState implements State {
private float patrolTimer = 0;
private final float patrolPointDuration = 5.0f;
@Override
public void enter() {
patrolTimer = 0;
enemy.setAnimation("walk");
enemy.moveToNextPatrolPoint();
System.out.println(enemy.getName() + " started patrolling");
}
@Override
public void update(float deltaTime) {
patrolTimer += deltaTime;
// Check for player detection
if (enemy.canSeePlayer(detectionRange)) {
stateMachine.changeState("CHASE");
return;
}
// Move to next patrol point
if (patrolTimer >= patrolPointDuration || enemy.hasReachedDestination()) {
enemy.moveToNextPatrolPoint();
patrolTimer = 0;
}
enemy.updateMovement(deltaTime);
}
@Override
public void exit() {
patrolTimer = 0;
}
@Override
public String getName() {
return "PATROL";
}
}
private class ChaseState implements State {
@Override
public void enter() {
enemy.setAnimation("run");
System.out.println(enemy.getName() + " is chasing player");
}
@Override
public void update(float deltaTime) {
if (!enemy.canSeePlayer(detectionRange)) {
stateMachine.changeState("IDLE");
return;
}
if (enemy.getDistanceToPlayer() <= attackRange) {
stateMachine.changeState("ATTACK");
return;
}
if (enemy.getHealthPercentage() < 0.3f) {
stateMachine.changeState("FLEE");
return;
}
enemy.chasePlayer(deltaTime);
}
@Override
public void exit() {
enemy.stopMovement();
}
@Override
public String getName() {
return "CHASE";
}
}
private class AttackState implements State {
private float attackCooldown = 0;
private final float attackInterval = 1.5f;
@Override
public void enter() {
attackCooldown = 0;
enemy.setAnimation("attack");
System.out.println(enemy.getName() + " is attacking");
}
@Override
public void update(float deltaTime) {
attackCooldown += deltaTime;
if (!enemy.canSeePlayer(attackRange)) {
stateMachine.changeState("CHASE");
return;
}
if (enemy.getHealthPercentage() < 0.2f) {
stateMachine.changeState("FLEE");
return;
}
if (attackCooldown >= attackInterval) {
enemy.performAttack();
attackCooldown = 0;
}
enemy.facePlayer();
}
@Override
public void exit() {
attackCooldown = 0;
}
@Override
public String getName() {
return "ATTACK";
}
}
private class FleeState implements State {
private float fleeTime = 0;
private final float maxFleeTime = 8.0f;
@Override
public void enter() {
fleeTime = 0;
enemy.setAnimation("run");
enemy.startFleeing();
System.out.println(enemy.getName() + " is fleeing");
}
@Override
public void update(float deltaTime) {
fleeTime += deltaTime;
if (fleeTime >= maxFleeTime || !enemy.canSeePlayer(detectionRange * 1.5f)) {
stateMachine.changeState("IDLE");
return;
}
if (enemy.getHealthPercentage() < 0.1f) {
stateMachine.changeState("DEAD");
return;
}
enemy.updateFleeMovement(deltaTime);
}
@Override
public void exit() {
fleeTime = 0;
enemy.stopMovement();
}
@Override
public String getName() {
return "FLEE";
}
}
private class DeadState implements State {
@Override
public void enter() {
enemy.setAnimation("dead");
enemy.onDeath();
System.out.println(enemy.getName() + " has died");
}
@Override
public void update(float deltaTime) {
// No updates in dead state
}
@Override
public void exit() {
// Cannot exit dead state
}
@Override
public String getName() {
return "DEAD";
}
}
// Getters
public GenericStateMachine getStateMachine() {
return stateMachine;
}
public String getCurrentStateName() {
return stateMachine.getCurrentState().getName();
}
}
2. Enemy Entity Class
package com.game.fsm.enemy;
public class Enemy {
private String name;
private float health;
private float maxHealth;
private Vector2 position;
private Vector2[] patrolPoints;
private int currentPatrolIndex;
private Player target;
public Enemy(String name, float maxHealth, Vector2 startPosition) {
this.name = name;
this.maxHealth = maxHealth;
this.health = maxHealth;
this.position = startPosition;
this.patrolPoints = new Vector2[0];
this.currentPatrolIndex = 0;
}
public boolean canSeePlayer(float range) {
if (target == null) return false;
return position.distanceTo(target.getPosition()) <= range;
}
public float getDistanceToPlayer() {
return target != null ? position.distanceTo(target.getPosition()) : Float.MAX_VALUE;
}
public float getHealthPercentage() {
return health / maxHealth;
}
public void setAnimation(String animation) {
// Implementation for animation system
System.out.println(name + " animation: " + animation);
}
public void moveToNextPatrolPoint() {
if (patrolPoints.length == 0) return;
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.length;
// Set movement target to patrolPoints[currentPatrolIndex]
}
public boolean hasReachedDestination() {
// Implementation for movement system
return true; // Simplified
}
public void updateMovement(float deltaTime) {
// Implementation for movement system
}
public void chasePlayer(float deltaTime) {
if (target != null) {
// Move toward player
// position.moveTowards(target.getPosition(), speed * deltaTime);
}
}
public void stopMovement() {
// Stop any ongoing movement
}
public void performAttack() {
// Attack logic
System.out.println(name + " attacks!");
if (target != null) {
// target.takeDamage(attackDamage);
}
}
public void facePlayer() {
if (target != null) {
// Rotate to face player
}
}
public void startFleeing() {
// Set flee destination away from player
}
public void updateFleeMovement(float deltaTime) {
// Move away from player
}
public void onDeath() {
// Death logic - drop loot, play death animation, etc.
}
public void takeDamage(float damage) {
health = Math.max(0, health - damage);
if (health <= 0) {
// State machine will transition to DEAD state
}
}
// Getters and setters
public String getName() { return name; }
public Vector2 getPosition() { return position; }
public void setTarget(Player target) { this.target = target; }
public void setPatrolPoints(Vector2[] points) { this.patrolPoints = points; }
}
Advanced FSM with Transition System
1. Transition-Based State Machine
package com.game.fsm.advanced;
import java.util.*;
public class TransitionStateMachine implements StateMachine {
private final Map<String, State> states;
private final Map<String, List<Transition>> transitions;
private State currentState;
private final Object context;
public TransitionStateMachine(Object context) {
this.states = new HashMap<>();
this.transitions = new HashMap<>();
this.currentState = null;
this.context = context;
}
public void addState(State state) {
states.put(state.getName(), state);
transitions.put(state.getName(), new ArrayList<>());
}
public void addTransition(String fromState, String toState, TransitionCondition condition) {
List<Transition> stateTransitions = transitions.get(fromState);
if (stateTransitions != null) {
stateTransitions.add(new Transition(fromState, toState, condition));
}
}
@Override
public void setInitialState(String stateName) {
State initialState = states.get(stateName);
if (initialState != null) {
currentState = initialState;
currentState.enter();
}
}
@Override
public void changeState(String stateName) {
State nextState = states.get(stateName);
if (nextState == null) return;
if (currentState != null) {
currentState.exit();
}
currentState = nextState;
currentState.enter();
}
@Override
public void update(float deltaTime) {
if (currentState == null) return;
// Check for transitions
checkTransitions();
// Update current state
currentState.update(deltaTime);
}
private void checkTransitions() {
List<Transition> stateTransitions = transitions.get(currentState.getName());
if (stateTransitions == null) return;
for (Transition transition : stateTransitions) {
if (transition.condition.isMet(context)) {
changeState(transition.toState);
break; // Only take first valid transition
}
}
}
@Override
public State getCurrentState() {
return currentState;
}
// Transition classes
public static class Transition {
public final String fromState;
public final String toState;
public final TransitionCondition condition;
public Transition(String fromState, String toState, TransitionCondition condition) {
this.fromState = fromState;
this.toState = toState;
this.condition = condition;
}
}
public interface TransitionCondition {
boolean isMet(Object context);
}
}
2. Conditional Transitions
package com.game.fsm.advanced;
public class EnemyTransitionConditions {
public static class PlayerInRangeCondition implements TransitionStateMachine.TransitionCondition {
private final float range;
public PlayerInRangeCondition(float range) {
this.range = range;
}
@Override
public boolean isMet(Object context) {
if (context instanceof Enemy) {
Enemy enemy = (Enemy) context;
return enemy.canSeePlayer(range);
}
return false;
}
}
public static class PlayerOutOfRangeCondition implements TransitionStateMachine.TransitionCondition {
private final float range;
public PlayerOutOfRangeCondition(float range) {
this.range = range;
}
@Override
public boolean isMet(Object context) {
if (context instanceof Enemy) {
Enemy enemy = (Enemy) context;
return !enemy.canSeePlayer(range);
}
return false;
}
}
public static class LowHealthCondition implements TransitionStateMachine.TransitionCondition {
private final float threshold;
public LowHealthCondition(float threshold) {
this.threshold = threshold;
}
@Override
public boolean isMet(Object context) {
if (context instanceof Enemy) {
Enemy enemy = (Enemy) context;
return enemy.getHealthPercentage() < threshold;
}
return false;
}
}
public static class TimerCondition implements TransitionStateMachine.TransitionCondition {
private final float duration;
private float elapsedTime;
public TimerCondition(float duration) {
this.duration = duration;
this.elapsedTime = 0;
}
@Override
public boolean isMet(Object context) {
// This would need to be updated each frame
// Implementation would track time in the state machine update
return elapsedTime >= duration;
}
public void update(float deltaTime) {
elapsedTime += deltaTime;
}
public void reset() {
elapsedTime = 0;
}
}
public static class AndCondition implements TransitionStateMachine.TransitionCondition {
private final TransitionStateMachine.TransitionCondition[] conditions;
public AndCondition(TransitionStateMachine.TransitionCondition... conditions) {
this.conditions = conditions;
}
@Override
public boolean isMet(Object context) {
for (TransitionStateMachine.TransitionCondition condition : conditions) {
if (!condition.isMet(context)) {
return false;
}
}
return true;
}
}
public static class OrCondition implements TransitionStateMachine.TransitionCondition {
private final TransitionStateMachine.TransitionCondition[] conditions;
public OrCondition(TransitionStateMachine.TransitionCondition... conditions) {
this.conditions = conditions;
}
@Override
public boolean isMet(Object context) {
for (TransitionStateMachine.TransitionCondition condition : conditions) {
if (condition.isMet(context)) {
return true;
}
}
return false;
}
}
}
Game State Management
1. Game-Wide State Machine
package com.game.fsm.gamestates;
import com.game.fsm.State;
import com.game.fsm.GenericStateMachine;
public class GameStateManager {
private final GenericStateMachine stateMachine;
public GameStateManager() {
this.stateMachine = new GenericStateMachine();
initializeGameStates();
}
private void initializeGameStates() {
stateMachine.addState(new MainMenuState());
stateMachine.addState(new LoadingState());
stateMachine.addState(new PlayingState());
stateMachine.addState(new PausedState());
stateMachine.addState(new GameOverState());
stateMachine.addState(new VictoryState());
stateMachine.setInitialState("MAIN_MENU");
}
public void update(float deltaTime) {
stateMachine.update(deltaTime);
}
public void render() {
// Delegate rendering to current state
GameState currentState = (GameState) stateMachine.getCurrentState();
currentState.render();
}
public void handleInput(int keyCode, boolean pressed) {
GameState currentState = (GameState) stateMachine.getCurrentState();
currentState.handleInput(keyCode, pressed);
}
// Game States
public abstract class GameState implements State {
public abstract void render();
public abstract void handleInput(int keyCode, boolean pressed);
}
private class MainMenuState extends GameState {
@Override
public void enter() {
System.out.println("Entering Main Menu");
// Initialize menu UI
}
@Override
public void update(float deltaTime) {
// Update menu animations, etc.
}
@Override
public void exit() {
// Clean up menu resources
}
@Override
public void render() {
// Render menu UI
}
@Override
public void handleInput(int keyCode, boolean pressed) {
if (pressed && keyCode == java.awt.event.KeyEvent.VK_ENTER) {
stateMachine.changeState("LOADING");
}
}
@Override
public String getName() {
return "MAIN_MENU";
}
}
private class LoadingState extends GameState {
private float loadTime = 0;
@Override
public void enter() {
System.out.println("Loading game resources...");
loadTime = 0;
// Start loading assets
}
@Override
public void update(float deltaTime) {
loadTime += deltaTime;
// Simulate loading completion
if (loadTime >= 2.0f) { // 2 seconds loading time
stateMachine.changeState("PLAYING");
}
}
@Override
public void exit() {
// Finalize loading
}
@Override
public void render() {
// Render loading screen with progress
}
@Override
public void handleInput(int keyCode, boolean pressed) {
// Loading state typically doesn't handle input
}
@Override
public String getName() {
return "LOADING";
}
}
private class PlayingState extends GameState {
@Override
public void enter() {
System.out.println("Game started!");
// Initialize game world
}
@Override
public void update(float deltaTime) {
// Update game logic
// Update entities, physics, etc.
}
@Override
public void exit() {
// Pause game world
}
@Override
public void render() {
// Render game world
}
@Override
public void handleInput(int keyCode, boolean pressed) {
if (pressed && keyCode == java.awt.event.KeyEvent.VK_ESCAPE) {
stateMachine.changeState("PAUSED");
}
if (pressed && keyCode == java.awt.event.KeyEvent.VK_G) {
// Example: player died
stateMachine.changeState("GAME_OVER");
}
if (pressed && keyCode == java.awt.event.KeyEvent.VK_V) {
// Example: player won
stateMachine.changeState("VICTORY");
}
}
@Override
public String getName() {
return "PLAYING";
}
}
private class PausedState extends GameState {
@Override
public void enter() {
System.out.println("Game paused");
// Show pause menu
}
@Override
public void update(float deltaTime) {
// Update pause menu animations
}
@Override
public void exit() {
// Hide pause menu
}
@Override
public void render() {
// Render pause menu over game world
}
@Override
public void handleInput(int keyCode, boolean pressed) {
if (pressed && keyCode == java.awt.event.KeyEvent.VK_ESCAPE) {
stateMachine.changeState("PLAYING");
}
if (pressed && keyCode == java.awt.event.KeyEvent.VK_M) {
stateMachine.changeState("MAIN_MENU");
}
}
@Override
public String getName() {
return "PAUSED";
}
}
private class GameOverState extends GameState {
@Override
public void enter() {
System.out.println("Game Over");
// Show game over screen
}
@Override
public void update(float deltaTime) {
// Update game over screen
}
@Override
public void exit() {
// Clean up
}
@Override
public void render() {
// Render game over screen
}
@Override
public void handleInput(int keyCode, boolean pressed) {
if (pressed && keyCode == java.awt.event.KeyEvent.VK_ENTER) {
stateMachine.changeState("MAIN_MENU");
}
if (pressed && keyCode == java.awt.event.KeyEvent.VK_R) {
stateMachine.changeState("PLAYING"); // Restart
}
}
@Override
public String getName() {
return "GAME_OVER";
}
}
private class VictoryState extends GameState {
@Override
public void enter() {
System.out.println("Victory!");
// Show victory screen
}
@Override
public void update(float deltaTime) {
// Update victory screen
}
@Override
public void exit() {
// Clean up
}
@Override
public void render() {
// Render victory screen
}
@Override
public void handleInput(int keyCode, boolean pressed) {
if (pressed && keyCode == java.awt.event.KeyEvent.VK_ENTER) {
stateMachine.changeState("MAIN_MENU");
}
}
@Override
public String getName() {
return "VICTORY";
}
}
}
Hierarchical State Machines
1. Hierarchical FSM Implementation
package com.game.fsm.hierarchical;
import java.util.*;
public class HierarchicalStateMachine implements State {
private final Map<String, State> states;
private State currentState;
private State parentState;
private final String name;
public HierarchicalStateMachine(String name) {
this.name = name;
this.states = new HashMap<>();
this.currentState = null;
this.parentState = null;
}
public void addState(State state) {
states.put(state.getName(), state);
}
public void setInitialState(String stateName) {
State initialState = states.get(stateName);
if (initialState != null) {
currentState = initialState;
}
}
public void setParentState(State parent) {
this.parentState = parent;
}
@Override
public void enter() {
if (currentState != null) {
currentState.enter();
}
}
@Override
public void update(float deltaTime) {
if (currentState != null) {
currentState.update(deltaTime);
}
}
@Override
public void exit() {
if (currentState != null) {
currentState.exit();
}
}
public void changeState(String stateName) {
State nextState = states.get(stateName);
if (nextState == null) return;
if (currentState != null) {
currentState.exit();
}
currentState = nextState;
currentState.enter();
}
@Override
public String getName() {
return name;
}
public State getCurrentState() {
return currentState;
}
// Example usage for character with locomotion and combat hierarchies
public static class CharacterHSM {
private final HierarchicalStateMachine rootFSM;
private final HierarchicalStateMachine locomotionFSM;
private final HierarchicalStateMachine combatFSM;
public CharacterHSM() {
// Root FSM
rootFSM = new HierarchicalStateMachine("ROOT");
// Locomotion hierarchy
locomotionFSM = new HierarchicalStateMachine("LOCOMOTION");
locomotionFSM.addState(new IdleState());
locomotionFSM.addState(new WalkingState());
locomotionFSM.addState(new RunningState());
locomotionFSM.setInitialState("IDLE");
// Combat hierarchy
combatFSM = new HierarchicalStateMachine("COMBAT");
combatFSM.addState(new PeacefulState());
combatFSM.addState(new CombatState());
combatFSM.setInitialState("PEACEFUL");
// Add sub-machines to root
rootFSM.addState(locomotionFSM);
rootFSM.addState(combatFSM);
rootFSM.setInitialState("LOCOMOTION");
}
public void update(float deltaTime) {
rootFSM.update(deltaTime);
}
// State implementations for locomotion
private class IdleState implements State {
@Override public void enter() { System.out.println("Idle"); }
@Override public void update(float deltaTime) { }
@Override public void exit() { }
@Override public String getName() { return "IDLE"; }
}
private class WalkingState implements State {
@Override public void enter() { System.out.println("Walking"); }
@Override public void update(float deltaTime) { }
@Override public void exit() { }
@Override public String getName() { return "WALKING"; }
}
// State implementations for combat
private class PeacefulState implements State {
@Override public void enter() { System.out.println("Peaceful"); }
@Override public void update(float deltaTime) { }
@Override public void exit() { }
@Override public String getName() { return "PEACEFUL"; }
}
private class CombatState implements State {
@Override public void enter() { System.out.println("Combat"); }
@Override public void update(float deltaTime) { }
@Override public void exit() { }
@Override public String getName() { return "COMBAT"; }
}
}
}
FSM with Event System
1. Event-Driven State Machine
package com.game.fsm.events;
import java.util.*;
public class EventStateMachine implements State {
private final Map<String, State> states;
private final Map<String, Map<Class<?>, String>> eventTransitions;
private State currentState;
private final String name;
public EventStateMachine(String name) {
this.name = name;
this.states = new HashMap<>();
this.eventTransitions = new HashMap<>();
this.currentState = null;
}
public void addState(State state) {
states.put(state.getName(), state);
eventTransitions.put(state.getName(), new HashMap<>());
}
public void setInitialState(String stateName) {
currentState = states.get(stateName);
if (currentState != null) {
currentState.enter();
}
}
public void addEventTransition(String fromState, Class<?> eventClass, String toState) {
Map<Class<?>, String> transitions = eventTransitions.get(fromState);
if (transitions != null) {
transitions.put(eventClass, toState);
}
}
public void sendEvent(Object event) {
if (currentState == null) return;
Map<Class<?>, String> transitions = eventTransitions.get(currentState.getName());
if (transitions != null) {
String toState = transitions.get(event.getClass());
if (toState != null) {
changeState(toState);
}
}
}
private void changeState(String stateName) {
State nextState = states.get(stateName);
if (nextState == null) return;
if (currentState != null) {
currentState.exit();
}
currentState = nextState;
currentState.enter();
}
@Override
public void enter() {
if (currentState != null) {
currentState.enter();
}
}
@Override
public void update(float deltaTime) {
if (currentState != null) {
currentState.update(deltaTime);
}
}
@Override
public void exit() {
if (currentState != null) {
currentState.exit();
}
}
@Override
public String getName() {
return name;
}
// Event classes
public static class DamageEvent {
public final float amount;
public final Object source;
public DamageEvent(float amount, Object source) {
this.amount = amount;
this.source = source;
}
}
public static class HealEvent {
public final float amount;
public HealEvent(float amount) {
this.amount = amount;
}
}
public static class PlayerSpottedEvent {
public final Object player;
public PlayerSpottedEvent(Object player) {
this.player = player;
}
}
public static class PlayerLostEvent {
public PlayerLostEvent() {}
}
}
Performance and Debugging
1. FSM Debugger and Profiler
package com.game.fsm.debug;
import com.game.fsm.State;
import com.game.fsm.StateMachine;
import java.util.*;
public class FSMDebugger {
private final StateMachine stateMachine;
private final Map<String, StateStats> stateStats;
private long startTime;
public FSMDebugger(StateMachine stateMachine) {
this.stateMachine = stateMachine;
this.stateStats = new HashMap<>();
this.startTime = System.currentTimeMillis();
}
public void onStateChange(State oldState, State newState) {
if (oldState != null) {
StateStats stats = stateStats.get(oldState.getName());
if (stats != null) {
stats.recordExit();
}
}
if (newState != null) {
StateStats stats = stateStats.computeIfAbsent(newState.getName(),
k -> new StateStats());
stats.recordEnter();
}
logStateChange(oldState, newState);
}
private void logStateChange(State oldState, State newState) {
String oldName = oldState != null ? oldState.getName() : "null";
String newName = newState != null ? newState.getName() : "null";
System.out.printf("[FSM] %s -> %s%n", oldName, newName);
}
public void printStatistics() {
long totalTime = System.currentTimeMillis() - startTime;
System.out.println("=== FSM Statistics ===");
System.out.printf("Total running time: %.2f seconds%n", totalTime / 1000.0);
for (Map.Entry<String, StateStats> entry : stateStats.entrySet()) {
StateStats stats = entry.getValue();
double percentage = (stats.totalTime * 100.0) / totalTime;
System.out.printf("State: %s%n", entry.getKey());
System.out.printf(" Entries: %d%n", stats.entryCount);
System.out.printf(" Total time: %.2fs (%.1f%%)%n",
stats.totalTime / 1000.0, percentage);
System.out.printf(" Average time: %.2fs%n",
stats.getAverageTime() / 1000.0);
}
}
private static class StateStats {
public int entryCount = 0;
public long totalTime = 0;
public long lastEntryTime = 0;
public void recordEnter() {
entryCount++;
lastEntryTime = System.currentTimeMillis();
}
public void recordExit() {
if (lastEntryTime > 0) {
totalTime += System.currentTimeMillis() - lastEntryTime;
lastEntryTime = 0;
}
}
public double getAverageTime() {
return entryCount > 0 ? (double) totalTime / entryCount : 0;
}
}
}
Conclusion
Finite State Machines are essential for game development:
Key Benefits:
- Organized logic - Clean separation of behaviors
- Predictable behavior - Well-defined state transitions
- Maintainability - Easy to modify and extend
- Debugging - Clear state flow for troubleshooting
Common Game Applications:
- Character AI - Enemy behaviors, NPC routines
- Game flow - Menu navigation, level progression
- Animation states - Character movement and actions
- UI systems - Menu navigation, dialog flows
- Game objects - Doors, switches, interactive elements
Best Practices:
- Keep states focused and single-purpose
- Use hierarchical FSMs for complex behaviors
- Implement event-driven transitions for flexibility
- Add debugging and profiling capabilities
- Consider using state machine visualizers for complex systems
FSMs provide a robust foundation for managing game logic that scales from simple character behaviors to complex game-wide state management.