image-swapSometimes, the design of an app dictates the need for “image swapping,” in which the developer needs to draw an image at a given location and then, after some time or some action, swap that image for another. Here’s a theoretical example:

local myImage = display.newImageRect( "image1.png", 64, 64 )
-- After some time or on some event...
myImage:swapImage( "image2.png" )

But this is not how Corona SDK works as :swapImage() is not a built-in Corona method. When Corona builds an image, it has to read the file in from the app bundle’s file system, allocate texture memory for the image, render the PNG or JPG compressed data into four color channels, and return a display object to you to work with. To “swap” that image, Corona would need to destroy the object in question and create a new image. It’s the same amount of work required in this example:

local myImage = display.newImageRect( "image1.png", 64, 64 )
-- After some time or on some event...
myImage:removeSelf()
myImage = nil
myImage = display.newImageRect( "image2.png", 64, 64 )

In this scenario, you’re probably not planning on using the first image again soon, since the original image was destroyed. The advantage to this method is that it minimizes texture memory, since only one of the two images is in memory at once. However, if you want to get the original image back, you’re caught in this cycle of image loading and unloading over and over. In itself, this is an inefficient process that can impact performance. Thus, if you need to swap images more frequently, you should explore other techniques. Let’s look at a few options.

The 2-Image Swap

If you have two images, you can simply load them both, add them to a display group, and reference the images as parameters of the group. In this example we will use a typical touch handler and swap the image in the “began” phase of touch handler:

-- Create a basic display group
local ballGroup = display.newGroup()
-- Create a red ball inside the group
local ballGroup = display.newGroup()
local redBall = display.newImageRect( ballGroup, "images/red-ball.png", 128, 128 )
redBall.x = display.contentCenterX
redBall.y = display.contentCenterY
-- Create a blue ball inside the same group
local blueBall = display.newImageRect( ballGroup, "images/blue-ball.png", 128, 128 )
blueBall.x = display.contentCenterX
blueBall.y = display.contentCenterY
-- Make the blue ball invisible
blueBall.isVisible = false
-- Create an attribute of the group, set as the visible (red) ball
ballGroup.whichBall = "red"

In this case, we load two images: redBall and blueBall. We position them at the same location, making one visible and the other not. We also set an attribute on the group (whichBall) so we know which is the visible image. Now for the touch handler:

local function handleTouch( event )
   local t = event.target 
   local phase = event.phase
   if ( phase == "began" ) then
      if ( ballGroup.whichBall == "red" ) then
         redBall.isVisible = false
         blueBall.isVisible = true
         ballGroup.whichBall = "blue"
      else
         redBall.isVisible = true
         blueBall.isVisible = false
         ballGroup.whichBall = "red"
      end               
      local parent = t.parent
      parent:insert( t )
      display.getCurrentStage():setFocus( t )
      t.isFocus = true
      t.x0 = event.x - t.x
      t.y0 = event.y - t.y
   elseif ( t.isFocus ) then
      if ( "moved" == phase ) then
         -- Make object move (we subtract t.x0,t.y0 so that moves are relative to initial grab point, rather than object "snapping").
         t.x = event.x - t.x0
         t.y = event.y - t.y0
      elseif ( "ended" == phase or "cancelled" == phase ) then
         display.getCurrentStage():setFocus( nil )
         t.isFocus = false
      end
   end

   -- Important to return true. This tells the system that the event should not be propagated to listeners of any objects underneath.
   return true
end
ballGroup:addEventListener( "touch", handleTouch )

Let’s focus in on the swapping code in the began phase:

if ( ballGroup.whichBall == "red" ) then
   redBall.isVisible = false
   blueBall.isVisible = true
   ballGroup.whichBall = "blue"
else
   redBall.isVisible = true
   blueBall.isVisible = false
   ballGroup.whichBall = "red"
end

If the touch begins, and the ball is red, we make that image invisible, make the blue ball visible, and note which is the “current” image. Because the move code is on the group, not the individual images, the images move together as a unit.

