Today’s tutorial illustrates how to build a level select scene for a game. Level select screens are common in games which are divided into levels from which the player can resume play or choose to replay for maximum score.

levselect

Setup

This tutorial module is built atop the Corona Composer API. It assumes that we have a scene module game.lua which is loaded when a particular level is selected, along with a scene named menu.lua which will be shown if the player decides not to play a level.

We also need a common “data” table which can be accessed in multiple scenes. The following code loads a mydata.lua file which mimics the simple data module outlined in the Goodbye Globals! tutorial:

local M = {}
M.maxLevels = 50
M.settings = {}
M.settings.currentLevel = 1
M.settings.unlockedLevels = 4
M.settings.soundOn = true
M.settings.musicOn = true
M.settings.levels = {} 

-- These lines are just here to pre-populate the table.
-- In reality, your app would likely create a level entry when each level is unlocked and the score/stars are saved.
-- Perhaps this happens at the end of your game level, or in a scene between game levels.
M.settings.levels[1] = {}
M.settings.levels[1].stars = 3
M.settings.levels[1].score = 3833
M.settings.levels[2] = {}
M.settings.levels[2].stars = 2
M.settings.levels[2].score = 4394
M.settings.levels[3] = {}
M.settings.levels[3].stars = 1
M.settings.levels[3].score = 8384
M.settings.levels[4] = {}
M.settings.levels[4].stars = 0
M.settings.levels[4].score = 10294
-- levels data members:
--      .stars -- Stars earned per level
--      .score -- Score for the level
return M

This looks more complex than it actually is. The bottom half just pre-populates the table for purposes of this tutorial — this will allow us to see the results without building a functioning game around it. The key table members are at the top:

  • .maxLevels — The maximum number of levels for the game (to be included in the level select screen).
  • .settings — Data table that will hold various player data and otherwise.
  • .settings.currentLevel — Level that the player is currently playing.
  • .settings.unlockedLevels — Highest level attained (completed) by the player.
  • .settings.soundOn / .settings.musicOn — Booleans for player audio preferences.
  • .settings.levels — Table that tracks the player’s per-level progress. For this tutorial, only .stars is important, but we could also track the player’s per-level score.

This entire table can be easily saved using the loadsave module saveTable() call:

loadsave.saveTable( myData.settings, "settings.json" )

Scene Setup

Now that the basic data module is configured, let’s look at the core scene:

local composer = require( "composer" )
local scene = composer.newScene()

local widget = require( "widget" )

