Spine Extension 4.0.0

The addition of Tracks for Spine Nodes in the 4.0.0 release of the Spine Extensions introduced breaking changes. Please be aware of them.

:warning: Breaking Change: GUI Spine Callback Behavior

Previous Behavior (Before Multi-Track):

gui.play_spine_anim(node, "run", gui.PLAYBACK_ONCE_FORWARD, {},
function(self, node)
    -- Callback was only called on animation completion
    -- No need to check message type
    print("Animation completed")
end)

New Behavior (Multi-Track Implementation):

gui.play_spine_anim(node, "run", gui.PLAYBACK_ONCE_FORWARD, {},
function(self, node, message_id, message)
    -- Callback now receives both completion AND spine events
    if message_id == hash("spine_animation_done") then
        print("Animation completed on track", message.track)
    elseif message_id == hash("spine_event") then
        print("Spine event:", message.event_id)
    end
end)

:new_button:Added support tracks for GUI Spine nodes.

gui.play_spine_anim

-- Without tracks: Single animation (uses default track 1)
gui.play_spine_anim(node, "run", gui.PLAYBACK_ONCE_FORWARD)

-- With tracks: Multi-track animations for layered effects
gui.play_spine_anim(node, "idle", gui.PLAYBACK_LOOP_FORWARD, { track = 1 })  -- base animation
gui.play_spine_anim(node, "aim", gui.PLAYBACK_LOOP_FORWARD, { track = 2 })   -- overlay animation
gui.play_spine_anim(node, "shoot", gui.PLAYBACK_ONCE_FORWARD, { track = 3 }) -- one-shot action

gui.cancel_spine

-- Without tracks: Cancel all animations
gui.cancel_spine(node)

-- With tracks: Cancel specific tracks or all tracks
gui.cancel_spine(node, { track = 2 })   -- cancel only track 2
gui.cancel_spine(node, { track = -1 })  -- cancel all tracks (explicit)

gui.get_spine_animation

-- Without tracks: Get animation from default track
local anim_id = gui.get_spine_animation(node)

-- With tracks: Get animation from specific track
local track1_anim = gui.get_spine_animation(node, { track = 1 })
local track2_anim = gui.get_spine_animation(node, { track = 2 })

gui.set_spine_cursor / gui.get_spine_cursor

-- Without tracks: Control cursor on default track
gui.set_spine_cursor(node, 0.5)
local cursor = gui.get_spine_cursor(node)

-- With tracks: Control cursor per track
gui.set_spine_cursor(node, 0.5, { track = 1 })  -- set track 1 to 50%
gui.set_spine_cursor(node, 0.8, { track = 2 })  -- set track 2 to 80%
local cursor1 = gui.get_spine_cursor(node, { track = 1 })
local cursor2 = gui.get_spine_cursor(node, { track = 2 })

gui.set_spine_playback_rate / gui.get_spine_playback_rate

-- Without tracks: Control playback rate globally
gui.set_spine_playback_rate(node, 2.0)
local rate = gui.get_spine_playback_rate(node)

-- With tracks: Independent playback rates per track
gui.set_spine_playback_rate(node, 1.0, { track = 1 })  -- normal speed walk
gui.set_spine_playback_rate(node, 0.5, { track = 2 })  -- slow breathing
gui.set_spine_playback_rate(node, 3.0, { track = 3 })  -- fast shooting

Complete Multi-Track Animation Example

function init(self)
    local node = gui.get_node("character")

    -- Old approach: Single animation at a time
    -- gui.play_spine_anim(node, "idle", gui.PLAYBACK_LOOP_FORWARD)

    -- New approach: Layered animations
    gui.play_spine_anim(node, "idle", gui.PLAYBACK_LOOP_FORWARD, { track = 1 })     -- base movement
    gui.play_spine_anim(node, "breathe", gui.PLAYBACK_LOOP_FORWARD, { track = 2 })  -- breathing overlay

    -- Interactive one-shot animation on separate track
    gui.play_spine_anim(node, "wave", gui.PLAYBACK_ONCE_FORWARD, { track = 3 })
end

function on_input(self, action_id, action)
    local node = gui.get_node("character")

    if action_id == hash("shoot") then
        -- Play shooting animation without interrupting base animations
        gui.play_spine_anim(node, "shoot", gui.PLAYBACK_ONCE_FORWARD, { track = 4 })
    elseif action_id == hash("stop_breathing") then
        -- Stop only the breathing animation
        gui.cancel_spine(node, { track = 2 })
    end
end

Several other changes have recently been made to the Spine Extension.

Create “spine_scene” in runtime

Now it is possible to create spine_scene in runtime:

      function init(self)
          -- Load Spine JSON data
          local json = sys.load_resource("/data/character.spinejson")

          -- Create spinescene dynamically
          local scene = resource.create_spinescene("/dyn/character.spinescenec", {
               spine_data = json,


              atlas_path = "/textures/character.a.texturesetc"
          })

          go.set("/gui#swap", "spine_scene", scene, { key = "spineboy" })
        -- It's posisble to set from gui component itself as well:
        -- gui.set(msg.url(), "spine_scene", scene, { key = "spineboy" })
      end

Ability to specify Mix Blend

  • Added mix blend for tracks spine.MIX_BLEND_*.
    Mix Blend controls how timeline values are mixed with setup pose values or current pose values when a timeline is applied with alpha < 1.
spine.play_anim(self.url, directions[direction_index], go.PLAYBACK_ONCE_FORWARD, {track = 3,
mix_blend = spine.MIX_BLEND_ADD,
blend_duration = 0.5}, function(_, message_id)

Thanks @sashkent3


Physics functions

New functions added:

spine.physics_translate("player#spinemodel", offset)
spine.physics_rotate(self.spine_model, center_pos, angle_change)
gui.spine_physics_translate(gui.get_node("spine_node"), translation)
gui.spine_physics_rotate(gui.get_node("spine_node"), vmath.vector3(10, 5, 0), 45)

Editor scripts for spine components and nodes

This version allows editing Spine scenes of GUI nodes using editor scripts, e.g.:

editor.transact({
    editor.tx.add("/main.gui", "spine_scenes", {
        spine_scene = "/bg.spinescene"
    }),
})
editor.transact({
  editor.tx.add("/main.gui", "nodes", {
    type = "gui-node-type-spine",
    spine_scene = "bg"
  })
})

Mixing and clearing spine skins at runtime

  • Added the ability to add and copy skins to each other, as well as clear them:
spine.clear_skin(self.spine_model_id, "base_empty")
spine.add_skin(self.spine_model_id, "base_empty", "blue_head")
spine.add_skin(self.spine_model_id, "base_empty", "original_body")
spine.set_skin(self.spine_model_id, "base_empty")

Fixed issues

  • Fixed issues where, in some cases, using animations with a mask could crash the engine
  • Aligns the Editor’s behavior with Bob’s: both now treat missing images in an atlas used by a spinescene as errors.
  • Fixed issue where Spine model components could not be selected by clicking in the Editor
9 Likes

Thank you :blush:

Where is the documentation for which spine version goes with the defold version?

Every release has a badge with minimum needed version specified https://github.com/defold/extension-spine/releases

3 Likes

Ahh..so these two spots…

BTW: You say a minimum needed version…does this mean we can run a higher version of Spine? Or should we match the version?

That means you can use this or newer version of Defold with this particular version of the extebsion

1 Like