Graphics 2.0 Fill Swap

If you’re a Pro or Enterprise subscriber, you can take advantage of the Graphics 2.0 fill methods to swap images:

local image1 = { type="image", filename="images/red-ball.png" }
local image2 = { type="image", filename="images/blue-ball.png" }

local ball = display.newRect( 0, 0, 128, 128 )
ball.x = display.contentCenterX
ball.y = display.contentCenterY
ball.fill = image1
ball.whichBall = "red"

This method eliminates the need for the display group — just create the base image (in this case a rectangle the size of our images), position it, and fill it with the red image “paint.” Now, in the touch handler, which is now on the object itself since we eliminated the group, the code is a bit simpler. Just remember that t in this example is the event.target or the ball object.

if ( t.whichBall == "red" ) then
   t.fill = image2
   t.whichBall = "blue"
else
   t.fill = image1
   t.whichBall = "red"
end

Multi-Image Swap

Sometimes you’ll want to swap more than two images. For a game like Candy Crush or Bejeweled, you may want to have several images that occupy a spot on the screen and quickly change among them. In this case, we can go back to the group model, load all of the images into an array, and access them via their index number:

local ballGroup = display.newGroup()
local balls = {}
local ballImages = {
   "images/red-ball.png",
   "images/blue-ball.png",
   "images/green-ball.png",
   "images/yellow-ball.png",
   "images/pink-ball.png"
   }
for i = 1, #ballImages do
   balls[i] = display.newImageRect( ballGroup, ballImages[i], 128, 128 )
   balls[i].x = display.contentCenterX
   balls[i].y = display.contentCenterY
   balls[i].isVisible = false
end
ballGroup.currentBall = 1
balls[ballGroup.currentBall].isVisible = true

Here, we iterate through the ballImages table to create the display objects, storing them in the balls table as we go along, and make each one invisible. Then, we make the first one “current” and visible again. Now, to manage the swapping in a cyclical manner, we can execute this code:

