12 August 2014
Tutorial: Building a Level Selection Scene
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.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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:
1 |
loadsave.saveTable( myData.settings, "settings.json" ) |
Scene Setup
Now that the basic data module is configured, let’s look at the core scene:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
local composer = require( "composer" ) local scene = composer.newScene() local widget = require( "widget" ) -- Require "global" data table -- 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:
- 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.
- 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 usecomposer.gotoScene()
to go to the appropriate scene. - Inside the
handleLevelSelect()
function, we set thecurrentLevel
value within themyData.settings
table so that it’s easy to access from thegame.lua
scene. We also call composer.removeScene() to make sure that the game scene loads fresh each time. - 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. - 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
andyOffset
) and a count of the current number of buttons in the row (cellCount
). ThecellCount
variable starts at 1, and this tutorial will place 5 buttons in each row. - 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. - 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. - 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.
- 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. - 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 resetxOffset
to the first position in the row, increment thecellCount
variable, and add the number of pixels at which to draw the next row (yOffset
). - 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.
julien
Posted at 15:21h, 12 AugustWon’t you need to remove the background and the buttons in the scene:destroy ?
That would imply to declare them outside the create, wouldn’t it ?
Rob Miracle
Posted at 17:49h, 12 AugustComposer (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
Mo
Posted at 21:32h, 12 AugustFANTASTIC 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.
Rob Miracle
Posted at 16:55h, 13 AugustGreat idea Mo! I’ll see what I can come up with.
Rob
Peter Dwyer
Posted at 02:12h, 13 AugustIrony 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.
>_<
Mo
Posted at 23:37h, 14 August@Rob. Super cool, thank you!
Mo
Jeff Zivkovic
Posted at 16:52h, 17 AugustAwesome 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.
Jeff Zivkovic
Posted at 16:53h, 17 AugustCan somebody please direct me to a finished app example where composer is used?
Thanks a bunch,
Jeff
Jeff Zivkovic
Posted at 16:58h, 17 AugustSorry. Nevermind. I found the example that came with the Corona download.
Feeling sheepish.
Jeff Zivkovic
Posted at 06:07h, 18 AugustOkay. 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
Rob Miracle
Posted at 15:33h, 18 AugustPlease 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
Lori
Posted at 07:58h, 10 OctoberI 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
Rob Miracle
Posted at 19:46h, 10 Octobertry:
composer.gotoScene( “level” .. tostring(event.target.id), { effect=”crossFade”, time=333 } )
Sobh
Posted at 16:03h, 13 Junehi
thanks for the tut
i am facing one issue . i am trying to change the fillColor to use an image using
defaultFile = “lock.png”,
overFile = “lock.png”,
to be able to put a lock or unlock image according to if the level is passed or not
but whatever i do , the defaultFile and overFile didnot work .
and i removed the fillColor but still it show white !
Sobh
Posted at 04:38h, 19 JuneOk i find the issue
for any one who face this
you can remove the shape line and create a rect image with the lock shape on it and use it as the default file .
Guest
Posted at 05:57h, 05 JuneI have downloaded files, when I open it in simulator error message apper:
“ERROR: Runtime error
14:54:00.051 module ‘utility’ not found:
14:54:00.051 no field package.preload[‘utility’]”
You did not say anything about it in tutorial?
Rob Miracle
Posted at 11:15h, 05 JuneIn game.lua there is a line: local utility = require(“utility”)
Simply remove that line and you will be all set.
Jonas
Posted at 08:59h, 27 AugustHello! Thank you for this great tutorial! I’m having a great time programming my games! 🙂
I did this in my game, the functionality is exactally the same, I copied and pasted the code, the only thing I changed was the “id” variable that is equal to “i”, so now it is a number, not a string . But something is going wrong: When I click a level button, say 3, it stores the id in myData.settings.currentLevel, it becomes 3. But when game.lua is loaded, all the variables in gameData.lua are reset to the original ones in the file. So gameData.settings.currentLevel becomes 1 again. Any idea of what I’m doing wrong? Thank you!
Jonas
Posted at 09:02h, 27 AugustSorry, there is a typo in my comment, the second time I said “gameData” instead of “myData”, I meant “myData”.
Rob Miracle
Posted at 11:08h, 28 AugustThis is probably a question best asked in the forums. First, changing it to .i instead of .id to make it a number was an unnecessary step. You could have just set the id to a number. Are you by any change reading the saved settings in game.lua and perhaps not saving the settings in levelselect.lua? This would cause this problem. Generally you only need to load the settings in main.lua since the myData table will persist through the app. You however should save the settings every time you make a change you need to keep.