Improving the Corona Spine runtime

Hi, we’re developing our next game using Spine. and before I say anything else I just want to mention this tool changed our lives! I brought us (2 programmers and one graphic designer) to work closer than ever saving us hundreds lines of code and giving us results we never saw in the past.

We’ve been using it for over two months now and with a few tweaks to the runtime it works great for us.

I just wanted to share what we thought should change in the runtime and also give you the code to our “modified” version:

  1. The Original runtime cannot work with imageSheet and sprites. You can work with imageSheets by writing your own .createImage function to load images from an image sheet but you cannot control the way it deletes and creates a new image each time the attachment is changed. In our improved version you can also provide a .modifyImage function that gives you a chance to modify a sprite instead of deleting it.

  2. Each frame the runtime is reordering all the images, even if no changes were made (it re-inserts all images to the parent group). This just seems wastefull… We want to insert each image once and leave it.

  3. The runtime sets .x, .y, .rotation, .xScale, .yScale  of each image each frame(!) even if it didn’t change. We wanted to use :translate, :rotate, :scale instead (they run much faster than setting the properties) and to only call them if the value really changed.

  4. We had some issues with setting alpha through the editor so we fixed it by seperating it from the RGB values.

And here is the code replacing the original spine.lua:

spine = {} spine.utils = require "spine-lua.utils" spine.SkeletonJson = require "spine-lua.SkeletonJson" spine.SkeletonData = require "spine-lua.SkeletonData" spine.BoneData = require "spine-lua.BoneData" spine.SlotData = require "spine-lua.SlotData" spine.Skin = require "spine-lua.Skin" spine.RegionAttachment = require "spine-lua.RegionAttachment" spine.Skeleton = require "spine-lua.Skeleton" spine.Bone = require "spine-lua.Bone" spine.Slot = require "spine-lua.Slot" spine.AttachmentLoader = require "spine-lua.AttachmentLoader" spine.Animation = require "spine-lua.Animation" spine.utils.readFile = function (fileName, base) if not base then base = system.ResourceDirectory end local path = system.pathForFile(fileName, base) local file = io.open(path, "r") if not file then return nil end local contents = file:read("\*a") io.close(file) return contents end local json = require "json" spine.utils.readJSON = function (text) return json.decode(text) end spine.Skeleton.failed = {} -- Placeholder for an image that failed to load. spine.Skeleton.new\_super = spine.Skeleton.new function spine.Skeleton.new (skeletonData, group) -- Skeleton extends a group. local self = spine.Skeleton.new\_super(skeletonData) self.group = group or display.newGroup() self.images = {} -- createImage can customize where images are found. function self:createImage (attachment) return display.newImage(attachment.name .. ".png") end -- updateWorldTransform positions images. local updateWorldTransform\_super = self.updateWorldTransform function self:updateWorldTransform () updateWorldTransform\_super(self) local images = self.images for i,slot in ipairs(self.drawOrder) do local attachment = slot.attachment local image = images[slot] if not attachment then -- Attachment is gone, remove the image. if image then image:removeSelf() images[slot] = nil end else if image and image.attachment ~= attachment then -- Attachment image has changed. if self.modifyImage then self:modifyImage( image, attachment ) image.lastR, image.lastG, image.lastB, image.lastA = nil, nil, nil, nil image.attachment = attachment else --if no modifier supplied just remove the image and let it recreate image:removeSelf() images[slot] = nil image = nil end end if not image then-- Create new image. image = self:createImage( attachment ) if image then image.attachment = attachment image:setReferencePoint(display.CenterReferencePoint) image.width = attachment.width image.height = attachment.height else print("Error creating image: " .. attachment.name) image = spine.Skeleton.failed end images[slot] = image if i \< self.group.numChildren then self.group:insert( i, image ) else self.group:insert( image ) end end -- Position image based on attachment and bone. if image ~= spine.Skeleton.failed then local x = (slot.bone.worldX + attachment.x \* slot.bone.m00 + attachment.y \* slot.bone.m01) local y = -(slot.bone.worldY + attachment.x \* slot.bone.m10 + attachment.y \* slot.bone.m11) local flipX, flipY = ((self.flipX and -1) or 1), ((self.flipY and -1) or 1) local xScale = (slot.bone.worldScaleY \* attachment.scaleX) \* flipX local yScale = (slot.bone.worldScaleY \* attachment.scaleY) \* flipY local rotation = -(slot.bone.worldRotation + attachment.rotation) \* flipX \* flipY if not image.lastX then image.x, image.y = x, y image.lastX, image.lastY = x, y elseif image.lastX ~= x or image.lastY ~= y then image:translate( x-image.lastX, y-image.lastY ) image.lastX, image.lastY = x, y end if not image.lastScaleX then image.xScale, image.yScale = xScale, yScale image.lastScaleX, image.lastScaleY = xScale, yScale elseif image.lastScaleX ~= xScale or image.lastScaleY ~= yScale then image:scale( xScale/image.lastScaleX, yScale/image.lastScaleY ) image.lastScaleX, image.lastScaleY = xScale, yScale end if not image.lastRotation then image.rotation = rotation image.lastRotation = rotation elseif rotation ~= image.lastRotation then image:rotate( rotation - image.lastRotation ) image.lastRotation = rotation end if not image.lastR or image.lastR ~= slot.r or image.lastG ~= slot.g or image.lastB ~= image.lastB then image:setFillColor(slot.r, slot.g, slot.b) image.lastR, image.lastG, image.lastB = slot.r, slot.g, slot.b end if slot.a and (not slot.lastA or image.lastA ~= slot.a) then image.lastA = slot.a / 255 image.alpha = image.lastA end end end end if self.debug then for i,bone in ipairs(self.bones) do if not bone.line then bone.line = display.newLine(0, 0, bone.data.length, 0) end bone.line.x = bone.worldX bone.line.y = -bone.worldY bone.line.rotation = -bone.worldRotation if self.flipX then bone.line.xScale = -1 bone.line.rotation = -bone.line.rotation else bone.line.xScale = 1 end if self.flipY then bone.line.yScale = -1 bone.line.rotation = -bone.line.rotation else bone.line.yScale = 1 end bone.line:setColor(255, 0, 0) self.group:insert(bone.line) if not bone.circle then bone.circle = display.newCircle(0, 0, 3) end bone.circle.x = bone.worldX bone.circle.y = -bone.worldY self.group:insert(bone.circle) end end end return self end return spine