local idx = t.currentBall
balls[idx].isVisible = false
idx = idx + 1
if ( idx > #balls ) then
   idx = 1
end
balls[idx].isVisible = true
t.currentBall = idx

While this method is perfectly valid, using individual image files is not the best use of memory and load time. This is why we encourage the use of image sheets, in which you load a single image “sheet” containing all of the individual images, and load them from that sheet. Image sheets take a little more initial setup, but the the benefits are well worth it. Converting the above code to image sheets may look as follows:

local ballGroup = display.newGroup()
local options = {
   width = 128,
   height = 128,
   numFrames = 8,
   sheetContentWidth = 512,
   sheetContentHeight = 256
}
local ballSheet = graphics.newImageSheet( "images/balls.png", options )
local balls = {}
for i = 1, 8 do
   balls[i] = display.newImageRect( ballSheet, i, 128, 128 )
   balls[i].x = display.contentCenterX
   balls[i].y = display.contentCenterY
   ballGroup:insert( balls[i] )
   balls[i].isVisible = false
end
ballGroup.currentBall = 1
balls[ballGroup.currentBall].isVisible = true

From a code perspective, this is not much different from the array method above, other than the efficiencies gained from using image sheets — but image sheets are a gateway to an excellent way swap images: sprites.

Sprites

Corona features a comprehensive sprite engine. While the name “sprite” may seem to indicate only an animated character or object in a game, you should consider it as simply a series of images which can be used for multiple purposes, including swapping images. Let’s look at the sprite-based version which builds upon the image sheet version above:

local options = {
   width = 128,
   height = 128,
   numFrames = 8,
   sheetContentWidth = 512,
   sheetContentHeight = 256
}
local ballSheet = graphics.newImageSheet( "images/balls.png", options )

local sequenceData = {
   name = "balls",
   start = 1,
   count = 8,
}

local balls = display.newSprite( ballSheet, sequenceData )
balls:setSequence( "balls" )
balls.currentBall = 1
balls:setFrame( balls.currentBall )
balls.x = display.contentCenterX
balls.y = display.contentCenterY

Like the version using Graphics 2.0 fills, we no longer need the display group since the sprite is, by definition, a multi-frame object than can reside in any group by itself. Once the image sheet is set up and the sequence defined (all eight frames), simply call display.newSprite() with the image sheet and the sequence data. Next, use the :setFrame() method to pick which frame to display and the sprite engine handles the rest — no need to make other images invisible as in some of the other examples.

local idx = t.currentBall
idx = idx + 1
if idx > 8 then
    idx = 1
end
t.currentBall = idx
t:setFrame(idx)

In Summary

As you can see, there are various approaches and methods to the “swap images” concept, and it depends on your needs and design specifics as to which method is most suitable. To experiment with the concepts discussed in this tutorial, please download the sample project and get started.

    • Sprites were engineered to be and efficient means of swapping images. I would say the fill method is probably quick enough for smaller images, but you probably wouldn’t want to try and replicate sprite animations with fill though.

    • I’m not sure how to use a transition with the G2.0 fill method or the sprite method, but for the others instead of setting .isVisibile to true or falls, you would use two transitions:

      transition.to(balls[idx], {time=250, alpha=0})
      idx = idx + 1
      if ( idx > #balls ) then
      idx = 1
      end
      transition.to(balls[idx], {time=250, alpha=1})

      Of course the objects that are not showing would need their alpha to be initially set to 0 and the one showing to 1.

      Rob

  1. Hi Rob,

    There’s a standard way of cycling through things, which should be mentioned here as many programmers are not aware of the modulus operation and it’s uses.

    i=(i+1) % n, if you have the array beginning with 0, which is better for this reason
    or
    i=i+1%n, if you have the array starting at index 1.

    Sad thing is, lua starts it’s arrays at 1 by default, so you normally have to use the latter alternative which is a pain in my eyes..

  2. Hi Rob, I’m a beginner in lua because of our thesis project.. I’m having a problem in swapping scenes but if I use only 2 different scene it’s working but when I add more, it won’t swap.. here’s the code I’m using…

    local sheetData1 = { width=960, height=720, numFrames=4, sheetContentWidth=2048, sheetContentHeight=2048 }
    local sheet1 = graphics.newImageSheet( “spritesheet.png”, sheetData1 )

    — 2nd image sheet
    local sheetData2 = { width=960, height=720, numFrames=4, sheetContentWidth=1926, sheetContentHeight=1446 }
    local sheet2 = graphics.newImageSheet( “spritesheet2.png”, sheetData2 )

    local sequenceData = {
    { name=”seq1″, sheet=sheet1, start=1, count=4, time=2000, loopCount=0 },
    { name=”seq2″, sheet=sheet2, start=1, count=4, time=2000, loopCount=0 }
    }

    local myAnimation = display.newSprite( sheet1, sequenceData )
    myAnimation.x = display.contentWidth/2 ; myAnimation.y = display.contentHeight/2
    myAnimation:play()

    — swap the sequence to ‘seq2′ which uses the second image sheet
    local function swapSheet()
    myAnimation:setSequence( “seq2″ )
    myAnimation:play()
    end
    timer.performWithDelay( 2000, swapSheet )

    I don’t know where to put the 3rd spritesheet.. hope to get an answer Thank you! :)

  3. I have a probleme with swapImage:
    local yolo=display.newImage(“yolo0.png”,0,0)
    yolo:swapImage( “yolo1.png” )

    Runtime error
    c:\users\er\documents\corona projects\3 kings\constructanim.lua:139: attempt to call method ‘swapImage’ (a nil value)
    stack traceback:
    c:\users\er\documents\corona projects\3 kings\constructanim.lua:139: in function
    ?: in function [Finished in 17.3s]
    It’s make a long time i have this probleme, i use actualy .fill but the problem with “fill” it’s resize my image and it’s take quite a long time to do the fill for the mobile phone.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>