Skip to main content

Simplifying sprites and image sheets in Corona SDK

I like many things about Corona SDK, but one thing I don't like very much is the API provided for handling of image sheets and sprites. In my opinion it is difficult to use and very fragile because of poor encapsulation and coupling, including things like:

  • No common way to create images and sprites from the same image sheet. You're left using display.newImage(), display.newImageRect() and display.newSprite().
  • Selecting frame from an image sheet is done using frame number, for example display.newImage(sheet, 1). Not only does this expose implementation details, it also makes the code really hard to read since you don't know the meaning of the integer unless you examine the image sheet and actually count the frames. Imagine having a large image sheet with 20+ frames trying to figure out what image will be displayed by display.newImage(sheet, 13)... And if you ever want to change the frame order within the sheet, you must also change every call to display.newImage() and display.newImageRect() where the image sheet is referenced. Needless to say, this can be really painful.
  • Same thing with selecting a frame from a sprite sequence. This is also done using frame number. But this time there's a small distinction from the image case that makes a big difference. The frame number you specify is related to the frames of the sprite sequence rather than the actual image sheet. This is described better in this sprite tutorial by Corona Labs.
  • The error handling is poor. For example, the end result of an invalid image sheet or sprite sequence will often be just a nil object returned by one of the display.newX() functions, leading to tedious and frustrating debugging sessions. Another example is that specifying a frame number that is out of bounds for a given sprite sequence only yields a warning in the console log. I'd prefer to get an in-your-face error message instead, since this will always be a programming error that can easily be fixed.
To address these issues I have created a class called Spright which is basically just a factory class for images and sprites. It solves the issues mentioned above by wrapping up the image sheet together with its sprite sequences so you won't have to care about them anymore once your Spright objects have been set up. Sprites and images created by Spright are just normal Corona SDK objects, so once created you can use them just like normal sprites and images.  

The following image of a 6-sided die is used in my example to show how to create both static images and animated sprites using the same Spright object. Save it as die.png in the same folder as the example code.

Running the example in the simulator should look something like this, where the top three dice are static and the bottom three animated sprites using different sequences.


--[[
Example use of the Spright class for Corona SDK
Markus Ranner 2016
--]]
local Spright = require("Spright")
display.setStatusBar(display.HiddenStatusBar)
local sprights = {
die = Spright.new(
"die.png",
{
-- NOTE! To be able to find a frame by name, the "name" property must be set
frames = {
{
name = "one",
x=0,
y=0,
width=128,
height=128,
},
{
name = "two",
x=128,
y=0,
width=128,
height=128,
},
{
name = "three",
x=256,
y=0,
width=128,
height=128,
},
{
name = "four",
x=384,
y=0,
width=128,
height=128,
},
{
name = "five",
x=512,
y=0,
width=128,
height=128,
},
{
name = "six",
x=640,
y=0,
width=128,
height=128,
}
},
sheetContentWidth = 768,
sheetContentHeight = 128
},
{
{
name = "full",
start = 1,
count = 6,
time = 3000,
loopCount = 0,
},
{
name = "even",
frames = {2, 4, 6},
time = 3000,
loopCount = 0,
},
{
name = "odd",
frames = {1, 3, 5},
time = 3000,
loopCount = 0,
}
}
)
}
-- Create some static images
local images = { "one", 3, "five" }
for i = 1, #images do
local img = sprights.die:newImage(images[i])
img.x = display.contentCenterX
img.y = 100 + 140 * i
end
-- Create a few animated sprites
local sprite = sprights.die:newSprite()
sprite.x = display.contentCenterX
sprite.y = display.contentCenterY + 100
sprite:setSequence("even")
sprite:setFrameByName("four")
sprite:play()
sprite = sprights.die:newSprite()
sprite.x = display.contentCenterX
sprite.y = display.contentCenterY + 240
sprite:setSequence("odd")
sprite:setFrameByName("five")
sprite:play()
sprite = sprights.die:newSprite()
sprite.x = display.contentCenterX
sprite.y = display.contentCenterY + 380
sprite:setSequence("full")
sprite:setFrameByName("one")
sprite:play()
view raw main.lua hosted with ❤ by GitHub
--[[
Corona SDK class to encapsulate and simplify the handling of image sheets and sprites.
Markus Ranner 2016
--]]
local Spright = {}
Spright.__index = Spright
--- Private functions --------------
local function createImageSheet(imageFilename, options)
local imageSheet = graphics.newImageSheet( imageFilename, options )
return imageSheet
end
-- Create an index table to be able to look up sprite frames by name rather than by numerical index
local function createFrameIndex(frames)
local index = {}
for i = 1, #frames do
local frame = frames[i]
if (frame.name) then
index[frame.name] = i
end
end
return index
end
-- Create a table containing the sprite sequences indexed by sequence name
local function createSpriteSequencesIndex(spriteSequences)
local index = {}
if (spriteSequences) then
local sequencesByName = {}
for i = 1, #spriteSequences do
local seq = spriteSequences[i]
index[seq.name] = seq
end
end
return index
end
local function findFrameIndexInSequenceByName(spright, frameName, sequenceName)
local frameIndexInSheet = spright.frameIndex[frameName]
local sequence = spright.spriteSequencesByName[sequenceName]
-- Find the first frame of the sequence that has a matching frame index. Return nil if none matching.
-- Handle both cases for sequence configuration, i.e. a frames table, or start+count
local frameIndexInSequence = nil
if (sequence.frames) then
for i = 1, #sequence.frames do
local frame = sequence.frames[i]
if (frame == frameIndexInSheet) then
frameIndexInSequence = i
break
end
end
else
if ((frameIndexInSheet >= sequence.start) and (frameIndexInSheet <= (sequence.start + sequence.count))) then
frameIndexInSequence = frameIndexInSheet - sequence.start + 1
end
end
return frameIndexInSequence
end
--- Public functions --------------
function Spright:newSprite()
assert(self.spriteSequences and (#self.spriteSequences > 0), "Can't create sprite since no sprite sequences have been defined. Make sure to pass at least one sequence to Spright.new()")
local sprite = display.newSprite( self.imageSheet, self.spriteSequences )
local spright = self
-- Decorate the sprite object with a new function setFrameByName
function sprite:setFrameByName(frameName)
if (not spright.frameIndex[frameName]) then
error("Invalid frame name specified for image sheet: " .. frameName)
end
local frame = findFrameIndexInSequenceByName(spright, frameName, self.sequence)
if (not frame) then
error("No frame in sequence '" .. self.sequence .. "' matches frame name: " .. frameName)
end
self:setFrame(frame)
end
return sprite
end
function Spright:newImage(frame)
if (type(frame) ~= "number") then
if (not self.frameIndex[frame]) then
error("Invalid frame name specified for image: " .. frame)
end
frame = self.frameIndex[frame]
end
return display.newImage(self.imageSheet, frame)
end
function Spright.new(imageFilename, imageSheetOptions, spriteSequences)
assert(imageFilename, "Parameter imageFilename must be specified")
assert(imageSheetOptions, "Parameter imageSheetOptions must be specified")
params = params or {}
local newSpright = {
imageSheet = createImageSheet(imageFilename, imageSheetOptions),
frameIndex = createFrameIndex(imageSheetOptions.frames),
spriteSequences = spriteSequences,
spriteSequencesByName = createSpriteSequencesIndex(spriteSequences),
}
setmetatable(newSpright, Spright)
return newSpright
end
return Spright
view raw Spright.lua hosted with ❤ by GitHub

Comments