ExFSM is a tiny, flexible, framework-agnostic Finite State Machine designed for game development and simulation loops.
It supports:
- Synchronous or asynchronous state transitions
- Enter / Exit / Update / Repeat lifecycle methods
- Named states with easy registration
- State parameters (passed automatically from set() to update())
- Simple success/error results via FSMResult
Perfect for menus, AI behaviors, combat states, cutscene logic, animation controllers, and more.
- Minimal API, no external dependencies
- Promise-aware, allowing async loading, delays, transitions, and cleanup
- Safe transition flow:
- Exit current state → Enter next state → Update with params
- Named state registration via strings or ExState instances
- Optional params preserved across frames (update() receives them every tick)
Just drop the module into your project:
import { ExFSM, ExState } from "./ExFSM";Most methods return a simple, consistent structure:
type FSMResult<T> = { success: boolean; message?: string; value?: T };Easy to debug and handle errors without exceptions.
🚦 Basic Usage
- Create an FSM instance
const fsm = new ExFSM();- Define some states
class IdleState extends ExState {
enter(prev) {
console.log("Entering idle");
}
update() {
console.log("Idling...");
}
}
class MoveState extends ExState {
enter(prev, direction) {
console.log("Moving in direction:", direction);
}
update(direction) {
console.log("Still moving:", direction);
}
}- Register states
fsm.register(new IdleState("idle"), new MoveState("move"));You can also register by string:
fsm.register("jump", "attack");This automatically creates ExState instances with those names.
fsm.set("idle");
fsm.update(); // calls idle.update()fsm.set("move", { x: 1, y: 0 });
fsm.update(); // calls move.update({ x:1, y:0 })class FadeOut extends ExState {
async exit(next) {
await fadeScreenToBlack();
}
}ExFSM handles this automatically:
await fsm.set("nextState");If a state’s enter() or exit() returns a Promise, the FSM waits before continuing.
Call fsm.update() every frame:
function gameLoop() {
fsm.update();
requestAnimationFrame(gameLoop);
}If your state’s update() returns a Promise, the FSM waits for it as well.
Each ExState also includes an unused optional lifecycle method:
repeat(...params): void | Promise<void>;This is reserved for custom behaviors your game may need — such as repeating transitions, animation cycling, cooldown ticks, or retry loops. (Your engine can call it manually if desired.)
register(...states)
Registers state names or ExState instances.
set(state, ...params)
Transitions to another state, calling:
- current.exit()
- next.enter()
- stores params for future updates
update()
Calls current.update(...params).
get()
Returns the current state.
has(state)
Checks if a state is registered.
reset()
Clears the FSM entirely.
fsm.register(new ExState("idle"), new ExState("chase"), new ExState("attack"));
fsm.set("idle");
// in your update loop: if (distanceToPlayer < 200) fsm.set("chase"); if (distanceToPlayer < 30) fsm.set("attack");
fsm.update();class LoadAssets extends ExState {
async enter() {
console.log("Loading...");
await loadAssetsAsync();
}
update() {
console.log("Done loading — switching!");
fsm.set("mainMenu");
}
}- If no state is active, fsm.update() returns an error.
- State names are case-sensitive.
- Registering a state with an existing name overwrites it (with a console warning).
MIT — Free to use in commercial and hobby projects alike.