Designing a Game Application as a State Machine: A Beginner's Guide
Introduction to State Machines
A state machine is like a flowchart of your game's behavior. Imagine you're creating a simple adventure game - at any moment, your character could be in different situations: exploring, fighting, talking to NPCs, or managing inventory. Each of these situations is a "state", and the rules for moving between them are "transitions".
Why Use State Machines?
For beginners, state machines offer several advantages:
- Clear Organization: Your game logic becomes easier to understand and debug
- Predictable Behavior: You always know exactly what can happen next
- Easy to Expand: Adding new features becomes more structured
- Better Testing: Each state can be tested independently
Understanding Game States Through Examples
Example 1: A Simple RPG Character
Let's start with a basic character state system:
// Basic character states
enum CharacterState {
Idle,
Walking,
Running,
Jumping,
Attacking,
TakingDamage,
}
// Character data structure
struct Character {
state: CharacterState,
position: Position,
health: u32,
stamina: u32,
inventory: Vec<Item>,
}
// Position in 2D space
struct Position {
x: f32,
y: f32,
}
// Game item
struct Item {
id: u32,
name: String,
weight: u32,
}
Example 2: Turn-Based Battle System
Here's how we might structure a turn-based battle system:
enum BattleState {
BattleStart,
PlayerTurn,
EnemyTurn,
Victory,
Defeat,
}
struct BattleSystem {
current_state: BattleState,
player: Fighter,
enemy: Fighter,
turn_count: u32,
}
struct Fighter {
health: u32,
attack: u32,
defense: u32,
special_moves: Vec<SpecialMove>,
}
State Transitions: The Heart of Game Logic
Understanding States
Let's visualize how states flow in a simple game:
stateDiagram-v2
MainMenu --> GamePlaying
GamePlaying --> GamePaused
GamePlaying --> BattleMode
BattleMode --> GamePlaying
GamePlaying --> GameOver
GamePaused --> GamePlaying
GameOver --> MainMenu
Except for the game itself, there are many in-game states that control the game flow. These states are the heart of your game logic, and they determine how your game behaves. Basically, there are two kinds of state you can implement in your game:
- Static States: These states are fixed and cannot be changed. For example, the main menu, game over screen, or game paused state.
- Dynamic States: These states can be changed based on user input or game events. For example, the game playing state, battle mode state, or inventory state.
And those states can be divided into two categories:
- Global States: These states control the flow of the game, such as the main menu, game over screen, or game paused state, and can be accessed from anywhere in the game.
- Local States: These states are related to a specific aspect of the game, such as the game playing state, battle mode state, or inventory state, and can only be accessed within the specific context of the game.
State Flow
Let's visualize how states flow in a turn-based battle system as example 2:
stateDiagram-v2
[*] --> BattleStart
BattleStart --> PlayerTurn : Initialize Battle
PlayerTurn --> EnemyTurn : Player Action Complete
PlayerTurn --> Victory : Enemy HP <= 0
PlayerTurn --> Defeat : Player HP <= 0
EnemyTurn --> PlayerTurn : Enemy Action Complete
EnemyTurn --> Victory : Enemy HP <= 0
EnemyTurn --> Defeat : Player HP <= 0
Victory --> [*] : Battle End
Defeat --> [*] : Battle End
note right of BattleStart
Setup initial battle conditions:
- Load fighter stats
- Initialize turn counter
end note
note right of PlayerTurn
Player action options:
- Basic attack
- Special moves
- Use items
end note
note right of EnemyTurn
Enemy AI processing:
- Choose attack pattern
- Execute action
end note
1. BattleStart
Initial state when combat begins.
Entry Actions
- Initialize player and enemy Fighter structures
- Set initial health, attack, and defense values
- Load special moves into Vec
- Set turn_count to 0
Exit Conditions
- Battle initialization complete
Transitions To
- PlayerTurn (automatic after initialization)
2. PlayerTurn
Player's action phase.
Entry Actions
- Increment turn_count
- Display player options
- Enable player input
Available Actions
- Basic attack: damage = player.attack - enemy.defense
- Special moves: custom effects from special_moves Vec
Exit Conditions
- Player completes action
- Health check after action
Transitions To
- EnemyTurn (if enemy alive)
- Victory (if enemy HP <= 0)
- Defeat (if player HP <= 0)
3. EnemyTurn
Enemy's action phase.
Entry Actions
- Calculate enemy NPC decision
- Display enemy action animation
NPC Algorithm Processing
- Choose between basic attack and special moves
- Target selection (in case of multiple targets)
Exit Conditions
- Enemy completes action
- Health check after action
Transitions To
- PlayerTurn (if player alive)
- Victory (if enemy HP <= 0)
- Defeat (if player HP <= 0)
4. Victory
Battle won state.
Entry Actions
- Play victory animation
- Calculate rewards
- Update player progress
Exit Conditions
- Victory sequence complete
Transitions To
- [End State]
5. Defeat
Battle lost state.
Entry Actions
- Play defeat animation
- Save game statistics
- Prepare retry options
Exit Conditions
- Defeat sequence complete
Transitions To
- [End State]
Common Patterns and Best Practices
1. State Initialization
Always initialize your game with a clear starting state:
impl GameState {
fn new() -> Self {
Self {
current_scene: SceneType::MainMenu,
player: Player::new_game(),
game_time: GameTime::default(),
battle_state: None,
dialogue_state: None,
inventory_state: None,
}
}
}
2. State Validation
Always validate state transitions:
fn validate_transition(&self, new_state: &GameState) -> Result<(), String> {
// Check if transition is allowed
if !self.is_valid_transition(new_state) {
return Err("Invalid state transition".to_string());
}
// Validate game rules
if !self.validate_game_rules(new_state) {
return Err("Game rules violated".to_string());
}
Ok(())
}
3. Error Handling
Use proper error handling for state transitions:
enum GameError {
InvalidState(String),
SaveError(String),
LoadError(String),
}
type GameResult<T> = Result<T, GameError>;
Debugging and Testing
1. State Logging
Implement logging for state transitions use zkWasm SDK:
fn log_state_transition(&self, old_state: &GameState, new_state: &GameState) {
zkwasm_rust_sdk::dbg!("State Transition:");
zkwasm_rust_sdk::dbg!(" From: {:?}", old_state.current_scene);
zkwasm_rust_sdk::dbg!(" To: {:?}", new_state.current_scene);
zkwasm_rust_sdk::dbg!(" Player Previous Health: {:?}",
old_state.player.health
);
zkwasm_rust_sdk::dbg!(" Player Current Health: {:?}",
new_state.player.health
);
}
2. State Testing
Write tests for your state machine:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_battle_transition() {
let mut game = GameState::new();
game.start_battle(Enemy::new());
assert_eq!(game.current_scene, SceneType::Battle);
assert!(game.battle_state.is_some());
}
}
You can also test state transitions based on your RPC calls, for example, in the helloworld rollup we can find a test file which you can modify to test the state transition of the zkWasm Mini Rollup Application:
import { Player } from "./api.js";
import assert from "assert";
let account = "1234";
let player = new Player(account, "http://localhost:3000");
async function main() {
let state = await player.getState();
console.log(state);
console.log("register");
await player.register();
let pre_counter = state.player.data.counter;
console.log(pre_counter);
console.log("inc counter");
state = await player.incCounter();
let post_counter = state.player.data.counter;
console.log(post_counter);
assert(post_counter == pre_counter + 1);
}
main();
Conclusion
Building a game as a state machine makes your code more:
- Organized and easier to understand
- Reliable and predictable
- Testable and debuggable
- Ready for zkWasm integration
Remember to:
- Start simple and add complexity gradually
- Keep your states well-defined and documented
- Implement proper error handling and validation
- Test each state and transition thoroughly
- Consider performance implications of state changes
By following these patterns and practices, you'll create a solid foundation for your application that's both maintainable and extensible.