StateMachine
A generic, type-safe state machine library for Gleam with easing function support and transition weights.
Features
- 🎯 Fully Generic: Works with any state type (enums, strings, custom types)
- 🎨 Easing Support: Compatible with easings_gleam for smooth transitions
- ⚖️ Weighted Transitions: Prioritize transitions when multiple conditions are true
- 🔄 Automatic Blending: Smooth interpolation between states with configurable durations
- ⚡ Context-Aware Conditions: Pass game/app context to transition condition functions
- 🛡️ Type-Safe: Leverage Gleam’s type system for correctness
- 🔧 Immutable: Pure functional updates, no hidden state
- ⏱️ Duration-Based: Uses gleam_time’s Duration type for precise timing
Installation
gleam add statemachine
Quick Start
import statemachine
import gleam/option.{None}
import gleam/time/duration
// Define your state type
type PlayerState {
Idle
Walking
Running
}
// Define your context
type GameContext {
GameContext(velocity: Float)
}
// Create state machine
let machine =
statemachine.new(initial_state: Idle)
|> statemachine.with_state(state: Walking)
|> statemachine.with_state(state: Running)
|> statemachine.with_transition(
from: Idle,
to: Walking,
condition: statemachine.Custom(fn(ctx: GameContext) { ctx.velocity >. 0.1 }),
blend_duration: duration.milliseconds(200),
easing: None,
weight: 0,
)
|> statemachine.with_transition(
from: Walking,
to: Running,
condition: statemachine.Custom(fn(ctx: GameContext) { ctx.velocity >. 5.0 }),
blend_duration: duration.milliseconds(300),
easing: None,
weight: 0,
)
// Update in game loop
fn update(model: Model, delta_time: duration.Duration) {
let ctx = GameContext(velocity: model.velocity)
let #(new_machine, transitioned) = statemachine.update(machine, ctx, delta_time)
case statemachine.state_data(new_machine) {
statemachine.Single(state) -> // Use single state
statemachine.BlendingData(from, to, factor) -> // Interpolate
}
}
Using with easings_gleam
The library works seamlessly with easings_gleam for beautiful transitions:
import statemachine
import easings
import gleam/option.{Some}
import gleam/time/duration
statemachine.with_transition(
from: Idle,
to: Running,
condition: statemachine.Always,
blend_duration: duration.milliseconds(500),
easing: Some(easings.cubic_in_out),
weight: 0,
)
Transition Conditions
Three types of conditions control when transitions occur:
Always
Transition immediately:
import gleam/time/duration
statemachine.with_transition(
from: Idle,
to: Jump,
condition: statemachine.Always,
blend_duration: duration.milliseconds(100),
easing: None,
weight: 0,
)
AfterDuration
Transition after spending time in the current state:
import gleam/time/duration
import easings
import gleam/option.{Some}
// Automatically transition after 5 seconds
statemachine.with_transition(
from: Idle,
to: Sleeping,
condition: statemachine.AfterDuration(duration.seconds(5)),
blend_duration: duration.seconds(1),
easing: Some(easings.ease_in_out_sine),
weight: 0,
)
Custom
Transition based on custom game/app context:
// Velocity-based
statemachine.Custom(fn(ctx) { ctx.velocity >. 5.0 })
// Input-based
statemachine.Custom(fn(ctx) { ctx.jump_pressed && ctx.is_grounded })
Transition Weights
When multiple transitions from the same state have their conditions met, the transition with the highest weight is chosen:
import statemachine
import gleam/time/duration
statemachine.new(initial_state: Idle)
|> statemachine.with_state(state: Idle)
|> statemachine.with_state(state: Walking)
|> statemachine.with_state(state: Running)
// Lower priority transition
|> statemachine.with_transition(
from: Idle,
to: Walking,
condition: statemachine.Always,
blend_duration: duration.milliseconds(200),
easing: None,
weight: 5,
)
// Higher priority transition - this one will be chosen!
|> statemachine.with_transition(
from: Idle,
to: Running,
condition: statemachine.Always,
blend_duration: duration.milliseconds(200),
easing: None,
weight: 10,
)
This is useful for creating layered logic where more specific conditions take precedence.
Manual Transitions
Force transitions programmatically (useful for events like damage, death, cutscenes):
import easings
import gleam/option.{None, Some}
import gleam/time/duration
// Force transition with custom blend and easing
let machine = statemachine.transition_to(
machine,
HitReaction,
blend_duration: Some(duration.milliseconds(100)),
easing: Some(easings.ease_out_back),
)
// Force transition with defaults
let machine = statemachine.transition_to(
machine,
Dead,
blend_duration: None,
easing: None,
)
API Overview
Building State Machines
new(initial_state:)- Create a new state machinewith_state(machine, state:)- Register a statewith_transition(...)- Define conditional transitions with easing and weightwith_default_blend(machine, duration:)- Set default blend durationwith_default_easing(machine, easing:)- Set default easing function
Runtime
update(machine, context, delta_time)- Evaluate conditions and updatestate_data(machine)- Get current or blended state data (returnsStateData)transition_to(...)- Manually force a transition
Querying
is_blending(machine)- Check if transitioningblend_progress(machine)- Get linear blend progress (0.0 to 1.0)state_ids(machine)- Get Set of all state IDsstate_count(machine)- Count statestransition_count(machine)- Count transitions
Types
StateData(state)- Result ofstate_data():Single(state)orBlendingData(from, to, factor)Condition(ctx)- Transition conditions:Always,AfterDuration(Duration), orCustom(fn(ctx) -> Bool)
Philosophy
StateMachine is designed to be:
- Generic: Works with any state type, not just enums
- Composable: Build complex state graphs declaratively
- Type-safe: Leverage Gleam’s type system
- Functional: Immutable updates, pure functions
- Flexible: Compatible with existing easing libraries
- Precise: Uses Duration for accurate timing
Use Cases
- Game Animations: Character controllers, NPC behaviors
- UI Transitions: Menu states, loading screens, modal animations
- Application States: Workflow management, process orchestration
- Audio: Music/sound state management with crossfading
License
MIT
Contributing
Contributions welcome! This library was extracted from Tiramisu to benefit the broader Gleam ecosystem.