-- Require "global" data table (http://coronalabs.com/blog/2013/05/28/tutorial-goodbye-globals/)
-- This will contain relevant data like the current level, max levels, number of stars earned, etc.
local myData = require( "mydata" )

-- Declare vertices for vector stars (an image is probably preferable for an actual game).
local starVertices = { 0,-8,1.763,-2.427,7.608,-2.472,2.853,0.927,4.702,6.472,0.0,3.0,-4.702,6.472,-2.853,0.927,-7.608,-2.472,-1.763,-2.427 }

-- Button handler to cancel the level selection and return to the menu
local function handleCancelButtonEvent( event )
    if ( "ended" == event.phase ) then
        composer.gotoScene( "menu", { effect="crossFade", time=333 } )
    end
end

-- Button handler to go to the selected level
local function handleLevelSelect( event )
    if ( "ended" == event.phase ) then
        -- 'event.target' is the button and '.id' is a number indicating which level to go to.  
        -- The 'game' scene will use this setting to determine which level to load.
        -- This could be done via passed parameters as well.
        myData.settings.currentLevel = event.target.id

        -- Purge the game scene so we have a fresh start
        composer.removeScene( "game", false )

        -- Go to the game scene
        composer.gotoScene( "game", { effect="crossFade", time=333 } )
    end
end

-- Declare the Composer event handlers
-- On scene create...
function scene:create( event )
    local sceneGroup = self.view

    -- Create background
    local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight )
    background:setFillColor( 1 )
    background.x = display.contentCenterX
    background.y = display.contentCenterY
    sceneGroup:insert( background )

    -- Use a scrollView to contain the level buttons (for support of more than one full screen).
    -- Since this will only scroll vertically, lock horizontal scrolling.
    local levelSelectGroup = widget.newScrollView({
        width = 460,
        height = 260,
        scrollWidth = 460,
        scrollHeight = 800,
        horizontalScrollDisabled = true
    })

    -- 'xOffset', 'yOffset' and 'cellCount' are used to position the buttons in the grid.
    local xOffset = 64
    local yOffset = 24
    local cellCount = 1

    -- Define the array to hold the buttons
    local buttons = {}

    -- Read 'maxLevels' from the 'myData' table. Loop over them and generating one button for each.
    for i = 1, myData.maxLevels do
        -- Create a button
        buttons[i] = widget.newButton({
            label = tostring( i ),
            id = tostring( i ),
            onEvent = handleLevelSelect,
            emboss = false,
            shape="roundedRect",
            width = 48,
            height = 32,
            font = native.systemFontBold,
            fontSize = 18,
            labelColor = { default = { 1, 1, 1 }, over = { 0.5, 0.5, 0.5 } },
            cornerRadius = 8,
            labelYOffset = -6, 
            fillColor = { default={ 0, 0.5, 1, 1 }, over={ 0.5, 0.75, 1, 1 } },
            strokeColor = { default={ 0, 0, 1, 1 }, over={ 0.333, 0.667, 1, 1 } },
            strokeWidth = 2
        })
        -- Position the button in the grid and add it to the scrollView
        buttons[i].x = xOffset
        buttons[i].y = yOffset
        levelSelectGroup:insert( buttons[i] )

        -- Check to see if the player has achieved (completed) this level.
        -- The '.unlockedLevels' value tracks the maximum unlocked level.
        -- First, however, check to make sure that this value has been set.
        -- If not set (new user), this value should be 1.

        -- If the level is locked, disable the button and fade it out.
        if ( myData.settings.unlockedLevels == nil ) then
            myData.settings.unlockedLevels = 1
        end
        if ( i <= myData.settings.unlockedLevels ) then
            buttons[i]:setEnabled( true )
            buttons[i].alpha = 1.0
        else 
            buttons[i]:setEnabled( false ) 
            buttons[i].alpha = 0.5 
        end 

        -- Generate stars earned for each level, but only if:
        -- a. The 'levels' table exists 
        -- b. There is a 'stars' value inside of the 'levels' table 
        -- c. The number of stars is greater than 0 (no need to draw zero stars). 

        local star = {} 
        if ( myData.settings.levels[i] and myData.settings.levels[i].stars and myData.settings.levels[i].stars > 0 ) then
            for j = 1, myData.settings.levels[i].stars do
                star[j] = display.newPolygon( 0, 0, starVertices )
                star[j]:setFillColor( 1, 0.9, 0 )
                star[j].strokeWidth = 1
                star[j]:setStrokeColor( 1, 0.8, 0 )
                star[j].x = buttons[i].x + (j * 16) - 32
                star[j].y = buttons[i].y + 8
                levelSelectGroup:insert( star[j] )
            end
        end

        -- Compute the position of the next button.
        -- This tutorial draws 5 buttons across.
        -- It also spaces based on the button width and height + initial offset from the left.
        xOffset = xOffset + 75
        cellCount = cellCount + 1
        if ( cellCount > 5 ) then
            cellCount = 1
            xOffset = 64
            yOffset = yOffset + 45
        end
    end

    -- Place the scrollView into the scene and center it.
    sceneGroup:insert( levelSelectGroup )
    levelSelectGroup.x = display.contentCenterX
    levelSelectGroup.y = display.contentCenterY

    -- Create a cancel button for return to the menu scene.
    local doneButton = widget.newButton({
        id = "button1",
        label = "Cancel",
        onEvent = handleCancelButtonEvent
    })
    doneButton.x = display.contentCenterX
    doneButton.y = display.contentHeight - 20
    sceneGroup:insert( doneButton )
end

-- On scene show...
function scene:show( event )
    local sceneGroup = self.view

    if ( event.phase == "did" ) then
    end
end

-- On scene hide...
function scene:hide( event )
    local sceneGroup = self.view

    if ( event.phase == "will" ) then
    end
end

-- On scene destroy...
function scene:destroy( event )
    local sceneGroup = self.view   
end

-- Composer scene listeners
scene:addEventListener( "create", scene )
scene:addEventListener( "show", scene )
scene:addEventListener( "hide", scene )
scene:addEventListener( "destroy", scene )
return scene

Looking in Depth

