Jump to content

[TOPIC: topicViewTemplate]
[GLOBAL: userSmallPhoto]
Photo

Improving the Corona Spine runtime
Started by gtt Aug 14 2013 12:29 PM

21 replies to this topic
[TOPIC CONTROLS]
This topic has been archived. This means that you cannot reply to this topic.
[/TOPIC CONTROLS]
[modOptionsDropdown]
[/modOptionsDropdown]
[reputationFilter]
[TOPIC: post.html]
#1

gtt

[GLOBAL: userInfoPane.html]
gtt
  • Contributor

  • 164 posts
  • Corona SDK

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!



[TOPIC: post.html]
#2

Reaver

[GLOBAL: userInfoPane.html]
Reaver
  • Enthusiast

  • 87 posts
  • Corona SDK

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?



[TOPIC: post.html]
#3

jstrahan

[GLOBAL: userInfoPane.html]
jstrahan
  • Corona Geek

  • 1,926 posts
  • Corona SDK

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

[TOPIC: post.html]
#4

gtt

[GLOBAL: userInfoPane.html]
gtt
  • Contributor

  • 164 posts
  • Corona SDK

@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 :)

 

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



[TOPIC: post.html]
#5

jstrahan

[GLOBAL: userInfoPane.html]
jstrahan
  • Corona Geek

  • 1,926 posts
  • Corona SDK

sorry my bad didnt catch that



[TOPIC: post.html]
#6

Reaver

[GLOBAL: userInfoPane.html]
Reaver
  • Enthusiast

  • 87 posts
  • Corona SDK

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.



[TOPIC: post.html]
#7

gtt

[GLOBAL: userInfoPane.html]
gtt
  • Contributor

  • 164 posts
  • Corona SDK

@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 :)

 

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



[TOPIC: post.html]
#8

matias9

[GLOBAL: userInfoPane.html]
matias9
  • Observer

  • 18 posts
  • Corona SDK

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?



[TOPIC: post.html]
#9

dale.carman

[GLOBAL: userInfoPane.html]
dale.carman
  • Observer

  • 7 posts
  • Corona SDK

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



[TOPIC: post.html]
#10

matias9

[GLOBAL: userInfoPane.html]
matias9
  • Observer

  • 18 posts
  • Corona SDK

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 
  sheetData = require("sheet@2x")
  sheetImage = graphics.newImageSheet("sheet@2x.png",sheetData:getSheet())
else
  sheetData = require("sheet")
  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)
    local frameIndex = sheetData:getFrameIndex(attachment.name)
    return display.newImageRect(group, sheetImage, frameIndex, attachment.width,attachment.height)
end
skeleton:setToSetupPose()
skeleton:updateWorldTransform() 

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



[TOPIC: post.html]
#11

dale.carman

[GLOBAL: userInfoPane.html]
dale.carman
  • Observer

  • 7 posts
  • Corona SDK

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

 

 

spine.AnimationStateData = require "spine-lua.AnimationStateData"
spine.AnimationState = require "spine-lua.AnimationState"
 
Just wondering.


[TOPIC: post.html]
#12

dale.carman

[GLOBAL: userInfoPane.html]
dale.carman
  • Observer

  • 7 posts
  • Corona SDK

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



[TOPIC: post.html]
#13

gtt

[GLOBAL: userInfoPane.html]
gtt
  • Contributor

  • 164 posts
  • Corona SDK

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



[TOPIC: post.html]
#14

dale.carman

[GLOBAL: userInfoPane.html]
dale.carman
  • Observer

  • 7 posts
  • Corona SDK

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

 

thanks,

Dale



[TOPIC: post.html]
#15

gtt

[GLOBAL: userInfoPane.html]
gtt
  • Contributor

  • 164 posts
  • Corona SDK

Hi, we have found a bug with draw order. I've edited the original post to include the fix.

 

The change is on line 82. instead of #images we are using self.group.numChildren.

 

This bug reproduced when we had a hidden attachment that was suppose to be the back most image in the draw order but when it was added it was not pushed back properly.

 

Also we are not yet supporting key draw order which is a new feature in spine which we haven't even tested or used..



[TOPIC: post.html]
#16

dale.carman

[GLOBAL: userInfoPane.html]
dale.carman
  • Observer

  • 7 posts
  • Corona SDK

thanks, gtt. I will update mine...this could fix an issue that 
I was having.

thanks,

Dale



[TOPIC: post.html]
#17

richard9

[GLOBAL: userInfoPane.html]
richard9
  • Corona Geek

  • 1,118 posts
  • Enterprise

Has the official runtime been updated with these fixes?



[TOPIC: post.html]
#18

Reaver

[GLOBAL: userInfoPane.html]
Reaver
  • Enthusiast

  • 87 posts
  • Corona SDK

Has the official runtime been updated with these fixes?



As far as I've seen the have not been added unfortunately.

[TOPIC: post.html]
#19

richard9

[GLOBAL: userInfoPane.html]
richard9
  • Corona Geek

  • 1,118 posts
  • Enterprise

Okay, I've replaced the Esoteric code with this, and it works great as far as supporting TexturePacker and ImageSheets.

 

However, all skeletons are coming in scaled quite large (I have to set xScale and yScale to 0.25 for the size to fit). So I'm guessing when you work with Spine itself, you have to use the (normal) assets and not the @4x assets. Blargh!

 

Thanks to everyone who contributed to this thread. It's really made Spine somewhat usable



[TOPIC: post.html]
#20

Esoteric Software

[GLOBAL: userInfoPane.html]
Esoteric Software
  • Observer

  • 20 posts
  • Corona SDK

Hi guys, great work! I've merged the changes into the official runtime. Is that ok, gtt? I fixed a few bugs, eg typos that check the wrong variable, etc.

 

I'll be implementing keyable draw order, events, and bounding boxes soon. Edit: done.



[TOPIC: post.html]
#21

Reaver

[GLOBAL: userInfoPane.html]
Reaver
  • Enthusiast

  • 87 posts
  • Corona SDK

Thanks Nate! :D



[TOPIC: post.html]
#22

gtt

[GLOBAL: userInfoPane.html]
gtt
  • Contributor

  • 164 posts
  • Corona SDK

Of course it's OK!! Thanks Nate this is wonderful! we are back to the official runtime!




[topic_controls]
[/topic_controls]