Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
The animation state machine provides race-condition-safe animation graph execution with priority-based transitions, transactional updates, and zero-allocation hot paths. Perfect for character AI and gameplay logic.
The Animator component links entities to animation state machines:
import {AnimationStateMachine,AnimationGraphRegistry} from '@web-engine-dev/core/animation';import { addComponent } from 'bitecs';import { Animator } from '@web-engine-dev/core/engine/ecs/components';// Create entity with animatorconst entity = createEntity(world);addComponent(world, entity, Animator);// Load animation graphconst graph = AnimationGraphRegistry.get('character_locomotion');// Acquire state machine from poolconst config = {maxQueueSize: 16,maxRequestAgeFrames: 10,blendCurve: BlendCurve.SmoothStep,entityId: entity,};const stateMachine = AnimationStateMachine.acquire(config);// Initialize with graph, mixer, and modelstateMachine.initialize(graph, mixer, skinnedMesh);// Store in componentAnimator.stateMachine[entity] = stateMachine;
Animation graphs define the structure of state machines with nodes, transitions, and parameters:
import type { AnimationGraphDef } from '@web-engine-dev/core/animation';const locomotionGraph: AnimationGraphDef = {id: 'locomotion',name: 'Character Locomotion',// Parameters for transitions and blend treesparameters: [{ name: 'speed', type: 'float', defaultValue: 0 },{ name: 'grounded', type: 'bool', defaultValue: true },{ name: 'jump', type: 'trigger', defaultValue: false },],// Animation nodes (clips, blend trees)nodes: [{id: 'idle',name: 'Idle',type: 'clip',clipName: 'Idle',loop: true,transitions: [{id: 'idle_to_locomotion',targetNodeId: 'locomotion',duration: 0.2,conditions: [{ parameterName: 'speed', operator: '>', value: 0.1 }],hasExitTime: false,},{id: 'idle_to_jump',targetNodeId: 'jump',duration: 0.1,conditions: [{ parameterName: 'jump', operator: '==', value: true }],hasExitTime: false,},],events: [],},{id: 'locomotion',name: 'Locomotion Blend',type: 'blendTree1D',parameterName: 'speed',thresholds: [{ clipName: 'Walk', threshold: 1.5 },{ clipName: 'Run', threshold: 4.0 },{ clipName: 'Sprint', threshold: 7.0 },],transitions: [{id: 'locomotion_to_idle',targetNodeId: 'idle',duration: 0.3,conditions: [{ parameterName: 'speed', operator: '<', value: 0.1 }],hasExitTime: false,},],events: [],},{id: 'jump',name: 'Jump',type: 'clip',clipName: 'Jump',loop: false,transitions: [{id: 'jump_to_idle',targetNodeId: 'idle',duration: 0.2,conditions: [{ parameterName: 'grounded', operator: '==', value: true }],hasExitTime: true,exitTime: 0.9, // Transition near end of jump},],events: [{ name: 'jump_start', time: 0.0, payload: { force: 5 } },{ name: 'jump_land', time: 0.9, payload: null },],},],entryNodeId: 'idle', // Start in idle state};
The state machine supports three node types:
Play a single animation clip with optional speed and looping:
Blend between clips based on a single parameter:
Blend in 2D parameter space for directional movement:
Transitions define how the state machine moves between nodes:
| Property | Type | Description |
|---|---|---|
| id | string | Unique transition identifier |
| targetNodeId | string | Node to transition to |
| duration | number | Crossfade duration in seconds |
| conditions | Condition[] | All must be true to trigger |
| hasExitTime | boolean | Require normalized time before transition |
| exitTime | number | Normalized time (0-1) to allow transition |
The state machine uses priority levels to handle interruptions:
import { TransitionPriority } from '@web-engine-dev/core/animation';// Queue normal transitionstateMachine.queueTransition('walk',0.3,TransitionPriority.Normal // Can be interrupted);// Queue high-priority attack (interrupts walk)stateMachine.queueTransition('attack',0.1,TransitionPriority.Action, // Higher priority{ interruptible: false } // Cannot be interrupted);// Force immediate transition (interrupts everything)stateMachine.queueTransition('death',0.0,TransitionPriority.Immediate);
| Type | Value | Use Case |
|---|---|---|
| float | number | Speed, angle, blend weight |
| int | number | State index, combo counter |
| bool | boolean | Grounded, attacking, dead |
| trigger | boolean | Jump, attack (auto-reset) |
// Set individual parameterstateMachine.setParameter('speed', velocity.length());stateMachine.setParameter('grounded', isGrounded);// Batch update (more efficient)const updates = new Map([['speed', 3.5],['grounded', true],['jump', false],]);stateMachine.updateParameters(updates);// Get parameter valueconst currentSpeed = stateMachine.getParameter('speed');
Conditions evaluate parameter values with comparison operators:
// Multiple conditions (ALL must be true)const transition = {targetNodeId: 'sprint',duration: 0.2,conditions: [{ parameterName: 'speed', operator: '>', value: 6.0 },{ parameterName: 'stamina', operator: '>', value: 10 },{ parameterName: 'crouching', operator: '==', value: false },],hasExitTime: false,};// Transition triggers when speed > 6 AND stamina > 10 AND not crouching
Complex AI can be organized hierarchically with sub-state machines:
// Main graphconst mainGraph: AnimationGraphDef = {id: 'main',name: 'Main Character Controller',parameters: [{ name: 'combat_mode', type: 'bool', defaultValue: false },],nodes: [{id: 'locomotion_subgraph',type: 'subStateMachine',graphId: 'locomotion', // References separate graphtransitions: [{targetNodeId: 'combat_subgraph',conditions: [{ parameterName: 'combat_mode', operator: '==', value: true }],},],},{id: 'combat_subgraph',type: 'subStateMachine',graphId: 'combat',transitions: [{targetNodeId: 'locomotion_subgraph',conditions: [{ parameterName: 'combat_mode', operator: '==', value: false }],},],},],entryNodeId: 'locomotion_subgraph',};// Locomotion sub-graph (idle, walk, run)const locomotionGraph: AnimationGraphDef = {id: 'locomotion',// ... locomotion states};// Combat sub-graph (idle, attack, block, dodge)const combatGraph: AnimationGraphDef = {id: 'combat',// ... combat states};
State Machine Complexity
Keep root state machines under 20 nodes. Use sub-state machines for complex AI (e.g., separate graphs for locomotion, combat, interaction, vehicle).
Animation events trigger callbacks at specific times during playback:
const attackClip: AnimationClipNode = {id: 'attack',name: 'Sword Attack',type: 'clip',clipName: 'Attack_Sword',events: [{time: 0.3, // 30% through animationname: 'swing_start',payload: { damage: 10, radius: 1.5 },},{time: 0.6, // 60% through animationname: 'hit_check',payload: { hitbox: 'sword_blade' },},{time: 0.9, // Near endname: 'attack_complete',payload: null,},],transitions: [],};
import type { AnimationRuntimeEvent } from '@web-engine-dev/core/animation';// Update state machineconst result = stateMachine.update(delta);// Flush buffered eventsstateMachine.flushEvents((event: AnimationRuntimeEvent) => {console.log(`Event: ${event.eventName} at ${event.time.toFixed(2)}s`);switch (event.eventName) {case 'swing_start':playSwingSound();break;case 'hit_check':checkHitbox(event.payload.hitbox);break;case 'attack_complete':resetCombo();break;}});
Event Buffering
Events are buffered during state machine update to prevent re-entrant modifications. Always call flushEvents() after update() to process buffered events.
The state machine processes transitions and evaluates blend trees each frame:
// In animation systemfunction updateAnimationSystem(world, delta) {const query = defineQuery([Animator]);const entities = query(world);// Increment global frame counterincrementGlobalFrame();for (const eid of entities) {const stateMachine = Animator.stateMachine[eid];// 1. Update parameters from gameplayconst velocity = getVelocity(eid);stateMachine.setParameter('speed', velocity.length());stateMachine.setParameter('grounded', isGrounded(eid));// 2. Evaluate transitions (auto-triggers based on conditions)stateMachine.evaluateTransitions();// 3. Update state machineconst result = stateMachine.update(delta);// 4. Handle state changesif (result.transitionStarted) {console.log(`Transition started: ${stateMachine.getTargetNodeId()}`);}if (result.transitionCompleted) {console.log(`Entered state: ${result.newNodeId}`);}// 5. Flush eventsstateMachine.flushEvents(handleAnimationEvent);// 6. Update Three.js mixerconst mixer = Animator.mixer[eid];mixer.update(delta);}}
The state machine eliminates common race conditions:
| Operation | Complexity | Target Time |
|---|---|---|
| State machine update | O(1) | < 0.1ms |
| Transition queue insert | O(n) | < 0.01ms (n < 16) |
| Condition evaluation | O(c) | < 0.005ms (c < 4) |
| Event buffering | O(e) | < 0.01ms (e < 8) |