Chapter 5 — Converting the Game to Composer

Previous | Next

In the last chapter, we learned the basics of scene management. In this chapter, we are going to convert the original game file, now main_original.lua, into our game.lua scene.

Scene Structure

As you learned in the previous chapter, there are dedicated places in a Composer-enabled Lua file to put different aspects of your program. In our original version, we had the luxury of writing our code in a linear order, for example creating an object, positioning it on the screen, potentially adding a physical body, linking up event listeners, and then moving on to the next item.

Composer requires us to think a little differently. Accessibility becomes more important and this means that things must be coded in a more strict order. Using Composer properly requires that you consider the scene life cycle functions — scene:create(), scene:show(), scene:hide(), and scene:destroy() and that you run commands depending on whether the scene is on screen or still being assembled off screen. For example, we previously used commands that would start the continual creation of asteroids and put them in motion. In our modified game.lua scene, these commands will be deferred until the scene is fully on screen.

Accessible Code

First, make a copy of the standard scene-template.lua file, included with this chapter's source files, just like you did in the previous chapter before creating the menu scene. Rename this copy to game.lua and place it within your StarExplorer project folder.

Now open both main_original.lua and game.lua in separate editor windows/tabs. You will be moving several blocks of code from main_original.lua to game.lua, so it will be convenient to have both files open simultaneously.

Physics Setup

Since this game will obviously still utilize the physics engine, there's no reason to defer that setup until later. Copy your physics setup commands from main_original.lua and paste them into the scene-accessible space of game.lua, immediately following initialization of the scene:

-- -----------------------------------------------------------------------------------
-- Code outside of the scene event functions below will only be executed ONCE unless
-- the scene is removed entirely (not recycled) via "composer.removeScene()"
-- -----------------------------------------------------------------------------------

local physics = require( "physics" )
physics.start()
physics.setGravity( 0, 0 )
Note

Following this, we originally added the math.randomseed() command to set the "seed" for the pseudo-random number generator. However, you may recall that we re-stated this command in our modified main.lua file, so there's no reason to copy it over to game.lua.

Image Sheet

Next we'll need the image sheet configuration. Let's paste that just below the physics commands:

-- Configure image sheet
local sheetOptions =
{
    frames =
    {
        {   -- 1) asteroid 1
            x = 0,
            y = 0,
            width = 102,
            height = 85
        },
        {   -- 2) asteroid 2
            x = 0,
            y = 85,
            width = 90,
            height = 83
        },
        {   -- 3) asteroid 3
            x = 0,
            y = 168,
            width = 100,
            height = 97
        },
        {   -- 4) ship
            x = 0,
            y = 265,
            width = 98,
            height = 79
        },
        {   -- 5) laser
            x = 98,
            y = 265,
            width = 14,
            height = 40
        },
    }
}
local objectSheet = graphics.newImageSheet( "gameObjects.png", sheetOptions )

Initial Variables

Following the image sheet setup, paste in the entire block of initial variables from main_original.lua:

-- Initialize variables
local lives = 3
local score = 0
local died = false

local asteroidsTable = {}

local ship
local gameLoopTimer
local livesText
local scoreText

Display Groups

In our original version, we created three display groups for sorting and layering our game objects: backGroup, mainGroup and uiGroup. We are still going to use them, but a few small modifications will be necessary since we're using Composer.

In the last chapter, you learned how to insert scene objects into the scene's view group (sceneGroup). An important concept to understand at this point is that display groups can actually be inserted into other display groups! As such, we can maintain the three display groups from the original game and instill them into the scene's view group.

To facilitate this, we will defer the actual creation of our three groups until we create the scene. However, we still need to define the variables now using the forward declaration method that you learned about in chapter 2. So, instead of associating each variable with a display.newGroup(), just leave them undefined for the moment:

local backGroup
local mainGroup
local uiGroup
Notes
  • In our original version, we created the background, ship, lives text, and score text immediately following initialization of the display groups. In this new Composer-enabled version, we will defer these actions until later, inside the scene:create() function.

  • We added the display.setStatusBar( display.HiddenStatusBar ) command to our modified main.lua file to ensure that the status bar remains hidden throughout the app, so there's no reason to repeat it in game.lua.

