Skip to content

Spine Animation

The SpineAnimation component plays Spine skeletal animations. Add it to an entity in the scene editor along with Transform.

Properties

PropertyTypeDefaultDescription
skeletonPathstring''Path to skeleton JSON file
atlasPathstring''Path to atlas file
skinstring''Active skin name
animationstring''Current animation name
timeScalenumber1.0Playback speed multiplier
loopbooleantrueWhether to loop the animation
playingbooleantrueWhether currently playing
flipXbooleanfalseFlip horizontally
flipYbooleanfalseFlip vertically
colorColor{r:1, g:1, b:1, a:1}Tint color RGBA
layernumber0Render order
skeletonScalenumber1.0Skeleton scale factor
materialnumber0Material ID
enabledbooleantrueWhether this animation is rendered

Setup

  1. Place Spine export files (.json + .atlas + images) in your project’s assets/ folder
  2. In the scene editor: create an entity → add Transform and SpineAnimation
  3. Set skeletonPath and atlasPath to point to your Spine files
  4. Set animation to 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; // Pause
spine.playing = true; // Resume
spine.timeScale = 2.0; // Double speed
spine.timeScale = 0.5; // Half speed

Flipping

spine.flipX = true; // Face left
spine.flipX = false; // Face right (default)

Color Tinting

spine.color = { r: 1, g: 0, b: 0, a: 1 }; // Red tint
spine.color = { r: 1, g: 1, b: 1, a: 0.5 }; // 50% transparent

Example: 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 transitions
manager.setDefaultMix(entity, 0.2);
// Set mix duration for a specific transition pair
manager.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 slot
manager.setAttachment(entity, 'weapon', '');
// Swap to a different attachment
manager.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 IK
manager.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 SpineAnimation component 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 entity
const 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 entity
const anims = manager.getAnimations(entity); // ['idle', 'run', 'jump']
const skins = manager.getSkins(entity); // ['default', 'warrior']
// Get axis-aligned bounding box
const 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:

FieldTypeDescription
entityEntityThe entity that fired the event
typeSpineEventTypeOne of the event types listed above
tracknumberAnimation track index
animationNamestringName of the animation
eventNamestring?Name of the custom event (only for event type)
intValuenumber?Integer payload (only for event type)
floatValuenumber?Float payload (only for event type)
stringValuestring?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 entity
const constraints = manager.listConstraints(entity);
// { ik: ['aim-ik'], transform: ['hip-transform'], path: ['path-follow'] }
// IK: set target position with mix strength
manager.setIKTarget(entity, 'aim-ik', targetX, targetY, 1.0);
// Transform constraints: read and modify mix values
const 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 values
const 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.