spine_bevy 0.2.2

Bevy 0.19 plugin for the unofficial spine_runtime port: load, animate, and draw Spine 4.3 skeletons.
Documentation

spine_bevy

CI crates.io docs.rs

A Bevy 0.19 plugin that loads, animates, and draws Spine 4.3 skeletons. Each skeleton is an entity, drawn in 2D or 3D.

It builds on spine_runtime, an unofficial Rust port of the Spine runtime. This crate turns that runtime's output into Bevy meshes and materials, and has no unsafe code.

Built for Dead Money's own games and mostly written by AI agents under human direction.

You need a Spine Editor license

This crate's own code is MIT, but it depends on spine_runtime, a translation of Esoteric Software's spine-cpp under the Spine Runtimes License Agreement. Under Section 2 of the Spine Editor License Agreement:

  • Every developer who builds software with this crate needs their own Spine Editor license, including to build and run the examples. Players of a game you ship don't need one.
  • Include the Spine Runtimes License Agreement in the documentation or other materials that come with your product.

If you're unsure whether your use is covered, ask Esoteric Software.

This release reads Spine 4.3 exports, binary (.skel) or JSON, each paired with its .atlas. 4.2 exports won't load; re-export them from a 4.3 editor.

Quick start

[dependencies]
bevy = "0.19"
spine_bevy = "0.2"

It needs Rust 1.99 or newer.

use bevy::prelude::*;
use spine_bevy::{SpinePlugin, SpineSkeleton, SpineSkeletonAsset, SpineSkeletonLoaderSettings};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(SpinePlugin)
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2d);

    // Point the loader at the premultiplied-alpha atlas (see Atlases below).
    let skeleton: Handle<SpineSkeletonAsset> = asset_server
        .load_builder()
        .with_settings(|s: &mut SpineSkeletonLoaderSettings| {
            s.atlas_path = Some("spineboy/export/spineboy-pma.atlas".into());
        })
        .load("spineboy/export/spineboy-pro.skel");

    commands.spawn(SpineSkeleton::new(skeleton).with_initial_animation(0, "walk", true));
}

examples/spineboy_walk.rs is the runnable version. JSON exports load the same way, with SpineSkeletonJsonLoaderSettings.

Playing animations and skins

SpineSkeleton is a component, so change it from any system:

fn jump_on_space(keys: Res<ButtonInput<KeyCode>>, mut skeletons: Query<&mut SpineSkeleton>) {
    if !keys.just_pressed(KeyCode::Space) {
        return;
    }
    for mut skeleton in &mut skeletons {
        skeleton.play(0, "jump", false);
        skeleton.time_scale = 1.5;
    }
}
  • play(track, name, looping) starts an animation, and set_skin(name) switches skins. Both work before the asset finishes loading; they apply once it does.
  • with_initial_animation and with_initial_skin set them at spawn.
  • available_animations() and available_skins() list what the rig has.
  • time_scale, paused, and physics control playback.
  • For anything else, such as queuing animations or blending tracks, animation_state_mut() and skeleton_mut() give you the spine_runtime objects directly.

To change something on the frame it should take effect, order your system .before(SpineSet::Tick).

Events

Animation events keyed in the editor (footsteps, hit frames) arrive as Bevy messages, tagged with the skeleton's entity:

use spine_bevy::SpineKeyframeEvent;

fn footsteps(mut events: MessageReader<SpineKeyframeEvent>, skeletons: Query<&SpineSkeleton>) {
    for e in events.read() {
        let Some(skeleton) = skeletons.get(e.entity).ok().and_then(|s| s.skeleton()) else {
            continue;
        };
        if skeleton.data().events[e.event.data.index()].name == "footstep" {
            // play a sound
        }
    }
}

SpineStateEvent carries playback events: an animation starting, being interrupted, ending, or completing a loop.

3D

Add SpineRender3d beside the skeleton and use a Camera3d. The rig draws as a flat, unlit plane you can place and rotate with its Transform:

