Chapter 5 — Converting the Game to Composer

Previous | Next

In the last chapter, we learned the basics of scene management. In this chapter, we're 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 of Star Explorer, 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 that you think a little differently. Using it correctly 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 off screen.

Consider this concept like a movie scene: if the director is transitioning into a scene — fading in, panning the camera to a point, etc. — the actors in the scene usually won't begin acting until the scene is "ready" and focused. The same approach applies to Composer scenes. For instance, we already added commands to spawn asteroids and put them in motion, but in this game.lua scene we're about to create, those commands will only run once the scene is fully on screen.

Accessible Code

Let's get started! The first point of attack is the scene-accessible area of the file.

  1. Make a copy of the standard scene-template.lua file, included with this chapter's source files.

  2. Rename this copy to game.lua and place it within your StarExplorer project folder.

  3. Open both main_original.lua and game.lua in separate editor windows/tabs. You will be copying several blocks of code from main_original.lua to game.lua, so it's convenient to have both Lua 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:

local composer = require( "composer" )

local scene = composer.newScene()

-- -----------------------------------------------------------------------------------
-- 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

Previously, we followed these commands with the math.randomseed() command. However, you may recall that we re-stated that 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:

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

-- 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 following localized variables from main_original.lua:

local objectSheet = graphics.newImageSheet( "gameObjects.png", sheetOptions )

-- 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). Now, an important concept to understand 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 earlier. So, instead of associating each variable with a display.newGroup(), just leave them undefined for the moment:

local ship
local gameLoopTimer
local livesText
local scoreText

local backGroup
local mainGroup
local uiGroup
Note

In our original version, we created the background, ship, lives text, and score text immediately following initialization of the display groups. Now, because we're only creating local references for the display groups, we must defer these actions until later, inside the scene:create() function.

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.

First, copy over the updateText() function:

local backGroup
local mainGroup
local uiGroup


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

Follow this with the createAsteroid() function:

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

The next function, fireLaser(), should follow:

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


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
Important

Now observe the line directly following the fireLaser() function in main_original.lua:

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/paste in the next few 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 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.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 code 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:

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

-- create()
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? 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 just defined forward references. Add the following highlighted lines:

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

    -- 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
end

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

As you recall, most Solar2D 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 — you can't simply supply an inline group reference to insert the new display group into an existing group. Instead, you must use the object:insert() command.

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

    -- 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

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

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 both text objects:

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

    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 )
end

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

Essentially, you can create a forward reference in the scene-accessible area, assign an actual object to that reference inside a scene: function, and then other functions will associate the reference with the new object.

Notice that we are still inserting objects into their proper display groups such as backGroup, mainGroup, and uiGroup. This is the correct procedure because all of those groups were inserted into the scene's view group and they are now children of that parent group.

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

    -- 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 )

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

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 use scene:show() nor its companion function scene:hide(), but in this game scene we do. At this point, there are still some essential aspects which we haven't copied over from main_original.lua — primarily, we have not yet enabled collision detection or started the game loop to spawn asteroids. Fortunately, scene:show() can be used to put everything in motion!

Transition Effects

While we didn't utilize one for the menu scene, the composer.gotoScene() command allows you to specify a transition effect such as fading in, sliding in from a screen edge, cross-fading from the previous scene, etc. Naturally, there is a time duration associated with the start and finish of scene transitions, and this is where scene phases come into play.

Scene Phases

An important factor to understand (in contrast to scene:create()) is that Composer calls the scene:show() function twice. Of course it's imperative to know when each of these calls occurs so that we can take the proper actions at the proper time. This distinction is provided via event.phase within scene:show(). Basically, scene:show() calls/phases work like this:

  1. The first call occurs when the scene is ready to be shown, essentially after every command within scene:create() has been executed. In this case, event.phase is "will", effectively indicating that the scene "will show" and the transition effect is about to occur.

  2. The second call occurs immediately after the scene has shown — basically, when the scene transition has completed. In this case, event.phase is "did", meaning the scene "did show" and the transition effect completed.

Notice that the scene template already contains a conditional statement to check for each phase of scene:show():

-- show()
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

    end
end

Now that you understand this concept, simply paste code from your main_original.lua file into the proper conditional clause. For this game, we basically want to start the game running — spawning asteroids, detecting collisions, etc. — once the scene is fully on screen (the "did" phase). So, in your game.lua file, within the scene:show() function, add the three highlighted commands:

-- show()
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, these commands accomplish the following:

  1. Re-start the physics engine with physics.start() (remember that we paused it in scene:create()).
  2. Start collision detection.
  3. Start the game loop with our original timer.performWithDelay() command.

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

At this point there's a significant flaw in the game. When the player runs 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 Composer exits the scene when the player runs out of lives.

When we intend to exit the game scene, remember that the gameLoopTimer timer will still be running, spawning asteroids and removing off-screen asteroids. In addition, the physics engine will still be moving asteroids about. All of these things should be stopped inside the scene:hide() function.

Similar to scene:show(), scene:hide() will be called twice and the distinction is once again provided via event.phase. Basically, scene:hide() calls/phases work like this:

  1. The first call occurs when the scene is about to be hidden (transition off screen). In this case, event.phase is "will", effectively indicating that the scene "will hide" and the transition effect is about to occur.

  2. The second call occurs immediately after the scene is fully off screen. In this case, event.phase is "did", meaning the scene "did hide" and the transition effect completed.

For this scene, in the "will" and "did" phase conditions of scene:hide(), we'll "undo" some things by adding three commands:

-- hide()
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 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 detecting collisions or stop moving (physics.pause()) when the scene transition begins.

Scene Cleanup

Hopefully players will want to play the game again! By default, Composer caches 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 the game again, the scene comes back into view and new asteroids begin spawning. Unfortunately, there are some problems:

Depending on the game, cleaning up a scene to restart fresh can involve some effort. In this game, 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 to have an easier way to reset a scene? Fortunately, Composer offers one:

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 benefit mentioned above, but for most scenes it's not worth the effort to programmatically reset each aspect individually.

With this simplified approach, let's modify our scene:hide() function:

-- hide()
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()
        composer.removeScene( "game" )
    end
end

This addition shoud be clear — we simply call composer.removeScene( "game" ) within the "did" phase of scene:hide(), effectively destroying the scene after it transitions fully off screen.

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

This chapter was a bit more detailed and required many things to be copied over to very specific places within game.lua. If your code isn't working as expected, please compare it to the game.lua file bundled with this chapter's source files.

Extra Credit

Earlier in this chapter, you learned about scene transition effects. We already applied one to the composer.gotoScene() command within the endGame() function, so now let's apply one when the game scene comes into view.

If you recall, the game scene is only accessed via the Play button in the menu scene, so we'll modify that file:

  1. Open the menu.lua file within your StarExplorer folder.

  2. Find the gotoGame() function and add a "crossFade" effect with a duration of 800 milliseconds to the composer.gotoScene() command:

local function gotoGame()
    composer.gotoScene( "game", { time=800, effect="crossFade" } )
end
  1. Although we haven't yet created it, the highscores.lua scene should also appear with a transition effect. Since menu.lua is already open, modify the composer.gotoScene() command within the gotoHighScores() function immediately following gotoGame():
local function gotoGame()
    composer.gotoScene( "game", { time=800, effect="crossFade" } )
end

local function gotoHighScores()
    composer.gotoScene( "highscores", { time=800, effect="crossFade" } )
end
  1. Save your modified menu.lua file.

Great! Now the game scene — and the high scores scene, once we create it — will transition in/out with a nice cross-fade effect.

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.