In this chapter, we'll create a scene to display the ten highest scores for the game. We'll also explore how to save these scores to a persistent location.
For the most part, when you use Composer, scenes remain lives
and score
variables that we use within game.lua
are associated only with that scene.
This is generally good for keeping an app clean and organized, but sometimes you'll need to access variables/objects in one scene from a different scene that's inherently unaware of their existence. In this chapter, for instance, we'll need to access the player's score score
from game.lua
)highscores.lua
scene.
Lua itself provides various ways to pass and access data between modules, but Composer makes it even easier with the following commands:
composer.setVariable()
— Sets a variable declared in one scene to be accessible throughout the entire composer.getVariable()
— Allows you to retrieve the value of any variable previously set via composer.setVariable()
.Armed with these commands, let's update the endGame()
function:
Open your game.lua
file.
Locate the endGame()
function and replace its contents with the following two commands:
local function endGame() composer.setVariable( "finalScore", score ) composer.gotoScene( "highscores", { time=800, effect="crossFade" } ) end
The first new command, composer.setVariable( "finalScore", score )
finalScore
with an assigned value of the score
variable. With this in place, we'll be able to retrieve the value from the high scores scene.
The second command simply redirects the app to the highscores.lua
scene instead of the menu scene.
game.lua
file.You can pass any standard Lua variable type, including tables, as the value for composer.setVariable()
. You can even use it to make a local function from one scene accessible within another. This flexibility makes composer.setVariable()
and composer.getVariable()
two of the most useful APIs within the Composer toolset.
Our high scores scene will basically feature the ability to store and retrieve scores, determine the ten highest, and display them.
First, make a copy of the standard scene-template.lua
highscores.lua
, place it within your StarExplorer
project folder, and open it using your chosen text editor.
As usual, we'll start by initializing some variables. Place the following code in the
-- ----------------------------------------------------------------------------------- -- Code outside of the scene event functions below will only be executed ONCE unless -- the scene is removed entirely (not recycled) via "composer.removeScene()" -- ----------------------------------------------------------------------------------- -- Initialize variables local json = require( "json" ) local scoresTable = {} local filePath = system.pathForFile( "scores.json", system.DocumentsDirectory )
Let's briefly examine these commands:
With the first command, we load a new resource for our app: the JSON library. JSON is a convenient format for working with data. If you're not familiar with JSON, don't worry — for the purpose of this app, you just need to learn how to use the json.encode()
and json.decode()
commands. Respectively, these commands let you take a Lua table, store (encode) its contents in a
The next command, local scoresTable = {}
The final line generates an absolute path to a JSON file (scores.json
) which we'll use to save the ten highest scores. Don't worry that this file doesn't actually exist yet — this command will create the file and initiate a link to it under the variable filePath
.
You may wonder why we're creating a file to store the high scores data. Why not just store them locally in a Lua table? The reason is fundamental to app development in general. Basically, variables which exist in an app's local memory will be destroyed if the app quits/closes!
Essentially, any data which needs to be accessed at some point after the app quits/closes should be stored in a persistent state, and the easiest way to store persistent data is to save it to a file on the device. Furthermore, this file must be stored in a persistent location.
To ensure that the scores.json
file is placed within a persistent location, we specify system.DocumentsDirectory
as the second parameter of the command we just entered. This tells Corona to create the scores.json
file within the app's internal "documents" directory. While there are other places we could put the file, we won't go into details on them here — just remember that the documents directory, referenced by the constant system.DocumentsDirectory
, is the only place which provides truly persistent storage for files that are created from within the app. In other words, even if the player quits the app and doesn't open it again until a month later, the scores.json
file will still exist.
Now that we've initialized the file for storing scores, let's write a function to check for any scores which were previously saved. Of course there won't be any at this point, but we'll need this function eventually.
Directly following the commands you already added to the loadScores()
function:
local filePath = system.pathForFile( "scores.json", system.DocumentsDirectory ) local function loadScores() local file = io.open( filePath, "r" ) if file then local contents = file:read( "*a" ) io.close( file ) scoresTable = json.decode( contents ) end if ( scoresTable == nil or #scoresTable == 0 ) then scoresTable = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } end end
Dissecting this function, we accomplish the following:
When working with files containing data, the first step is to confirm that the file exists. The first command inside this function, local file = io.open( filePath, "r" )
scores.json
in the system.DocumentsDirectory
folder (remember that we set the variable filePath
to point to that file and folder). In this command, also note the second parameter, "r"
. This tells Corona to open the file with read access only, but that's sufficient here because we simply need to read the file contents.
In the conditional block following, if the file exists, its contents will be dumped into the local variable contents
. Once we have its contents, we close the file with io.close( file )
json.decode()
, we decode contents
and store the values in scoresTable
— basically, json.decode()
converts scores.json
into a Lua table which can be used in our app.
In the final conditional block, just in case the scores.json
file is empty or doesn't exist, we assign scoresTable
ten default values of 0
so that the scene has something to work with.
If you want to make things more interesting, start the game with ten default scores that the "computer" scored and challenge the player to beat them! For example:
scoresTable = { 10000, 7500, 5200, 4700, 3500, 3200, 1200, 1100, 800, 500 }
Saving data is just as easy as reading data. Following the previous function in the saveScores()
function:
if ( scoresTable == nil or #scoresTable == 0 ) then scoresTable = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } end end local function saveScores() for i = #scoresTable, 11, -1 do table.remove( scoresTable, i ) end local file = io.open( filePath, "w" ) if file then file:write( json.encode( scoresTable ) ) io.close( file ) end end
This function saves high scores data as follows:
First, we clear out any unneeded scores from scoresTable
. Because we only want to save the highest ten scores, anything beyond that can be discarded. Using a for
loop, we step backwards through the table from its total count (#scoresTable
) to 11
, effectively removing all but ten scores.
Next, we open the scores.json
file. Unlike our io.open()
call within loadScores()
, here we specify "w"
as the second parameter. This tells Corona to create (write) a new file or overwrite the file if it already exists. It also tells Corona to open the file with write access which is important because, when saving score data, we need to write data to the file.
Once the file is successfully open, we call file:write()
to write the scoresTable
data to the file, converted into JSON via the json.encode()
command. Finally, we close the file with io.close( file )
Before we show the scores to the player, we need to manipulate the scoresTable
table slightly. Specifically, we need to add the most recent score to the table and then sort the table entries from highest to lowest.
All of the work for this scene can be done within scene:create()
, so let's focus our attention on that function:
loadScores()
function:-- 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 -- Load the previous scores loadScores() end
scoresTable
table. Notice that we get its value by calling composer.getVariable()
with a sole parameter of "finalScore"
. This exhibits the power of the composer.setVariable()
game.lua
—highscores.lua
). Following that, we immediately reset its value to 0
since we won't need further record of it.-- Load the previous scores loadScores() -- Insert the saved score from the last game into the table, then reset it table.insert( scoresTable, composer.getVariable( "finalScore" ) ) composer.setVariable( "finalScore", 0 ) end
To sort the table, we use the Lua table.sort()
command. For this to work, we must provide it with the table to sort and a reference to a comparison function (compare()
) which determines if items need to swap places. Here we've coded the compare()
function directly inside of scene:create()
because it doesn't need to be accessible elsewhere.
-- Insert the saved score from the last game into the table, then reset it table.insert( scoresTable, composer.getVariable( "finalScore" ) ) composer.setVariable( "finalScore", 0 ) -- Sort the table entries from highest to lowest local function compare( a, b ) return a > b end table.sort( scoresTable, compare ) end
The compare()
function itself takes two values which table.sort()
provides. Since we are sorting a table of numbers, the two parameters a
and b
will be numerical values. The function compares these two values as in a
greater than b
?"true
and table.sort()
knows that it needs to swap the values. Essentially, when the table.sort()
process finishes, our scoresTable
values will be sorted from highest to lowest.
scores.json
by calling our saveScores()
function:-- Sort the table entries from highest to lowest local function compare( a, b ) return a > b end table.sort( scoresTable, compare ) -- Save the scores saveScores() end
-- Save the scores saveScores() local background = display.newImageRect( sceneGroup, "background.png", 800, 1400 ) background.x = display.contentCenterX background.y = display.contentCenterY local highScoresHeader = display.newText( sceneGroup, "High Scores", display.contentCenterX, 100, native.systemFont, 44 ) end
for
loop from 1
to 10
. For each score, we'll first set the intended y position by calculating a local yPos
variable. This will make the scores run down the screen, evenly spaced apart:local highScoresHeader = display.newText( sceneGroup, "High Scores", display.contentCenterX, 100, native.systemFont, 44 ) for i = 1, 10 do if ( scoresTable[i] ) then local yPos = 150 + ( i * 56 ) end end end
For each score line, we'll display two text objects. On the left will be a rank number from 1) to 10). Directly to its right will be the actual score:
for i = 1, 10 do if ( scoresTable[i] ) then local yPos = 150 + ( i * 56 ) local rankNum = display.newText( sceneGroup, i .. ")", display.contentCenterX-50, yPos, native.systemFont, 36 ) rankNum:setFillColor( 0.8 ) rankNum.anchorX = 1 local thisScore = display.newText( sceneGroup, scoresTable[i], display.contentCenterX-30, yPos, native.systemFont, 36 ) thisScore.anchorX = 0 end end end
Most of the above code should be straightforward, but we're introducing an important new concept in anchors. By default, Corona positions the center of any display object at the x and y coordinate given. However, sometimes you'll need to align a series of objects along their edges — here, the list of scores will look best if each rank number is
To accomplish this, notice that we set the anchorX
property of each object. This property typically ranges between 0
(left) and 1
(right), with a default of 0.5
(center). Since we want each rank number to be anchorX
to 1
, and for each score number, we set anchorX
to 0
for left alignment.
Naturally, Corona also supports vertical anchor points with the anchorY
property. Similar to its horizontal counterpart, this property typically ranges between 0
(top) and 1
(bottom), with a default of 0.5
(center). Anchors can even be set outside of the 0
to 1
range, although this usage is less common. Setting either anchorX
or anchorY
to values less than 0
or greater than 1
will place the anchor point conceptually somewhere in space outside of the object's edge boundaries, which can be useful in some instances.
"tap"
event listener will call a basic gotoMenu()
function which we'll write next.end end local menuButton = display.newText( sceneGroup, "Menu", display.contentCenterX, 810, native.systemFont, 44 ) menuButton:setFillColor( 0.75, 0.78, 1 ) menuButton:addEventListener( "tap", gotoMenu ) end
Back up in the "tap"
event for the menuButton
object. This is straightforward enough:
local function gotoMenu() composer.gotoScene( "menu", { time=800, effect="crossFade" } ) end -- ----------------------------------------------------------------------------------- -- Scene event functions -- -----------------------------------------------------------------------------------
Similar to the game.lua
scene, let's remove the highscores.lua
scene within its own scene:hide()
function. This will remove the scene from memory when players return to the menu scene.
Inside the scene:hide()
function, add the following highlighted line:
-- 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) elseif ( phase == "did" ) then -- Code here runs immediately after the scene goes entirely off screen composer.removeScene( "highscores" ) end end
This wraps up our high score scene! Save your modified highscores.lua
and game.lua
files and then relaunch the Simulator. Now you should be able to play the game and see your scores saved/sorted in the high scores scene.
This scene is relatively simple, but if your code isn't working as expected, please compare it to the highscores.lua
file bundled with this chapter's source files.
We've covered several more important concepts in this chapter:
Command/Property | Description |
---|---|
system.pathForFile() | Generates an absolute path using |
io.open() | Opens a file for reading or writing. |
io.close() | Closes an open file handle. |
file:read() | Reads a file, according to the given formats which specify what to read. |
file:write() | Writes the value of each of its arguments to the file. |
json.decode() | Decodes a JSON-encoded data structure and returns a Lua table with the data. |
json.encode() | Converts a Lua table into a |
composer.setVariable() | Sets a variable declared in one scene to be accessible throughout the entire Composer app. |
composer.getVariable() | Allows you to retrieve the value of any variable set via composer.setVariable(). |
table.sort() | Sorts table elements in a given order. |
object.anchorX | Property which allows you to control the alignment of a display object along the x direction. |
object.anchorY | Property which allows you to control the alignment of a display object along the y direction. |