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