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.
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)
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