Game Functions

As we continue past this point in main_original.lua, we come to the local functions which power our game's core functionality. Basically, these can be copied over directly into your game.lua file, pasted directly below the variables we just defined.

local function updateText()
    livesText.text = "Lives: " .. lives
    scoreText.text = "Score: " .. score
end


local function createAsteroid()

    local newAsteroid = display.newImageRect( mainGroup, objectSheet, 1, 102, 85 )
    table.insert( asteroidsTable, newAsteroid )
    physics.addBody( newAsteroid, "dynamic", { radius=40, bounce=0.8 } )
    newAsteroid.myName = "asteroid"

    local whereFrom = math.random( 3 )

    if ( whereFrom == 1 ) then
        -- From the left
        newAsteroid.x = -60
        newAsteroid.y = math.random( 500 )
        newAsteroid:setLinearVelocity( math.random( 40,120 ), math.random( 20,60 ) )
    elseif ( whereFrom == 2 ) then
        -- From the top
        newAsteroid.x = math.random( display.contentWidth )
        newAsteroid.y = -60
        newAsteroid:setLinearVelocity( math.random( -40,40 ), math.random( 40,120 ) )
    elseif ( whereFrom == 3 ) then
        -- From the right
        newAsteroid.x = display.contentWidth + 60
        newAsteroid.y = math.random( 500 )
        newAsteroid:setLinearVelocity( math.random( -120,-40 ), math.random( 20,60 ) )
    end

    newAsteroid:applyTorque( math.random( -6,6 ) )
end
Important

The next function (fireLaser()) should also follow, but notice the command immediately following it:

ship:addEventListener( "tap", fireLaser )

This creates the event listener function for ship, but we haven't created ship yet! Thus, we must defer this command until later, following creation of the actual ship object.

As you copy and paste in the next several functions, skip the commands which immediately follow them. Specifically, omit the following lines when you're copying over code from main_original.lua to game.lua:

  • ship:addEventListener( "tap", fireLaser )
  • ship:addEventListener( "touch", dragShip )
  • gameLoopTimer = timer.performWithDelay( 500, gameLoop, 0 )
  • Runtime:addEventListener( "collision", onCollision )

Essentially, the remainder of your scene-accessible space should be populated as follows:

local function fireLaser()

    local newLaser = display.newImageRect( mainGroup, objectSheet, 5, 14, 40 )
    physics.addBody( newLaser, "dynamic", { isSensor=true } )
    newLaser.isBullet = true
    newLaser.myName = "laser"

    newLaser.x = ship.x
    newLaser.y = ship.y
    newLaser:toBack()

    transition.to( newLaser, { y=-40, time=500,
        onComplete = function() display.remove( newLaser ) end
    } )
end


local function dragShip( event )

    local ship = event.target
    local phase = event.phase

    if ( "began" == phase ) then
        -- Set touch focus on the ship
        display.currentStage:setFocus( ship )
        -- Store initial offset position
        ship.touchOffsetX = event.x - ship.x

    elseif ( "moved" == phase ) then
        -- Move the ship to the new touch position
        ship.x = event.x - ship.touchOffsetX

    elseif ( "ended" == phase or "cancelled" == phase ) then
        -- Release touch focus on the ship
        display.currentStage:setFocus( nil )
    end

    return true  -- Prevents touch propagation to underlying objects
end


local function gameLoop()

    -- Create new asteroid
    createAsteroid()

    -- Remove asteroids which have drifted off screen
    for i = #asteroidsTable, 1, -1 do
        local thisAsteroid = asteroidsTable[i]

        if ( thisAsteroid.x < -100 or
             thisAsteroid.x > display.contentWidth + 100 or
             thisAsteroid.y < -100 or
             thisAsteroid.y > display.contentHeight + 100 )
        then
            display.remove( thisAsteroid )
            table.remove( asteroidsTable, i )
        end
    end
