This a workaround I made for the spriteSheet problem until it is solved al low-level.
It is a module that works very similar to the Corona sprite module with the following differences:
-
You need to call the init(suffix_table) function as the first step to provide a suffix table. Commonly you will copy and paste the same table you used in config.lua.
-
There is no newSpriteSheetFromData function. Instead use newSpriteSheet(fileName[, frameWidth, frameHeight]) for both uniform and non-uniform(lua based) sprite sheets. Use frameWidth and frameHeight only for uniform spriteSheets
-
The newSprite(spriteSet, imageWidth, imageHeight) function receives additional imageWidth and imageHeight parameters and works like the display.newImageRect() function
-
To dispose spriteSheets use the disposeSpriteSheet(spriteSheet) function instead of spriteSheet:dispose()
-
Every time you change x, y, xScale, yScale, rotation of the sprite or any ascendants call sprite:invalidate() to force a redraw on next frame
-
The result of newSprite is not really an sprite. It is a group with an sprite inside. Use getCurrentFrame(), setCurrentFrame(), isAnimating(), play(), prepare(), dispose() methods to acces the real sprite properties.
-
You can use the newSpriteAnimation function to create an spriteSheet, spriteSet, sequence and sprite all in a single step
Comments will explain most of it:
[lua]–[[
– By: juankpro
– spriteSheetFix fixes bug when treating uniform and non-uniform spriteSheets
– for multiple target devices. This is not supported naturally.
– 1) First you must include the module:
local spriteSheetFix = require(“spriteSheetFix”);
– 2) Then you must configure suffixes. This is an only-one time process.
– You tipically create a suffix table like the one used in config.lua
– and then register this table using the init() function:
imageSuffix =
{
["@2"] = 2,
["-ipad"] = 2.1,
["-foo"] = 3,
};
spriteSheetFix.init(imageSuffix);
– 3) Now you can start using the spriteSheets
– Usage 1: You can use the module in a very similar fashion as the Corona sprite module.
– Create a spriteSheet. The fix will look for a Lua file with the same name as the PNG file
– (e.g. sample.png sample.lua for iPod, sample@2.png and sample@2.lua for iPod4, etc)
– To create regular spriteSheets add a frameWidth and frameHeight pàrameters
local spriteSheet = spriteSheetFix.newSpriteSheet(“sample.png”);
– Create a spriteSet defining frames inside the spriteSheet
– (spriteSheet, startFrame, frameCount)
local spriteSet = spriteSheetFix.newSpriteSet(spriteSheet, 1, 21);
– Register a sequence by name defining frames inside the spriteSet
– (spriteSet, sequenceName, startFrame, frameCount, playTime, loop)
spriteSheetFix.add(spriteSet, “c1”, 1, 21, 700, true);
– Create the sprite and define the space to occupy in the base device (commonly ipod 340 x 480)
(spriteSet, rectWidth, rectHeight)
local sprite = spriteSheetFix.newSprite(spriteSet, 90, 56);
– Prepare the sequence to use
sprite:prepare(“c1”);
– Usage 2: You can set all parameters at once using the newSpriteAnimation() method.
– Here you define rectWidth, rectHeight, spriteSheet parameters,
– spriteSet parameters and sequence parameters.
– In a similar way as in Usage 1.
local sprite, spriteSheet = spriteSheetFix.newSpriteAnimation(
90,
56,
{
fileName = “cavando.png”,
},
{
startFrame = 1,
frameCount = 21,
},
{
name = “c1”,
startFrame = 1,
frameCount = 21,
playTime = 700,
loop = true,
}
);
– After this you can:
– play/pause the animation
sprite:play();
sprite:pause();
– get and set animation properties
sprite:isAnimating()
sprite:getCurrentFrame();
sprite:setCurrentFrame(3);
– read animation real size (not scaled and not frame size)
sprite:getAnimationWidth();
sprite:getAnimationHeight();
– to properly clean memory used by the sprite call
sprite:dispose();
– if you want to clean memory used by a spriteSheet disposeSpriteSheet()
– this will clear all sprites, spriteSets and sequences related to the
– spriteSheet.
spriteSheetFix.disposeSpriteSheet(spriteSheet);
– The fix uses an invalidation model to fix the spriteSheet position and scale.
– This models prevent running cpu-intensive calculations all the time.
– If you change the x, y, xScale, yScale, and rotation properties in the sprite
– or any of its ascendants you should call invalidate to forcce redrawing on next frame.
sprite:invalidate();
– Use drawNow() to draw immediately and not in the next frame (use sparingly)
sprite:drawNow();
– Use validateNow() to invalidate and draw immediately
– and not in the next frame (use sparingly)
sprite:drawNow();
Note:
The sprite returned is not really a sprite.
It is a group with an sprite inside and added some methods to access
sprite properties and methods.
To access the real sprite use the sprite property.
sprite.sprite.addEventListener(“sprite”, handler);
]]
module(…, package.seeall)
local sprite = require(“sprite”);
local listenerMap = {};
local spriteSheetDataMap = {};
local spriteSetDataMap = {};
suffix = nil;
–[[
Calculate a concatenated transfrom up to the stage object.
]]
local function getConcatenatedTransform(sprite)
local t = {x = 0, y = 0, xScale=1, yScale=1, rotation=0};
local obj = sprite;
repeat
t.x = t.x + obj.x;
t.y = t.y + obj.y;
t.xScale = t.xScale * obj.xScale;
t.yScale = t.yScale * obj.yScale;
t.rotation = t.rotation + obj.rotation;
obj = obj.parent;
until obj == nil
return t;
end
–[[
Transform a point using a transform object
]]
local function transformPoint(point, transform)
– Scale point
point.x = point.x * transform.xScale;
point.y = point.y * transform.yScale;
– Rotate point:
– First we find the length
local length = math.sqrt(point.x * point.x + point.y * point.y);
– Then we find the new angle
local rads = math.atan2(point.y, point.x);
rads = rads + (transform.rotation / 180 * math.pi);
– We re-calculate the point with the angle and length
point.x = math.cos(rads) * length;
point.y = math.sin(rads) * length;
return point;
end
–[[
Returns a frame in the spriteSheet given a spriteSet frame.
sequenceFrame: The frame in the spriteSet.
sequenceData: The sequence data to use to determine spriteSheet frame.
]]
local function getFrameInSpriteSet(sequenceFrame, sequenceData)
return (sequenceFrame - 1) + sequenceData.startFrame;
end
–[[
Returns a frame in the spriteSheet given a sprite frame.
sequenceFrame: The frame in the sequence.
sequenceData: The sequence data to use to determine spriteSheet frame.
spriteSetData: The spriteSet datato use to determine spriteSheet frame.
]]
local function getFrameInSpriteSheet(sequenceFrame, spriteSetData, sequenceData)
local frameInSpriteSet = getFrameInSpriteSet(sequenceFrame, sequenceData);
return (frameInSpriteSet - 1) + spriteSetData.startFrame;
end
–[[
Draws the sprite to fix its position everytime the sprite has been invalidated
or when a the currentFrame has changed
]]
local function draw(target, event)
– Uniform spriteSheets don’t do anything
if target.uniform then
return;
end
– Only if invalid
if target.isInvalid or (target.animating and target.previousFrame ~= target.currentFrame) then
– Reset location
target.x = 0;
target.y = 0;
– calculate concatenated transforms up to the stage
local transform = getConcatenatedTransform(target);
– cache important data
local spriteSheetData = target.spriteSheetData;
local sequenceData = target.sequenceData;
local spriteSetData = target.spriteSetData;
local frameInSpriteSheet = getFrameInSpriteSheet(target.currentFrame, spriteSetData, sequenceData);
local frameData = spriteSheetData.frames[frameInSpriteSheet];
– Untransformed Animation size
local animWidth = frameData.spriteSourceSize.width;
local animHeight = frameData.spriteSourceSize.height;
– Untransformed position
local xAnim = -(animWidth / 2);
local yAnim = -(animHeight / 2);
– frame position relative to untransformed position
local xFrame = frameData.spriteColorRect.x;
local yFrame = frameData.spriteColorRect.y;
– frame width (each frame has a different with)
local widthFrame = frameData.spriteColorRect.width;
local heightFrame = frameData.spriteColorRect.height;
– Frame center
local xFrameCenter = xAnim + xFrame + (widthFrame / 2);
local yFrameCenter = yAnim + yFrame + (heightFrame / 2);
– current frame “wrong” position
local pos = {x = - ((widthFrame / 2) + xFrame),
y=- ((heightFrame / 2) + yFrame)};
pos = transformPoint(pos, transform);
local xAnimReal = xFrameCenter + pos.x;
local yAnimReal = yFrameCenter + pos.y;
– “right” frame position
local fixedPos = transformPoint({x=-(animWidth / 2), y=-(animHeight / 2)}, transform);
fixedPos.x = fixedPos.x;
fixedPos.y = fixedPos.y;
– offset from “right” position to “wrong” position
local xOffset = fixedPos.x - xAnimReal;
local yOffset = fixedPos.y - yAnimReal;
– Fix position here
target.x = xOffset / target.parent.xScale;
target.y = yOffset / target.parent.yScale;
– Revert parent changes
local rads = math.atan2(target.y, target.x);
local length = math.sqrt(target.x * target.x + target.y * target.y);
target.x = math.cos(rads - (target.parent.rotation / 180 * math.pi)) * length;
target.y = math.sin(rads - (target.parent.rotation / 180 * math.pi)) * length;
– Release invalidation flags
target.isInvalid = false;
target.previousFrame = target.currentFrame;
end
end
–[[
This method is added to the returned sprite.
Tells the sprite to redraw on the next draw process
]]
local function invalidate(me)
me.sprite.isInvalid = true;
end
–[[
This method is added to the returned sprite.
Forces a redraw an the sprite whether invalidate()
has been called before or not.
]]
local function validateNow(me)
me:invalidate();
me.sprite:enterFrame();
end
–[[
This method is added to the returned sprite.
Forces a redraw an the sprite but only if invalidate()
has been called before
]]
local function drawNow(me)
me.sprite:enterFrame();
end
–[[
This method is added to the returned sprite.
Determines if the sprite is currently animating
]]
local function isAnimating(me)
return me.sprite.animating;
end
–[[
This method is added to the returned sprite.
Allows reading the current frame.
]]
local function getCurrentFrame(me)
return me.sprite.currentFrame;
end
–[[
This method is added to the returned sprite.
Allows setting the current frame.
]]
local function setCurrentFrame(me, value)
me.sprite.currentFrame = value;
me:invalidate();
end
–[[
This method is added to the returned sprite.
Pauses the currently running animation
]]
local function pause(me)
me.sprite:pause();
me:invalidate();
end
–[[
This method is added to the returned sprite.
Plays a sequence previously prepared by calling the prepare() method.
]]
local function play(me)
me.sprite:play();
me:invalidate();
end
–[[
This method is added to the returned sprite.
Prepares a named sequence to be used for animation.
The named sequence must be created before calling this method.
sequence: The name of the sequence to start playing.
]]
local function prepare(me, sequence)
local sprite = me.sprite;
local spriteSetData = sprite.spriteSetData;
sprite:prepare(sequence);
if not sprite.uniform then
local sequenceList = spriteSetData.sequences;
sprite.sequenceData = sequenceList[sequence];
Runtime:addEventListener(“enterFrame”, sprite);
sprite:enterFrame();
end
sprite.xScale = sprite.imageWidth / me:getAnimationWidth();
sprite.yScale = sprite.imageHeight / me:getAnimationHeight();
end
–[[
This method is added to the returned sprite.
Enabled proper garbage collection of the spriteSheet.
Call it always after you have finished working with it.
If you call disposeSpriteSheet function first all sprites
referencing the spritesheet will call this method automatically
so you don’t have to worry.
]]
local function dispose(me)
Runtime:removeEventListener(“enterFrame”, me.sprite);
end
–[[
This method is added to the returned sprite.
Allows getting the real animation width as defined by the Lua file, if
a non-uniform spriteSheet is used (Lua file), or defined by the frameWidth when
a uniform spriteSheet is used.
You should call this method on the sprite after calling the prepare method.
]]
local function getAnimationWidth(me)
if me.sprite.spriteSheetData.frames == nil then
return me.sprite.spriteSheetData.frameWidth;
elseif me.sprite.sequenceData then
local frameInSpriteSheet = getFrameInSpriteSheet(1, me.sprite.spriteSetData, me.sprite.sequenceData);
return me.sprite.spriteSheetData.frames[frameInSpriteSheet].spriteSourceSize.width;
end
return 0;
end
–[[
This method is added to the returned sprite.
Allows getting the real animation height as defined by the Lua file, if
a non-uniform spriteSheet is used (Lua file), or defined by the frameHeight when
a uniform spriteSheet is used.
You should call this method on the sprite after calling the prepare method.
]]
local function getAnimationHeight(me)
if me.sprite.spriteSheetData.frames == nil then
return me.sprite.spriteSheetData.frameHeight;
elseif me.sprite.sequenceData then
local frameInSpriteSheet = getFrameInSpriteSheet(1, me.sprite.spriteSetData, me.sprite.sequenceData);
return me.sprite.spriteSheetData.frames[frameInSpriteSheet].spriteSourceSize.height;
end
return 0;
end
–[[
Inserts a suffix into a table in the right ordered position
]]
local function insertSuffix(t, elem)
local pushed = false;
– Look for the right position and insert
for k,v in ipairs(t) do
if v.value > elem.value then
table.insert(t, k, elem);
pushed = true;
break;
end
end
if not pushed then
table.insert(t, elem);
end
end
–[[
Splits an URI into filePath and extensions
]]
local function splitUri(fileName)
local index = 0;
local savedIndex = nil;
– Look for the last point
repeat
savedIndex = index;
index = fileName:find(".", index + 1, true);
until index == nil
local parts = {};
– If point found
if savedIndex ~= 0 then
– Get the ext and fileName
parts.ext = fileName:sub(savedIndex);
parts.fileName = fileName:sub(1, savedIndex - 1);
else
– Otherwise set the whole string as the fileName
parts.fileName = fileName;
end
return parts;
end
–[[
Gets the Lua data for a sprite sheet using a name and a precalculated suffix
]]
local function getSpriteSheetData(fileName)
– Load the spritesheet data from lua
local sheetFile = require(fileName…suffix.key);
local sheetData = sheetFile.getSpriteSheetData();
return sheetData;
end
–[[
Initializes the fix with a resolution table to map
between a suffix and a resolution.
We need this function because of the lack of an Corona API
to access the configuration data.
t: The table typically reflects the table used in the config.lua file.
]]
function init(t)
local suffixList = {};
local vals = {};
local count = 1;
– Order suffixes by percentage value
for k,v in pairs(t) do
local suffixElem = {key = k, value = v};
insertSuffix(suffixList, suffixElem);
end
– Get the current scale
local xScale = 1 / display.contentScaleX;
– Get the suffix depending on scale
for k,v in ipairs(suffixList) do
if v.value > xScale then
if k > 1 then
suffix = suffixList[k - 1];
else
suffix = {key="", value=1};
end
break;
end
end
if suffix == nil then
suffix = suffixList[#suffixList];
end
end
–[[
Creates a new sprite sheet either uniform by setting the
frameWidth and frameHeight parameters or non-uniform by
setting only the fileName parameter.
fileName: Is the file name of the image to use as a spriteSheet.
If no other parameter is set it will also load a lua file
with the same name of the image file.
This method loads images and lua files by appending a suffix
depending on device resolution and rules loaded by init.
frameWidth: Is the width of each frame used for uniform spriteSheets.
frameHeight: Is the height of each frame used for uniform spriteSheets.
]]
function newSpriteSheet(fileName, frameWidth, frameHeight)
–Use always cached spriteSheets
for k,v in pairs(spriteSheetDataMap) do
if v.name == fileName then
return k;
end
end
– URI parts
local uri = splitUri(fileName);
local spriteSheet;
if uri.ext == nil then
uri.ext = “.png”
end
– The new image file name
local imageFileName = uri.fileName…suffix.key…uri.ext;
local sheetData;
– If the spriteSheet is uniform (defines frameWidth and frameHeight)
if type(frameWidth) == “number” and type(frameHeight) == “number” then
– Use frame width and height to create the spritesheet
frameWidth = math.floor((frameWidth * suffix.value) + 0.5);
frameHeight = math.floor((frameHeight * suffix.value) + 0.5);
spriteSheet = sprite.newSpriteSheet(imageFileName, frameWidth, frameHeight);
– Register spriteSheet data in the map to allow cache
sheetData = {
isUniform = true,
frameWidth=frameWidth,
frameHeight=frameHeight,
};
spriteSheetDataMap[spriteSheet] = sheetData;
else
– If non-uniform load the lua file first and then the image
sheetData = getSpriteSheetData(uri.fileName);
spriteSheet = sprite.newSpriteSheetFromData(imageFileName, sheetData);
– Register spriteSheet Lua data in the map
sheetData.isUniform = false;
spriteSheetDataMap[spriteSheet] = sheetData;
end
– Set the name of the spriteSheet
sheetData.name = fileName;
return spriteSheet;
end
–[[
Used instead of the spriteSheet:dispose() method to allow
proper garbage collection.
spriteSheet: The spriteSheet to dispose.
]]
function disposeSpriteSheet(spriteSheet)
local spriteSheetData = spriteSheetDataMap[spriteSheet];
– First dispose each sprite registered to the spritesheet
local sprites = spriteSheetData.sprites;
if sprites ~= nil then
for k,v in pairs(sprites) do
v:dispose();
end
end
– Then unregister spriteSets related to the spritesheet
local spriteSets = spriteSheetData.spriteSets;
if spriteSets ~= nil then
for k,v in pairs(spriteSets) do
spriteSetDataMap[v] = nil;
end
end
– Unregister the spritesheet
spriteSheetDataMap[spriteSheet] = nil;
– And finally dispose it
spriteSheet:dispose();
end
–[[
Creates a new spriteSet to group frames inside
the spriteSheet logically.
spriteSheet: The spriteSheet from which to extract the spriteSet.
startFrame: The frame of the spriteSheet where the spriteSet starts.
frameCount: Amount of frames to take from the spriteSheet.
]]
function newSpriteSet(spriteSheet, startFrame, frameCount)
local spriteSheetData = spriteSheetDataMap[spriteSheet];
local spriteSets = spriteSheetData.spriteSets;
– Use always the cached spriteSet
if spriteSets ~= nil then
for k,v in pairs(spriteSets) do
if v.startFrame == startFrame and v.frameCount == frameCount then
return k;
end
end
end
– If not cached create a new one
local spriteSet = sprite.newSpriteSet(spriteSheet, 1, 21);
– Register set with its data in the map
spriteSetDataMap[spriteSet] = {
startFrame = startFrame,
frameCount = frameCount,
spriteSheet = spriteSheet,
sequences = {},
};
– Register the sprite set in the specific spritesheet list
if spriteSets == nil then
spriteSets = {};
spriteSheetData.spriteSets = spriteSets;
end
table.insert(spriteSets, spriteSet);
return spriteSet;
end
–[[
Registers a sequence by name. Sequences are attached to spriteSets.
Existing sequences with the same name are replaced.
spriteSet: The sprite used to extract the sequence.
sequenceName: The name of the equence.
startFrame: The frame of the spriteSet where the sequence starts.
frameCount: Amount of frames to take from the sequence.
playTime: Milliseconds the animation will play.
loop: Determines whether to loop forever (true) or a fixed
amount of times if a numeric value is sent.
]]
function add(spriteSet, sequenceName, startFrame, frameCount, playTime, loop)
if loop == nil then
loop = false;
end
– Register the sequence with Corona SDK
sprite.add(spriteSet, sequenceName, startFrame, frameCount, playTime, loop);
– Register using our own data to make the fix work
– We never use cached data here, we just replace the existing sequence
spriteSetDataMap[spriteSet].sequences[sequenceName] = {
startFrame = startFrame,
frameCount = frameCount,
playTime = playTime,
loop = loop,
};
end
–[[
Creates a wrapper group around the sprite to overcome all current issues.
spriteSet: The spriteSet to use for the animation.
You should select the sequence using the
sprite:prepare() method.
imageWidth: The width into which the sprite will fit.
imageHeight: The height into whoch the sprite will fit.
]]
function newSprite(spriteSet, imageWidth, imageHeight)
local container = display.newGroup();
local spriteInstance = sprite.newSprite(spriteSet);
– Add new methods to the group to treat it as if it is a Sprite
container.isAnimating = isAnimating;
container.getCurrentFrame = getCurrentFrame;
container.setCurrentFrame = setCurrentFrame;
container.pause = pause;
container.play = play;
container.prepare = prepare;
container.getAnimationWidth = getAnimationWidth;
container.getAnimationHeight = getAnimationHeight;
container.dispose = dispose;
– Add invalidation methods
container.invalidate = invalidate;
container.drawNow = drawNow;
container.validateNow = validateNow;
– Check invalidation and calculate position each frame
spriteInstance.enterFrame = draw;
spriteInstance.isInvalid = true;
– Save original sprite and spriteSet
container.sprite = spriteInstance;
– Cache spriteSet, spriteSheet and imageWidth and imageHeight data into the sprite
local spriteSetData = spriteSetDataMap[spriteSet];
local spriteSheetData = spriteSheetDataMap[spriteSetData.spriteSheet];
spriteInstance.uniform = spriteSheetData.isUniform;
spriteInstance.spriteSetData = spriteSetData;
spriteInstance.spriteSheetData = spriteSheetData;
spriteInstance.imageWidth = imageWidth;
spriteInstance.imageHeight = imageHeight;
– Register the sprite to allow proper removal when disposing spriteSheets
if spriteSheetData.sprites == nil then
spriteSheetData.sprites = {};
end
table.insert(spriteSheetData.sprites, container);
container:insert(spriteInstance);
return container;
end
–[[
Convenience function to allow creating an spriteSheet, spriteSet, sequence and
sprite all in one step. It the image widht and height plus three tables with configuration data.
imageWidth: The width into which the sprite will fit.
imageHeight: The height into whoch the sprite will fit.
fileData: A table defining image file properties:
fileName: Is the file name of the image to use as a spriteSheet.
If no other parameter is set it will also load a lua file
with the same name of the image file.
This method loads images and lua files by appending a suffix
depending on device resolution and rules loaded by init.
frameWidth: Is the width of each frame used for uniform spriteSheets.
frameHeight: Is the height of each frame used for uniform spriteSheets.
spriteSetData: A table defining spriteSet information:
startFrame: The frame of the spriteSheet where the spriteSet starts.
frameCount: Amount of frames to take from the spriteSheet.
sequenceData: A table defining sequence information:
name: The name of the equence.
startFrame: The frame of the spriteSet where the sequence starts.
frameCount: Amount of frames to take from the sequence.
playTime: Milliseconds the animation will play.
loop: Determines whether to loop forever (true) or a fixed
amount of times if a numeric value is sent.
return the sprite and the spriteSheet created to enabled spriteSheet disposing
]]
function newSpriteAnimation(imageWidth, imageHeight, fileData, spriteSetData, sequenceData)
– Create the spritesheet
local spriteSheet = newSpriteSheet(
fileData.fileName,
fileData.frameWidth,
fileData.frameHeight
);
– Create the spriteset
local spriteSet = newSpriteSet(
spriteSheet,
spriteSetData.startFrame,
spriteSetData.frameCount
);
– Register the sequence by name
add(
spriteSet,
sequenceData.name,
sequenceData.startFrame,
sequenceData.frameCount,
sequenceData.playTime,
sequenceData.loop
);
– create the sprite and prepare the animation
local sprite = newSprite(spriteSet, imageWidth, imageHeight);
sprite:prepare(sequenceData.name);
return sprite, spriteSheet;
end[/lua] [import]uid: 46260 topic_id: 4284 reply_id: 33087[/import]