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:
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.
- 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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 |
Comments
Post a Comment