end


local function restoreShip()

    ship.isBodyActive = false
    ship:setLinearVelocity( 0, 0 )
    ship.x = display.contentCenterX
    ship.y = display.contentHeight - 100

    -- Fade in the ship
    transition.to( ship, { alpha=1, time=4000,
        onComplete = function()
            ship.isBodyActive = true
            died = false
        end
    } )
end


local function onCollision( event )

    if ( event.phase == "began" ) then

        local obj1 = event.object1
        local obj2 = event.object2

        if ( ( obj1.myName == "laser" and obj2.myName == "asteroid" ) or
             ( obj1.myName == "asteroid" and obj2.myName == "laser" ) )
        then
            -- Remove both the laser and asteroid
            display.remove( obj1 )
            display.remove( obj2 )

            for i = #asteroidsTable, 1, -1 do
                if ( asteroidsTable[i] == obj1 or asteroidsTable[i] == obj2 ) then
                    table.remove( asteroidsTable, i )
                    break
                end
            end

            -- Increase score
            score = score + 100
            scoreText.text = "Score: " .. score

        elseif ( ( obj1.myName == "ship" and obj2.myName == "asteroid" ) or
                 ( obj1.myName == "asteroid" and obj2.myName == "ship" ) )
        then
            if ( died == false ) then
                died = true

                -- Update lives
                lives = lives - 1
                livesText.text = "Lives: " .. lives

                if ( lives == 0 ) then
                    display.remove( ship )
                else
                    ship.alpha = 0
                    timer.performWithDelay( 1000, restoreShip )
                end
            end
        end
    end
end


-- -----------------------------------------------------------------------------------
-- Scene event functions
-- -----------------------------------------------------------------------------------

Great job! We now have all of the scene-accessible functions copied over. In the next few sections, we'll illustrate how the Composer scene: functions tie our game together.

Creating the Scene

Now we need to actually create the scene. For the menu scene in the previous chapter, we created the background, title, and two buttons within scene:create(). For this scene, we'll create the background, ship, and the two text objects. In addition, we'll pause the physics engine and actually create the three display groups required for layering our game objects.

Let's start with physics. Inside the scene:create() function, add the command physics.pause() as follows:

function scene:create( event )

    local sceneGroup = self.view
    -- Code here runs when the scene is first created but has not yet appeared on screen

    physics.pause()  -- Temporarily pause the physics engine
end

What is the purpose of this command at this point in the scene's life cycle? Good question! Remember that our game scene isn't truly on screen at this point and, because we don't want the game to start quite yet, we'll immediately pause the physics engine. This allows us to create objects, assign their physical bodies, and position them, but they won't be affected physically until we re-start the physics engine.

Next we need to create the three display groups for which we previously defined forward references. Add the following lines after the command you just added:

    -- Set up display groups
    backGroup = display.newGroup()  -- Display group for the background image
    sceneGroup:insert( backGroup )  -- Insert into the scene's view group

    mainGroup = display.newGroup()  -- Display group for the ship, asteroids, lasers, etc.
    sceneGroup:insert( mainGroup )  -- Insert into the scene's view group

    uiGroup = display.newGroup()    -- Display group for UI objects like the score
    sceneGroup:insert( uiGroup )    -- Insert into the scene's view group

Here, in addition to creating the groups, we also insert each group into the Composer scene's view group using sceneGroup:insert(). This is how we instill our original display groups into the scene.

As you recall, most Corona display object APIs accept a valid display group variable as a convenient inline shortcut for inserting the object into that group. However, display.newGroup() is one of the exceptions to this shortcut and you can't simply supply an inline group reference to insert the new group into that existing group. Instead, you must use the object:insert() command. This yields the same result as the inline method and can be used interchangeably with other display object APIs if you prefer.

With the groups in place, let's first create the background:

    -- Load the background
    local background = display.newImageRect( backGroup, "background.png", 800, 1400 )
    background.x = display.contentCenterX
    background.y = display.contentCenterY

