In the last couple of days I have tried to create some kind of template for level selection for games build with Corona SDK. This template is a level selection which you often see in mobile games. The user can select a level based on a grid outline and based on the status of the level the grid changes (open, locked or completed).
In the video below you can see what the template is.
How is the template build? I used storyboard to create the different scenes. We will have main.lua to launch the menu.lua. From the menu you can navigate to either gamecredits.lua or play.lua. Gamecredits.lua is empty as it has no purpose for this tutorial so there is only a back button. When you enter play.lua you get the grid with different levels. After selecting a level you enter level01.lua or level02.lua. I only have added level01.lua and level02.lua to the sample code, but it should be clear that you need a lua file for each level.
So lets start with main.lua. Main.lua simple starts the menu scene with a slideDown animation. Note that there are also two variable for housekeeping to determine the center of the screen.
local storyboard = require ("storyboard") storyboard.purgeOnSceneChange = true display.setStatusBar(display.HiddenStatusBar) centerX = display.contentCenterX centerY = display.contentCenterY storyboard.gotoScene ( "menu", { effect = "slideDown"} )
Next we need to launch the menu.lua, which has 2 texts fields positioned to navigate either to play.lua or to the gamecredits.lua
local storyboard = require( "storyboard" ) local scene = storyboard.newScene() -- local forward references should go here -- local function buttonHit(event) storyboard.gotoScene ( event.target.destination, {effect = "slideDown"} ) return true end -- Called when the scene's view does not exist: function scene:createScene( event ) local group = self.view local title = display.newText( "Welcome to Game", 0, 0, "Helvetica", 38 ) title.x = centerX title.y = display.screenOriginY + 40 group:insert(title) local playBtn = display.newText( "Start game", 0, 0, "Helvetica", 25 ) playBtn.x = centerX playBtn.y = centerY playBtn.destination = "play" playBtn:addEventListener("tap", buttonHit) group:insert(playBtn) local creditsBtn = display.newText( "Credits", 0, 0, "Helvetica", 25 ) creditsBtn.x = centerX creditsBtn.y = centerY + 80 creditsBtn.destination = "gamecredits" creditsBtn:addEventListener("tap", buttonHit) group:insert (creditsBtn) end -- Called immediately after scene has moved onscreen: function scene:enterScene( event ) local group = self.view -- INSERT code here (e.g. start timers, load audio, start listeners, etc.) end -- Called when scene is about to move offscreen: function scene:exitScene( event ) local group = self.view -- INSERT code here (e.g. stop timers, remove listeners, unload sounds, etc.) -- Remove listeners attached to the Runtime, timers, transitions, audio tracks end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) local group = self.view -- INSERT code here (e.g. remove listeners, widgets, save state, etc.) -- Remove listeners attached to the Runtime, timers, transitions, audio tracks end --------------------------------------------------------------------------------- -- END OF YOUR IMPLEMENTATION --------------------------------------------------------------------------------- -- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be -- automatically unloaded in low memory situations, or explicitly via a call to -- storyboard.purgeScene() or storyboard.removeScene(). scene:addEventListener( "destroyScene", scene ) --------------------------------------------------------------------------------- return scene
Now create the gameCredits.lua and set a title and a back button (we don’t want to get stuck).
local storyboard = require( "storyboard" ) local scene = storyboard.newScene() -- local forward references should go here -- local function buttonHit(event) storyboard.gotoScene ( event.target.destination, {effect = "slideUp"} ) return true end -- Called when the scene's view does not exist: function scene:createScene( event ) local group = self.view -- CREATE display objects and add them to 'group' here. -- Example use-case: Restore 'group' from previously saved state. local title = display.newText( "Game Credits", 0, 0, "Helvetica", 38 ) title.x = centerX title.y = display.screenOriginY + 40 group:insert(title) local backBtn = display.newText( "Back", 0, 0, "Helvetica", 25 ) backBtn.x = centerX backBtn.y = centerY backBtn.destination = "menu" backBtn:addEventListener("tap", buttonHit) group:insert(backBtn) end -- Called immediately after scene has moved onscreen: function scene:enterScene( event ) local group = self.view -- INSERT code here (e.g. start timers, load audio, start listeners, etc.) end -- Called when scene is about to move offscreen: function scene:exitScene( event ) local group = self.view -- INSERT code here (e.g. stop timers, remove listeners, unload sounds, etc.) -- Remove listeners attached to the Runtime, timers, transitions, audio tracks end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) local group = self.view -- INSERT code here (e.g. remove listeners, widgets, save state, etc.) -- Remove listeners attached to the Runtime, timers, transitions, audio tracks end --------------------------------------------------------------------------------- -- END OF YOUR IMPLEMENTATION --------------------------------------------------------------------------------- -- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be -- automatically unloaded in low memory situations, or explicitly via a call to -- storyboard.purgeScene() or storyboard.removeScene(). scene:addEventListener( "destroyScene", scene ) --------------------------------------------------------------------------------- return scene
Up till now we only build the storyboard which is standard stuff from the Corona SDK. In play.lua we will actually build the level grid and build the logic for handling the different statuses like open level, locked level, or completed level.
- We use level.png for an open level
- We use lock.png for a locked level
- We use greenchecked.png for a level which is completed.
You can find these PNG files on the bottom of this post in a zip-file
The code block below is your play.lua. Below the code block I explain some of the most important elements
local storyboard = require( "storyboard" ) local scene = storyboard.newScene() -- local forward references should go here -- levels = { 1, 2, 2, 2 , 2, --1 means level is open to be played (level.png) 2, 2, 2, 2, 2, --2 means level is locked (locked.png) 2, 2, 2, 2, 2 -- 3 means level is completed (greenchecked.png) } images ={ { getFile = "level.png", types = "play" }, { getFile = "lock.png", types = "locked"}, { getFile = "greenchecked.png", types = "done"} } local function buttonHit(event) storyboard.gotoScene ( event.target.destination, {effect = "slideUp"} ) print( event.target.destination) return true end -- Called when the scene's view does not exist: function scene:createScene( event ) local group = self.view local levelIndex =0 for i=0,2 do for j=1,5 do tablePlace = i*5 + j levelIndex = levelIndex + 1 local imagesId = levels[levelIndex] levelImg = display.newImageRect (images[imagesId].getFile , 45, 45 ) levelImg.x = 45 + (j*65) levelImg.y = 85+ (i*65) group:insert(levelImg) leveltxt = display.newText("Level "..tostring(tablePlace), 0,0, "Helvetica", 10) leveltxt.x = 45 + (j*65) leveltxt .y = 110+ (i*65) leveltxt:setTextColor (250, 255, 251) group:insert (leveltxt) levelImg.destination = "level0"..tostring(tablePlace) if images[imagesId].types ~= "locked" then levelImg:addEventListener("tap", buttonHit) end end end -- CREATE display objects and add them to 'group' here. -- Example use-case: Restore 'group' from previously saved state. local title = display.newText( "Level Selection", 0, 0, "Helvetica", 20 ) title.x = centerX title.y = display.screenOriginY + 40 group:insert(title) local backBtn = display.newText( "Back", 0, 0, "Helvetica", 20 ) backBtn.x = display.screenOriginX + 50 backBtn.y = display.contentHeight - 30 backBtn.destination = "menu" backBtn:addEventListener("tap", buttonHit) group:insert(backBtn) end -- Called immediately after scene has moved onscreen: function scene:enterScene( event ) local group = self.view -- INSERT code here (e.g. start timers, load audio, start listeners, etc.) end -- Called when scene is about to move offscreen: function scene:exitScene( event ) local group = self.view -- INSERT code here (e.g. stop timers, remove listeners, unload sounds, etc.) -- Remove listeners attached to the Runtime, timers, transitions, audio tracks end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) local group = self.view -- INSERT code here (e.g. remove listeners, widgets, save state, etc.) -- Remove listeners attached to the Runtime, timers, transitions, audio tracks end --------------------------------------------------------------------------------- -- END OF YOUR IMPLEMENTATION --------------------------------------------------------------------------------- -- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be -- automatically unloaded in low memory situations, or explicitly via a call to -- storyboard.purgeScene() or storyboard.removeScene(). scene:addEventListener( "destroyScene", scene ) --------------------------------------------------------------------------------- return scene
The levels table in line 10-15 holds the status of the levels. A level can have either the status: open, locked or completed. 1 meaning open, 2 locked and 3 completed. When we launch play.lua for the first time only level one is open. When you for example want the first three levels to be open, then set the first 3 values in the table as 1.
In line 17 till 21 you can see a table with the images belonging to one of the 3 statuses. Each element in the table has two values. The first one is the actual PNG file (getFile) and the second one is the type. We want a type element so we can make easily a reference later on.
Line 33- 55 is the more exciting stuff :). In this block we line up 15 levels in one screen by positioning 5 levels horizontal and 3 levels vertical. We create these by a for-loop. Later one we also want a level number text below the level so the user can read the level number. This is hold in the variable tablePlace and is later concatenated in levelText. When we loop through we want to hold a variable of the level called levelIndex at start we set this to zero so we always have only 15 levels in total. This levelIndex value is used to get the correct PNG file for the level. In imagesId = levels[levelIndex] we take our levelIndex and get the corresponding table value of levels. Then we create the level image (levelImg) by using imagesId to get our actual PNG levelImg = display.newImageRect (images[imagesId].getFile , 45, 45 ). We use levelImg.x and levelImg.y to position the each level in the grid. We also position below each level image a text of the level For example level 1, level 2, level 3 etc).
levelImg.destination = “level0″..tostring(tablePlace) creates the variable so we know which level.lua we need to open. You have to watch out here. As we concatenate tablePlace to “level0″ and tablePlace generates a number between 1 and 15. This means to make it work your level lua files MUST BE NAMED as level01.lua, level02.lua etc.
In the last part we have an if-statement which allows the user only to click on a level which is not locked (~=). You can see that we use the types element of the levels table to check for this.
Now open your level01.lua (the code below is the same for level02.lua, level03.lua etc). So for this example I only show level01.lua.
local storyboard = require( "storyboard" ) local scene = storyboard.newScene() local widget = require ("widget") local playfile = require ("play") -- local forward references should go here -- local completegameBtn local function buttonHit(event) storyboard.gotoScene ( event.target.destination, {effect = "slideUp"} ) return true end -- Called when the scene's view does not exist: function scene:createScene( event ) local group = self.view -- CREATE display objects and add them to 'group' here. -- Example use-case: Restore 'group' from previously saved state. local title = display.newText( "Level 1", 0, 0, "Helvetica", 38 ) title.x = centerX title.y = display.screenOriginY + 40 group:insert(title) local backBtn = display.newText( "Back", 0, 0, "Helvetica", 25 ) backBtn.x = display.screenOriginX + 50 backBtn.y = display.contentHeight - 30 backBtn.destination = "play" backBtn:addEventListener("tap", buttonHit) group:insert(backBtn) end -- Called immediately after scene has moved onscreen: function scene:enterScene( event ) local group = self.view -- INSERT code here (e.g. start timers, load audio, start listeners, etc.) local function btnClicked (event) levels[1] = 3 levels[2] = 1 completegameBtn.destination = "play" completegameBtn:addEventListener("tap", buttonHit) end completegameBtn = widget.newButton { label = "Complete game", onRelease=btnClicked} completegameBtn.x = centerX completegameBtn.y = centerY group:insert (completegameBtn) end -- Called when scene is about to move offscreen: function scene:exitScene( event ) local group = self.view -- INSERT code here (e.g. stop timers, remove listeners, unload sounds, etc.) -- Remove listeners attached to the Runtime, timers, transitions, audio tracks end -- Called prior to the removal of scene's "view" (display group) function scene:destroyScene( event ) local group = self.view -- INSERT code here (e.g. remove listeners, widgets, save state, etc.) -- Remove listeners attached to the Runtime, timers, transitions, audio tracks end --------------------------------------------------------------------------------- -- END OF YOUR IMPLEMENTATION --------------------------------------------------------------------------------- -- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be -- automatically unloaded in low memory situations, or explicitly via a call to -- storyboard.purgeScene() or storyboard.removeScene(). scene:addEventListener( "destroyScene", scene ) --------------------------------------------------------------------------------- return scene
In level01.lua we create a button, which completes the level when the user clicks it. (As you can see its a very difficult game….)
So when a user completes level 1 (levels[1]) we set the value in the levels table to 3. (which means the greenchecked.png). But we also set levels[2] as 1, which means level.png (level is open). For each lua level file you need to set this.
You can download the complete source code here.
In case you want to save the state of the level, so that users keep their progress when they close the app (game). Read this tutorial.
If I wish to have more than 15 levels, would I change the for-loop
Yes change the for-loop
—
for i=0,2 do
for j=1,5 do
—
i=0,2 are three rows (0, 1, 2). you can add easily 1 by making it i=0,3. Same for the columns j = 1, 5.
You probably get some space issues. You could add a slider widget (part of the Corona SDK
Thank You, and I tried exactly what you had said but I kept getting an error. I used i=0,3
and j=0,6. Am I supposed to change the equation below involving tablePlace = i*5 + j
Thank You, and I tried exactly what you had said but I kept getting an error. I used i=0,3
and j=0,6. Am I supposed to change the equation below involving tablePlace = i*5 + j
Hi Sean,
Sorry for that, I wasnt complete. I tested the following and it is working for me.
Update the for loop with:
for i=0,3 do
for j=1,6 do
but what was missing was the update of the level table
You need to add an extra column and row as well.
levels =
{
1, 2, 2, 2 , 2, 2, –1 means level is open to be played (level.png)
2, 2, 2, 2, 2, 2, –2 means level is locked (locked.png)
2, 2, 2, 2, 2 , 2, — 3 means level is completed (greenchecked.png)
2, 2, 2, 2, 2 , 2
}
Hope this helps
Awesome! Thank you!
I did that but I received an error:( Attempt to index global ‘levelImg’ (a nil value) )
Could the problem be that i am using text wrangler
Hi Sean,
No it shouldnt matter. Where exactly is it going wrong? Does the level layout load? Do you get the error directly after clicking one of the levels?
If you like please copy and paste your code from the createScene function
I get the error after clicking the start game button. Then the simulator stops before the level menu and displays the error
local storyboard = require( “storyboard” )
local scene = storyboard.newScene()
– local forward references should go here –
–levelProgress = 0
–levelImg = {}
–allLevels = {}
levels =
{
1, 2, 2, 2, 2, 2, –1 means level is open to be played (level.png)
2, 2, 2, 2, 2, 2, –2 means level is locked (locked.png)
2, 2, 2, 2, 2, 2, — 3 means level is completed (greenchecked.png)
2, 2, 2, 2, 2, 2,
}
images ={
{ getFile = “level.png”, types = “play” },
{ getFile = “lock.png”, types = “locked”},
{ getFile = “greenchecked.png”, types = “done”}
}
local function buttonHit(event)
storyboard.gotoScene ( event.target.destination, {effect = “slideUp”} )
print( event.target.destination)
return true
end
– Called when the scene’s view does not exist:
function scene:createScene( event )
local group = self.view
local levelIndex =0
for i=0,3 do
for j=1,6 do
tablePlace = i*5 + j
levelIndex = levelIndex + 1
local imagesId = levels[levelIndex]
levelImg = display.newImageRect (images[imagesId].getFile , 40, 40 )
levelImg.x = 45 + (j*65)
levelImg.y = 85 + (i*65)
group:insert(levelImg)
leveltxt = display.newText(tostring(tablePlace), 0,0, “TEN O CLOCK”, 25 )
leveltxt.x = 45 + (j*65)
leveltxt .y = 110 + (i*65)
leveltxt:setTextColor (250, 255, 251)
group:insert (leveltxt)
levelImg.destination = “level0″..tostring(tablePlace)
if images[imagesId].types ~= “locked” then
levelImg:addEventListener(“tap”, buttonHit)
end
end
end
– CREATE display objects and add them to ‘group’ here.
– Example use-case: Restore ‘group’ from previously saved state.
local title = display.newText( “Level Selection”, 0, 0, “TEN O CLOCK”, 30 )
title.x = centerX
title.y = display.screenOriginY + 30
group:insert(title)
local backBtn = display.newText( “Back”, 0, 0, “TEN O CLOCK”, 30 )
backBtn.x = display.screenOriginX + 50
backBtn.y = display.contentHeight – 30
backBtn.destination = “menu”
backBtn:addEventListener(“tap”, buttonHit)
group:insert(backBtn)
end
– Called immediately after scene has moved onscreen:
function scene:enterScene( event )
local group = self.view
– INSERT code here (e.g. start timers, load audio, start listeners, etc.)
end
– Called when scene is about to move offscreen:
function scene:exitScene( event )
local group = self.view
– INSERT code here (e.g. stop timers, remove listeners, unload sounds, etc.)
– Remove listeners attached to the Runtime, timers, transitions, audio tracks
end
– Called prior to the removal of scene’s “view” (display group)
function scene:destroyScene( event )
local group = self.view
– INSERT code here (e.g. remove listeners, widgets, save state, etc.)
– Remove listeners attached to the Runtime, timers, transitions, audio tracks
end
———————————————————————————
– END OF YOUR IMPLEMENTATION
———————————————————————————
– “createScene” event is dispatched if scene’s view does not exist
scene:addEventListener( “createScene”, scene )
– “enterScene” event is dispatched whenever scene transition has finished
scene:addEventListener( “enterScene”, scene )
– “exitScene” event is dispatched before next scene’s transition begins
scene:addEventListener( “exitScene”, scene )
– “destroyScene” event is dispatched before view is unloaded, which can be
– automatically unloaded in low memory situations, or explicitly via a call to
– storyboard.purgeScene() or storyboard.removeScene().
scene:addEventListener( “destroyScene”, scene )
———————————————————————————
return scene
Hi Sean,
Do you have the images in the same folder as your play.lua?
The images array has no path for the images in my code, meaning that level.png, lock.png etc are on the same level as my lua files. If you have for example an images folder you need to define the path as well.
Could this be the cause of your issue?
images ={
{ getFile = “level.png”, types = “play” },
{ getFile = “lock.png”, types = “locked”},
{ getFile = “greenchecked.png”, types = “done”}
}
Hey, can you give the simple saved state syntax in your tmplate. Coz every I close the app it have to play from the start.
I mean except lvl 1 its locked.
Btw its good template. Thanks
Hi Amar, thanks for visiting my blog. This is a very good point. I didn’t include it in the post. Let me update the post in the next 1-2 days and I will include it. I still have to make it myself but I have already an idea how to do it.
Hi Amar,
I added the saving level states functionality. See this post. Looking forward what you think about it.
http://www.christianpeeters.com/corona-sdk/corona-sdk-save-level-progress/
Best Christian
Hi Amar,
Thanks for sharing. Just wondering though… I notice that you are not removing event listeners created for levelImg and the buttons on exitscene…. Will this create memory leaks?
I meant to Christian earlier.
thnx for sharing! I’m reviewing this now. thanks for the code too!
Perfect, exactly what I was looking for! Thanks!
Hi Christian,
if I change “tap” with “collision”
local function btnClicked (event)
levels[1] = 3
levels[2] = 1
completegameBtn.destination = “play”
completegameBtn:addEventListener(“tap”, buttonHit)
end
how I fix this?
completegameBtn = widget.newButton { label = “Complete game”, onRelease=btnClicked}
I’ll explain, in your example, the level is completed when I click on “Complete Game” then returns to the screen play.lua and level 2 is unlocked …
I would that the level is complete with the collision of two objects.
could you explain how to change this thing please?
Hi Mark,
Apologies for my late response, I had no Internet connection for a few days.
It’s quite simple, you need to set up you collision function and when the object you want to collide happen you set the values of the levels table:
function onCollision(event)
if (event.object1.myName==”objectName” and event.object2.myName==”objectName”) then
levels[1] = 3
levels[2] = 1
end
Runtime:addEventListener(“collision”, onCollision)
okkk thank you very much Christian!!!
Hello Christian,
Thank you for this very easy to follow along storyboard tutorial. I’ve been surfing the web for ages in hopes of finding a very well explained tutorial of what storyboard is and how I would go about using it. I just want you to know that this post has exceeded my understanding of how to properly use storyboard, and I’d like to thank you for that. I’m going to go through some of your other tutorials now and hope to see many more in the future.
Thanks!
Hi there, Very nice tutorial :) What about to use this for a map game ? I mean, the levels button will be put on a map, just like in Bubble Witch Saga ? Do you have an ideal ? Thank you :)
Hi, thank you very much for that tutorial, very helpful.
Perhaps you have an idea what I am doing wrong while trying to use your code. I have sort of an Astroid clone and want the game to unlock the next level after a certain score was achieved.
I am using the play.lua file as in your example.
In my level01 file it says:
local playfile = require (“play”)
…..
local levelcomplete
…….
local function game_over()
if game_on then
game_on = false
local explosion = make_explosion()
explosion.x = ship.x
explosion.y = ship.y
ship.isVisible = false
timer.cancel( missile_timer )
timer.performWithDelay( 1200, function()
if score > 10 then
levels[1] = 3
levels[2] = 1
levelcomplete.destination = “play”
storyboard.showOverlay( “game_complete”, {effect=”fade”} )
else
storyboard.showOverlay( “game_over”, {effect=”fade”} )
end
end )
end
the game works great and everything does what it should do as long as you don’t achieve more than 10 points, otherwise I get this error:
Corona Simulator Runtime error
File: level01.lua
Line: 252
Attempt to index upvalue ‘levelcomplete’ (a nil value) —- this line: levelcomplete.destination = “play”
stack traceback:
level01.lua:252: in function ‘_listener’
?: in function
?: in function
any help is appreciated. thank you
Thank yu so much for this great example! It had really saved me from alot of headache. I am trying to make another layer this this by adding a WORLD selection, that has levels in that world to select from. I am having trouble saving the worlds array to the json file. I have little understanding of tables and table manipulation . can you point me in right direction to help me make this happen? so from main menu i want to select play. that should send me to a scene where i select the world. after selecting the world i should be send to another scene where i select a level in that world to play. and i want the save function as well
Hi,
Thanks for the great post. I was very helpful.
Although, i am facing a particular challenge implementing your code.
When i click on complete game on level01.lua, it takes me to play.lua but doesn’t unlock level02.lua.
What am i doing wrong. kindly assist.
Hello! How can we edit this to be compatible with Composer? Storyboard is no longer available with the new release of Corona SDK.