and Here is an example of createImage and modifyImage working with a TexturePacker imageSheet:

local info = require( "my\_image\_sheet" ) local sheet = graphics.newImageSheet( "my\_image\_sheet.png", info:getSheet() ) local sequence = { start=1, count=#info:getSheet().frames } local skeleton = spine.Skeleton.new( skeletonData, nil ) function skeleton:createImage( attachment ) local image = display.newSprite( sheet, sequence ) image:setFrame( info:getFrameIndex( attachment.name ) ) image.width, image.height = attachment.width, attachment.height return image end function skeleton:modifyImage( image, attachment ) image:setFrame( info:getFrameIndex( attachment.name ) ) image.width, image.height = attachment.width, attachment.height end

notice that it loads the image using newSprite instead of newImage and therefore you have the ability to just change the frame of the image instead of rebuilding it…

Also note that the attachment.name should match the frame name in the sheet info file created by texture packer.

We have noticed a performance improvement even in the simulator. and ever more so on slower devices!

Sounds great! You should consider making a pull request to the Corona runtime on github if you haven’t already, so that the changes are added to the official runtime.

What was the issue with the alpha value?

Have you tested how/if this works as intended with different skins and attachments?

newSprite was deprecated using the new sprite lib. would give faster results

@jstrahan: sprite.newSprite was deprecated. afaik display.newSprite __is__ the new API. As you can see I’m not importing the old sprite.* library anywhere…

@Reaver, we have a character with 16 different skins and it works as intended… I’m sure there might be some issues with our implementation as we only tested it for our one app… But we personally have no known issues

Regarding the pull request, I might do so :slight_smile:

Regarding the alpha, I can’t recall right now what was the exact issue… it’s not that big of a deal you can call setFillColor with the slot.a value and remove our treatment to .alpha…

sorry my bad didnt catch that

Are the frames in the spriteSheet named using skinname/imagename.png? Do you need to name all these images manually like this before packing? This might be a silly question…

Edit:

Are you using one spriteSheet for all skins or one for each skin? I’d like to use one for each skin since this will save memory usage if I will only be using one skin at a time.

This just went a little off topic… Sorry about that.

@Reaver, our change in the runtime itself has nothing to do with how the images are arranged in different sheets.

In our case we flattened the images so that they all sit in one directory (with no “/” at all) and the frame names are just the imagename (without the .png).

The way your images are arranged is up to you, you just need to provide two functions:

skeleton:createImage( attachment ) which receives an attachment and by its name it knows how to load the image. In your case you’ll need to map between the name of the attachment (which in our case is just the imagename) and the right image sheet and frame index.

skeleton:modifyImage( image, attachment) which knows how to modify an image, in your case it might need to determine if the new image is located in a different sheet and re-create the image. It might require a little tweak in order to support this (didn’t test it…) but it is of course possible…

So basically all the handling of creating and modifying images is actually written by you, our modified runtime just provides you the means to integrate with your images data model…

Not sure I really understand how multiple sheets saves you memory unless you are loading/unloading image sheets at runtime which could introduce some laginess to your game but I really don’t have the full picture :slight_smile:

We’ll be happy to help out if you need anymore help.

You definitely should consider forking the runtime on github, and making these changes into bite-sized chunks and send pull requests to Nate (the author of Spine). I’ve done some changes myself and he’s very welcoming, as he’s mostly spending his time on other runtimes and Spine itself these days.

Thanks for these though, I’ll be sure to test them out!

A question, though: Is :rotate :scale etc really faster than setting the properties? What sort of speed differences are we talking about?

Is there any way you can share a complete version of this? I am trying to get spine to work with image sheets. Also, how are you handling multiple resolutions? @2x and @4x?

thanks,

Dale

Hi Dale,

  to handle multiple resolutions, make sure you check the ‘identical layout’ box in autoSD, which should enable you to use Corona’s built-in multiresolution support. Other option - which allows for different layouts and thus less textures - is to ditch the Corona side of things completely and just manually load the correct sheet and script according to the resolution, eg:

if ( display.pixelWidth / display.actualContentWidth ) \> 0.8 then&nbsp; sheetData = require("sheet@2x") sheetImage = graphics.newImageSheet("sheet@2x.png",sheetData:getSheet()) else sheetData = require("sheet") &nbsp; sheetImage = graphics.newImageSheet("sheet.png",sheetData:getSheet()) end

The 0.8 being the same ratios as corona’s config uses.

For loading spine images from imagesheet, a more complete (but a bit simpler) example would be:

local json = spine.SkeletonJson.new() skeletonData = json:readSkeletonDataFile(filename) animation = self.skeletonData:findAnimation(animation) skeleton = spine.Skeleton.new(skeletonData) function skeleton:createImage(attachment) &nbsp;&nbsp;&nbsp; local frameIndex = sheetData:getFrameIndex(attachment.name) &nbsp;&nbsp;&nbsp; return display.newImageRect(group, sheetImage, frameIndex, attachment.width,attachment.height) end skeleton:setToSetupPose() skeleton:updateWorldTransform()&nbsp;

The technique above by gtt extends this approach a little bit, allowing for quicker changes of the images through the Corona sprite system.

Hope this helps,

  Matias

Is there any reason that you didn’t include this?

spine.AnimationStateData = require “spine-lua.AnimationStateData”

spine.AnimationState = require “spine-lua.AnimationState”

Just wondering.

Thanks Matias,

I was able to get it working with gtt’s system. I set up my spine character using the low res images and Corona is swapping out to the @2x, @4x as expected.

I am a little nervous using a new spine.lua as opposed to Nate’s just because I don’t know what I don’t know…know what I mean?

But it’s working, animation, image sheets, resolution, and skin swapping…woo hoo!!

Thanks, Dale

Happy to hear it works for you!

We are also releasing a game using our module soon. We are quite confident in this code as it’s really not touching the core Spine lua runtime and the code we changed is merely the code binding the image data created by the core lua runtime to Corona images. This seperation by Nate enabled us to manipulate only the part that links things to Corona which is as you can see a single loop that runs over all the slots after they’ve been computed by the generic lua runtime.

Not sure what these are:

spine.AnimationStateData = require “spine-lua.AnimationStateData”

spine.AnimationState = require “spine-lua.AnimationState”

 

I’ll look them in the original library not sure what they are used for…

 

@matias9, yes using the translate, scale, rotate function is much faster in Corona just read the Performance 101 tips (#2)

http://forums.coronalabs.com/topic/15165-tips-optimization-101/

Read the conversation about tests people have ran on this…

 

So I think this by itself is worth the change but you also get to work with the new sprite API and reduce a lot of image mangling + the support for dynamic resolution but that by itself can be done with the original runtime (just override the createImage and load your images from a sheet)

btw, do you know how to get the world position of a bone?

thanks,

Dale

Sounds great! You should consider making a pull request to the Corona runtime on github if you haven’t already, so that the changes are added to the official runtime.

What was the issue with the alpha value?

Have you tested how/if this works as intended with different skins and attachments?

newSprite was deprecated using the new sprite lib. would give faster results

@jstrahan: sprite.newSprite was deprecated. afaik display.newSprite __is__ the new API. As you can see I’m not importing the old sprite.* library anywhere…

@Reaver, we have a character with 16 different skins and it works as intended… I’m sure there might be some issues with our implementation as we only tested it for our one app… But we personally have no known issues

Regarding the pull request, I might do so :slight_smile:

Regarding the alpha, I can’t recall right now what was the exact issue… it’s not that big of a deal you can call setFillColor with the slot.a value and remove our treatment to .alpha…

sorry my bad didnt catch that

Are the frames in the spriteSheet named using skinname/imagename.png? Do you need to name all these images manually like this before packing? This might be a silly question…

Edit:

Are you using one spriteSheet for all skins or one for each skin? I’d like to use one for each skin since this will save memory usage if I will only be using one skin at a time.

This just went a little off topic… Sorry about that.

@Reaver, our change in the runtime itself has nothing to do with how the images are arranged in different sheets.

In our case we flattened the images so that they all sit in one directory (with no “/” at all) and the frame names are just the imagename (without the .png).

The way your images are arranged is up to you, you just need to provide two functions:

skeleton:createImage( attachment ) which receives an attachment and by its name it knows how to load the image. In your case you’ll need to map between the name of the attachment (which in our case is just the imagename) and the right image sheet and frame index.

skeleton:modifyImage( image, attachment) which knows how to modify an image, in your case it might need to determine if the new image is located in a different sheet and re-create the image. It might require a little tweak in order to support this (didn’t test it…) but it is of course possible…

So basically all the handling of creating and modifying images is actually written by you, our modified runtime just provides you the means to integrate with your images data model…

Not sure I really understand how multiple sheets saves you memory unless you are loading/unloading image sheets at runtime which could introduce some laginess to your game but I really don’t have the full picture :slight_smile:

We’ll be happy to help out if you need anymore help.