If you inspect the scene-accessible code area near the top of game.lua, you'll notice that we did not include a forward reference to background via local background. This is because, once we create and insert the background object into the scene's view, we'll never need to access it elsewhere in the code. Thus, we simply create it as a local object inside the scene:create() function.

Now let's create the ship and text objects:

    ship = display.newImageRect( mainGroup, objectSheet, 4, 98, 79 )
    ship.x = display.contentCenterX
    ship.y = display.contentHeight - 100
    physics.addBody( ship, { radius=30, isSensor=true } )
    ship.myName = "ship"

    -- Display lives and score
    livesText = display.newText( uiGroup, "Lives: " .. lives, 200, 80, native.systemFont, 36 )
    scoreText = display.newText( uiGroup, "Score: " .. score, 400, 80, native.systemFont, 36 )

Unlike the background, we create these three objects using variable references that we already included earlier in the scene-accessible code section. This is because other functions will need to know about these objects as the game runs.

Remember how we deferred adding the ship's event listeners in the scene-accessible section because ship didn't yet exist as an actual object? Now that it does exist, let's add its "tap" and "touch" event listeners:

    ship:addEventListener( "tap", fireLaser )
    ship:addEventListener( "touch", dragShip )

That's it! Our initial scene objects will now be created — albeit off screen — immediately before Composer proceeds to the next function in the scene's life cycle, scene:show().

Showing the Scene

In the menu scene, we didn't need to do anything with scene:show(), nor with its companion function scene:hide(), but in this game scene we do. Remember that, at this point, we have not yet enabled collision detection, re-started the physics engine, or started our game loop to spawn asteroids. Fortunately, scene:show() can be used to put everything in motion!

Phases and Transitions

An important difference between scene:create() and scene:show() is that Composer calls the scene:show() function twice. The first call occurs when the scene is ready to be shown and the second call occurs immediately after the scene has shown. While we didn't utilize it for the menu scene, the composer.gotoScene() call allows you to specify scene transition effects such as fading in, cross-fading, zooming in and out, and more. Naturally, there is a time duration associated with the beginning and end of such transitions and this is where the two calls come into play — the first occurs before the transition begins and the second occurs when the transition finishes.

Of course, we need to know when each of these calls occurs. This can be done by checking event.phase, a property associated with the event parameter passed to scene:show(). For the first call made to scene:show(), event.phase is "will", essentially meaning that the scene "will show" and the transition effect is about to occur. For the second call made to scene:show(), event.phase is "did", meaning the scene "did show" and the transition effect has finished.

The scene template already has conditional code in place to test which phase of scene:show() we're in, so you can paste code from your main_original.lua file into the proper conditional block. For this game, we basically want to start the game running — spawning asteroids, detecting collisions, etc. — once the scene is fully on screen, meaning the "did" phase. So, in your game.lua file, within the scene:show() function, add three commands:

function scene:show( event )

    local sceneGroup = self.view
    local phase = event.phase

    if ( phase == "will" ) then
        -- Code here runs when the scene is still off screen (but is about to come on screen)

    elseif ( phase == "did" ) then
        -- Code here runs when the scene is entirely on screen
        physics.start()
        Runtime:addEventListener( "collision", onCollision )
        gameLoopTimer = timer.performWithDelay( 500, gameLoop, 0 )
    end
end

Essentially, inside the elseif ( phase == "did" ) then conditional block, you'll notice that we:

  1. Re-start the physics engine with physics.start().
  2. Start collision detection.
  3. Start the game loop with our original timer.performWithDelay() command.
Action!

Let's check the result of our code! Save your modified game.lua file and then relaunch the Simulator. As expected, you'll be presented with the menu screen, but now we can actually proceed. Tap/click the Play button and, assuming you did everything up to this point correctly, Composer should proceed to the game.lua scene which plays identically to our original version of the game.

Hiding the Scene