commands.spawn((SpineSkeleton::new(skeleton), SpineRender3d, Transform::from_xyz(0.0, 0.0, -5.0)));

2D and 3D skeletons can share one app. examples/spineboy_walk_3d.rs shows a full scene.

Atlases

The materials expect premultiplied-alpha textures. Most Spine exports include a *-pma.atlas / *-pma.png pair beside the regular one; point the loader at it, as in the Quick start. Without settings, the loader looks for an atlas named after the skeleton (dropping -pro, -ess, or -ios). For a straight-alpha atlas, premultiply the PNGs in your asset pipeline.

Known limitations

Blending happens in linear color space. Spine's renderers blend in gamma (sRGB) space; Bevy blends in linear space. Opaque slots match Spine, but additive and semi-transparent slots come out somewhat different: additive highlights look dimmer and overlapping translucent slots less saturated than in the Spine editor. Matching exactly would mean drawing skeletons into a separate gamma-space target and compositing the result.

Performance

Skeletons update in parallel. Each render command is one draw call, and the runtime merges adjacent attachments that share a texture, blend mode, and color, so spineboy draws in one. Measured under Bevy 0.18 on an i9-14900K, 50 skeletons cost about 0.25 ms of CPU per frame and 1000 about 2.75 ms; past roughly 500 on screen, render extraction and draw calls dominate the frame. cargo run --release --example spine_stress measures it on your machine.

Version compatibility

Bevy Spine spine_bevy
0.19 4.3 0.2
0.18 4.3 f2a00bb
0.18 4.2 8c7be8a

Each Bevy minor needs its own spine_bevy release. The 0.18 rows are the last commits on that Bevy version; pair the 4.2 one with spine_runtime's v0.1.0 tag.

Examples

The examples use the rigs from Esoteric's spine-runtimes repository, cloned beside this one. That art is licensed separately and doesn't ship with this crate. A clone of this repo also builds against a spine_runtime checkout beside it.

git clone https://github.com/dead-money/spine_runtime ../spine_runtime
git clone -b 4.3 https://github.com/EsotericSoftware/spine-runtimes ../spine-runtimes
cargo run --example spine_browser
  • spine_browser is a gallery of every example rig. Space / Shift+Space switch rigs, N switches animations, S switches skins, R restarts, + / - change speed, Esc quits. --assets <path> or SPINE_EXAMPLES_DIR points it at the rigs.
  • spineboy_walk is the Quick start.
  • spineboy_walk_3d draws spineboy in a 3D scene with a ground plane, a light, and an orbiting camera.
  • spineboy_screenshot saves frame 60 to a PNG and exits (SPINE_SCREENSHOT, SPINE_SCREENSHOT_FRAMES).
  • spine_stress fills the screen with spineboys. ] / [ change the count, and --csv path.csv logs timings per frame. Run it with --release.

To record a GIF like the one above, spine_browser can save a frame sequence:

SPINE_BROWSER_RECORD_DIR=/tmp/frames SPINE_BROWSER_RECORD_FRAMES=120 SPINE_BROWSER_RECORD_WARMUP=45 \
cargo run --example spine_browser -- --rig celestial-circus-pro --anim swing --width 640 --height 360
ls /tmp/frames/frame_*.png | awk 'NR%2==1' | xargs convert -delay 3 -loop 0 -layers OptimizePlus clip.gif

Licensing

Source in this repository is © 2026 Dead Money under the MIT License. It's original work; no spine-cpp code is translated here.

spine_runtime, which this crate depends on, is a spine-cpp derivative under the Spine Runtimes License Agreement, not MIT. Its terms, including the Spine Editor license requirement above, apply to anything built with this crate.

Acknowledgements

Built on Bevy and spine_runtime, a port of Esoteric Software's Spine runtime. The upstream spine-runtimes repository is the source of truth for runtime behavior. Report runtime bugs to spine_runtime; report Bevy integration bugs here.