Spine Animation
The SpineAnimation component plays Spine skeletal animations. Add it to an entity in the scene editor along with Transform.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
skeletonPath | string | '' | Path to skeleton JSON file |
atlasPath | string | '' | Path to atlas file |
skin | string | '' | Active skin name |
animation | string | '' | Current animation name |
timeScale | number | 1.0 | Playback speed multiplier |
loop | boolean | true | Whether to loop the animation |
playing | boolean | true | Whether currently playing |
flipX | boolean | false | Flip horizontally |
flipY | boolean | false | Flip vertically |
color | Color | {r:1, g:1, b:1, a:1} | Tint color RGBA |
layer | number | 0 | Render order |
skeletonScale | number | 1.0 | Skeleton scale factor |
material | number | 0 | Material ID |
enabled | boolean | true | Whether this animation is rendered |
Setup
- Place Spine export files (
.json+.atlas+ images) in your project’sassets/folder - In the scene editor: create an entity → add
TransformandSpineAnimation - Set
skeletonPathandatlasPathto point to your Spine files - Set
animationto the name of the animation to play
Controlling Animations
Query the SpineAnimation component in a system to change animations at runtime:
import { defineSystem, addSystem, Res, Input, Query, Mut, SpineAnimation } from 'esengine';import { Player } from './components';
addSystem(defineSystem( [Res(Input), Query(Mut(SpineAnimation), Player)], (input, query) => { for (const [entity, spine] of query) { if (input.isKeyPressed('Space')) { spine.animation = 'jump'; spine.loop = false; } else if (input.isKeyDown('KeyD') || input.isKeyDown('KeyA')) { spine.animation = 'run'; spine.loop = true; } else { spine.animation = 'idle'; spine.loop = true; } } }));Skins
Switch skins to change the character’s appearance:
spine.skin = 'warrior';Playback Control
spine.playing = false; // Pausespine.playing = true; // Resumespine.timeScale = 2.0; // Double speedspine.timeScale = 0.5; // Half speedFlipping
spine.flipX = true; // Face leftspine.flipX = false; // Face right (default)Color Tinting
spine.color = { r: 1, g: 0, b: 0, a: 1 }; // Red tintspine.color = { r: 1, g: 1, b: 1, a: 0.5 }; // 50% transparentExample: Character Controller
Define a CharacterState component, attach it with SpineAnimation on the entity in the editor:
import { defineComponent, defineSystem, addSystem, Res, Input, Time, Query, Mut, Transform, SpineAnimation} from 'esengine';
const CharacterState = defineComponent('CharacterState', { speed: 200, currentAnim: 'idle'});
addSystem(defineSystem( [Res(Input), Res(Time), Query(Mut(Transform), Mut(SpineAnimation), Mut(CharacterState))], (input, time, query) => { for (const [entity, transform, spine, state] of query) { let moving = false;
if (input.isKeyDown('KeyD')) { transform.position.x += state.speed * time.delta; spine.flipX = false; moving = true; } if (input.isKeyDown('KeyA')) { transform.position.x -= state.speed * time.delta; spine.flipX = true; moving = true; }
const newAnim = moving ? 'run' : 'idle'; if (state.currentAnim !== newAnim) { state.currentAnim = newAnim; spine.animation = newAnim; spine.loop = true; } } }));Animation Mixing
Crossfade between animations with configurable mix duration via SpineManager:
import { SpineManager } from 'esengine/spine';
const manager = app.getResource(SpineManager);
// Set default mix duration for all transitionsmanager.setDefaultMix(entity, 0.2);
// Set mix duration for a specific transition pairmanager.setMixDuration(entity, 'idle', 'run', 0.15);manager.setMixDuration(entity, 'run', 'idle', 0.3);Attachment Control
Show/hide slots and swap attachments at runtime:
const manager = app.getResource(SpineManager);
// Hide a slotmanager.setAttachment(entity, 'weapon', '');
// Swap to a different attachmentmanager.setAttachment(entity, 'weapon', 'sword');IK Constraints
Configure inverse kinematics constraints on Spine skeletons. IK targets can be positioned programmatically:
const manager = app.getResource(SpineManager);
// mix: 0.0 = no effect, 1.0 = full IKmanager.setIKTarget(entity, 'aim-ik', targetX, targetY, 1.0);Slot Color
Tint individual Spine slots:
const manager = app.getResource(SpineManager);
manager.setSlotColor(entity, 'body', 1, 0.5, 0.5, 1);Multi-Version Architecture
Estella supports Spine versions 3.8, 4.1, and 4.2 simultaneously. The engine automatically detects the Spine version from the skeleton data and selects the appropriate runtime backend.
import type { SpineVersion } from 'esengine/spine';// SpineVersion = '3.8' | '4.1' | '4.2'How It Works
- When a Spine asset is loaded, the engine detects the version from the skeleton file header
- A matching backend is selected automatically: C++ native (preferred) or WASM fallback
- No code changes are needed — the same
SpineAnimationcomponent works with all versions
SpineManager Runtime API
For advanced use cases, the SpineManager provides runtime introspection:
import { SpineManager } from 'esengine/spine';
// Detect version from raw data (before loading)const version = SpineManager.detectVersion(skelBinaryData);const versionJson = SpineManager.detectVersionJson(skelJsonString);
// Query a loaded entityconst manager = app.getResource(SpineManager);const ver = manager.getEntityVersion(entity); // '3.8' | '4.1' | '4.2'const anims = manager.getAnimations(entity); // ['idle', 'run', 'jump']const skins = manager.getSkins(entity); // ['default', 'warrior']const bounds = manager.getBounds(entity); // { x, y, width, height }Animation Queries & Events
Added in v0.12.0.
Animation Queries
Use the SpineManager resource to query animation state at runtime:
import { SpineManager } from 'esengine/spine';
const manager = app.getResource(SpineManager);
// List all available animations / skins for an entityconst anims = manager.getAnimations(entity); // ['idle', 'run', 'jump']const skins = manager.getSkins(entity); // ['default', 'warrior']
// Get axis-aligned bounding boxconst bounds = manager.getBounds(entity); // { x, y, width, height }Event Callbacks
Spine emits lifecycle and custom events each frame. The SpineEvents resource collects them into a read-only array you can consume in any system:
import { defineSystem, addSystem, Res } from 'esengine';import { SpineEvents } from 'esengine/spine';import type { SpineEvent } from 'esengine/spine';
addSystem(defineSystem( [Res(SpineEvents)], (spineEvents) => { for (const evt of spineEvents.events) { switch (evt.type) { case 'complete': console.log(`${evt.animationName} finished on track ${evt.track}`); break; case 'event': // Custom event defined in Spine editor console.log(`Custom event: ${evt.eventName}`, evt.intValue, evt.floatValue, evt.stringValue); break; } } }));Event types: start, interrupt, end, complete, event (custom).
Each SpineEvent contains:
| Field | Type | Description |
|---|---|---|
entity | Entity | The entity that fired the event |
type | SpineEventType | One of the event types listed above |
track | number | Animation track index |
animationName | string | Name of the animation |
eventName | string? | Name of the custom event (only for event type) |
intValue | number? | Integer payload (only for event type) |
floatValue | number? | Float payload (only for event type) |
stringValue | string? | String payload (only for event type) |
Constraint Control
Query and modify IK, transform, and path constraints at runtime:
const manager = app.getResource(SpineManager);
// List all constraints on an entityconst constraints = manager.listConstraints(entity);// { ik: ['aim-ik'], transform: ['hip-transform'], path: ['path-follow'] }
// IK: set target position with mix strengthmanager.setIKTarget(entity, 'aim-ik', targetX, targetY, 1.0);
// Transform constraints: read and modify mix valuesconst tmix = manager.getTransformConstraintMix(entity, 'hip-transform');// { mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY }manager.setTransformConstraintMix(entity, 'hip-transform', { ...tmix!, mixRotate: 0.5,});
// Path constraints: read and modify mix valuesconst pmix = manager.getPathConstraintMix(entity, 'path-follow');// { position, spacing, mixRotate, mixX, mixY }manager.setPathConstraintMix(entity, 'path-follow', { ...pmix!, mixRotate: 0.8,});Multi-Texture Batching
Spine skeletons that use multiple atlas pages are automatically batched by the renderer. No additional configuration is needed — the C++ backend handles multi-texture setups transparently, keeping draw calls low even with complex multi-atlas characters.
Resource Cleanup
Spine resources are automatically cleaned up when the owning entity is despawned. The engine calls SpineManager.removeEntity() via onDespawn, freeing the underlying WASM memory. You do not need to manually release Spine resources when destroying entities.