Currently there is a flaw in the game. When we run out of lives, the asteroids will continue to build up and there is no way to restart the game. This means that we need to adapt our game.lua code so that, when the player runs out of lives, Composer proceeds to the high scores scene. However, since we haven't yet created the highscores.lua file, let's update game.lua to simply return to the menu scene instead.

When we intend to leave the game scene, remember that the gameLoopTimer timer will still be running. In addition, the physics engine will still be moving asteroids about. Finally, asteroids will continue to spawn, but clearly we need to stop that. All of these things can be handled inside the scene:hide() function.

Similar to scene:show(), scene:hide() will be called twice, once when the scene is about to be hidden and again after the scene is fully off screen. This time, in the "will" and "did" phase conditions of scene:hide(), we'll "undo" some things by adding three commands:

function scene:hide( event )

    local sceneGroup = self.view
    local phase = event.phase

    if ( phase == "will" ) then
        -- Code here runs when the scene is on screen (but is about to go off screen)
        timer.cancel( gameLoopTimer )

    elseif ( phase == "did" ) then
        -- Code here runs immediately after the scene goes entirely off screen
        Runtime:removeEventListener( "collision", onCollision )
        physics.pause()
    end
end

These commands essentially reverse what we did in the scene:show() function:

  1. Stop the game loop by canceling the timer associated with gameLoopTimer.
  2. Stop collision detection by removing the runtime event listener.
  3. Pause the physics engine with physics.pause().
Note

Observe how we're intentionally using both phases of scene:hide(). The first command can occur before the scene begins to hide (phase == "will") since we don't need to generate new asteroids once the game is over. For the other two commands, we defer them until after the scene is fully off screen because we don't want the remaining asteroids to suddenly stop moving as the scene transition begins.

Scene Cleanup

Hopefully you'll want to play the game again. By default, Composer keeps scenes in memory to save processing power when the scene is revisited. So, even though it's hidden at this point, your game scene remains basically as you left it. If you play again, it will come back into view, new asteroids will be created and begin moving, but the previous asteroids will be there too. Your previous score will also still show and lives will remain at zero. Most importantly, the ship won't be showing. This obviously isn't practical!

Depending on your game, cleaning up a scene to restart fresh can involve some work. In this case, we would need to "undo" some things we did in scene:create() as well as remove the references to old asteroids contained in the asteroidsTable table. We would also need to reset score, lives, and the ship's visibility within scene:show(). None of this is exceptionally complicated, but wouldn't it be convenient if there was a simple way to reset everything in a scene? Fortunately, Composer has a command to do exactly that:

composer.removeScene( "game" )

Essentially, this command removes and destroys the game.lua scene as if it never existed. By doing so, you lose the caching benefits mentioned above, but for most scenes, the efficiency gained isn't worth the programming effort to reset everything.

Although this command is convenient, you can not remove the scene you are currently in. For our game, it's up to menu.lua to do this task, so let's open that file and make a slight modification to it. Find the function gotoGame() and, immediately before you call composer.gotoScene(), add the composer.removeScene( "game" ) command. In addition, add a "crossFade" transition effect to the composer.gotoScene() line following it.

local function gotoGame()
    composer.removeScene( "game" )
    composer.gotoScene( "game", { time=800, effect="crossFade" } )
end

Although we haven't yet written it, the highscores.lua scene will likely change each time as well, so removing it in a similar manner will be helpful. While we have menu.lua open, let's modify the gotoHighScores() function immediately following gotoGame():

local function gotoHighScores()
    composer.removeScene( "highscores" )
    composer.gotoScene( "highscores", { time=800, effect="crossFade" } )
end
Action!

Time to test our changes! Save your modified menu.lua and game.lua files and then relaunch the Simulator. Now you should be able to play the game repeatedly and get a clean reset each time.

Chapter Concepts

Here's a summary of the concepts we covered in this chapter:

Command/Property Description
physics.pause() Pauses the physics engine.
object:insert() Inserts an object into a group.
timer.cancel() Cancels a timer operation initiated with timer.performWithDelay().
object:removeEventListener() Removes an event listener from an object.
composer.removeScene() Removes a specific Composer scene.