All of this is based on the standard Composer scene. Although the code is commented, let’s examine each part and its functionality:

  1. To keep this tutorial simple, there are no required graphical assets. Instead, we use the display.newPolygon() API to generate vector stars. This API requires an array of vertices, so we define it in the scene’s main chunk in case there’s a need to use it elsewhere.
  2. The next two functions handle button events. Since our buttons use the onEvent handler, we need to test for the phase to ensure that the code is only executed once. In each handler function, we simply use composer.gotoScene() to go to the appropriate scene.
  3. Inside the handleLevelSelect() function, we set the currentLevel value within the myData.settings table so that it’s easy to access from the game.lua scene. We also call composer.removeScene() to make sure that the game scene loads fresh each time.
  4. The rest of the magic happens in the scene:create() event handler. First, we create a background, set its fill to white, and center it on screen. Then we create a widget.newScrollView() to hold the buttons in case we have more than one full screen of them. As designed, we can fit around 20 buttons on the screen without needing to scroll.
  5. Next, we initialize some variables that are used to compute each button’s location on the screen (within the scroll view). These include the x and y of the button (xOffset and yOffset) and a count of the current number of buttons in the row (cellCount). The cellCount variable starts at 1, and this tutorial will place 5 buttons in each row.
  6. For the max number of levels in the game (myData.maxLevels), we perform a loop and generate a widget.newButton() for each button. This tutorial uses the vector-based method of generating the button shape — in this case, a rounded rectangle.
  7. In the loop, we also set the button’s text label as the index of the array, thus numbering them in sequential order. We use this same value to set the button’s id value which is used when the player selects the level.
  8. The next block of code tests whether the number of unlocked levels has been passed in. Unlocked levels will be shown in full color with the button’s touch handler enabled, whereas locked levels will be faded out and disabled.
  9. If we want to show the number of stars earned per level, we can generate them now. In an actual produced game, we would likely use an image instead of a star-shaped polygon. Either way, we read the number of earned stars from the saved data (myData.settings.level.stars) and generate one star for each. Each star is positioned relative to the button’s x and y values.
  10. Next, we increment xOffset to position the next button further to the right. We also increment the number of buttons in the row and, if that value exceeds the number of buttons for each row, we reset xOffset to the first position in the row, increment the cellCount variable, and add the number of pixels at which to draw the next row (yOffset).
  11. Once the loop finishes, we insert the entire scroll view into the scene’s view group and center it on the screen.

Conclusion

While some of this module may seem complex, most of the process is straightforward. Of course, for an actual produced game, certain adjustments to variables and data structures would be necessary. In the meantime, if you want to experiment with the code base in this tutorial, please download it here.

    • Composer (and formerly Storyboard for those still using it) will automatically remove display objects that have been added to the scene’s view group (i.e. self.view or as localized “sceneGroup”. If the scene gets removed, then those objects will be freed up for you. You only need to worry about timers, native.* objects, transitions that may still be running, audio that may still be playing and have a call back and Runtime event listeners. Simple touch and tap listeners that are part of a display object like this are handled with the objects themselves.

      You might need to access these items outside of create scene if your code needs it, but for this example, there isn’t any thing you need to do that would need that. For instance, the touched button object is passed to the event handler as event.target, so I can still access the individual button from that function.

      Rob

  1. FANTASTIC tutorial! Thank you Rob. This is the type of things we all need to implement for games. can I suggest another tutorial that could be helpful to people making games? In addition to this level select tutorial, i would love a tutorial that explain how to make one of those menus that you select items by sweeping left or right. They are usually used to choose either a powerups or a world. Usually they are multiple box that aligned horizontally and you can scroll left/right with a sweep of you finger.

    Not sure If I am making any sense :)

    But thank you so much for this GREAT tutorial!

    Mo.

  2. Irony seems to be a weird theme in my life lately. I literally wrote one of these over the last few weeks because I wanted some way to select levels and refused to buy some pre-made pack to do what is essentially a simple task. Of course if you’re a game designer and developer you soon manage to turn simple into a war of attrition. Adding every feature under the sun from level groups to free roaming selection maps a-la candy crush and plants vs zombies 2.

    >_<

  3. Awesome Tutorial.

    But I’m thinking I missed some very basics about using composer. I don’t know the syntax to create the intended scene that would trigger the listener. I’ve been looking all over for an example where composer is used, so I can see the syntax and usage to actually create the scene and get this level selection rolling.

  4. Okay. Got it basically working.

    But I can’t figure out why I can’t see the button strokes and fill. I can see the stars and the word cancel. And the level number shows up momentarily when clicked (so, when it’s in “over” mode). But the rounded rectangles of the buttons are white-on-white. Any ideas?

    Thanks,

    Jeff

    • Please ask this in the forums. We are probably going to need you to post code and the forum comments isn’t a good place to do that.

      Rob

  5. I was able to get the buttons set up but I am having trouble getting them to go to the correct level. The game board for each level is unique in my game so I have scene’s for each: level1.lua, level2.lua etc. I am trying to modify the button handler code to do this. Below is the code I believe needs to be modified. I’ve tried if statements such as if buttons.id == 1 then…I would change the word “game” with “level1″. I would appreciate any help anyone can provide

    — Button handler to go to the selected level
    local function handleLevelSelect( event )
    if ( “ended” == event.phase ) then
    — ‘event.target’ is the button and ‘.id’ is a number indicating which level to go to.
    — The ‘game’ scene will use this setting to determine which level to load.
    — This could be done via passed parameters as well.
    myData.settings.currentLevel = event.target.id

    — Purge the game scene so we have a fresh start
    composer.removeScene( “game”, false )

    — Go to the game scene
    composer.gotoScene( “game”, { effect=”crossFade”, time=333 } )
    end
    end

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>