Chapter 6 — Implementing High Scores

Previous | Next

In this chapter, we are going to 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.

Composer-Accessible Data

For the most part, when you use Composer, scenes remain self-dedicated, meaning that variables, functions, and objects which you create within a given scene are isolated to that specific scene. For example, the 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) inside our upcoming highscores.lua scene.

Lua itself provides various ways to pass and access data between modules, but fortunately Composer makes it even easier with the following two commands:

Armed with these commands, let's update the endGame() function. Open your game.lua file, locate the function, and replace its entire contents with three new commands:

local function endGame()
    composer.setVariable( "finalScore", score )
    composer.removeScene( "highscores" )
    composer.gotoScene( "highscores", { time=800, effect="crossFade" } )
end

The first new command, composer.setVariable( "finalScore", score ), creates a Composer-accessible variable named 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 command following removes/destroys the high scores scene (we'll create this scene in the next section) and the third command redirects the app to that scene instead of the menu scene.

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.

High Scores Scene

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 file, included with this chapter's source files. Rename this copy to 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 scene-accessible code area:

-- Initialize variables
local json = require( "json" )

local scoresTable = {}

local filePath = system.pathForFile( "scores.json", system.DocumentsDirectory )

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 following built-in Corona commands:

These commands basically let you take a Lua table, store its contents in a well-understood format, and later translate that data back into a Lua table.

The next command, local scoresTable = {}, simply creates an empty table which will eventually contain the retrieved scores, or an updated list of scores to save.

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.

Important

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.

Loading Data

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 scene-accessible code area, include the following function:

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

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" ), attempts to open the file 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 ). Then, using the command json.decode(), we decode contents and store the values in scoresTable — basically, json.decode() converts the JSON file 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, you could 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

Saving data is just as easy as reading data. Following the previous function in the scene-accessible code area, add this function:

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

The first step is to clear out any redundant scores from scoresTable. Because we only need 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.

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

Displaying High Scores

Before we show the scores to the player, we need to manipulate the scoresTable table slightly. Specifically, we need to add the latest 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:

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

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

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

    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

    local menuButton = display.newText( sceneGroup, "Menu", display.contentCenterX, 810, native.systemFont, 44 )
    menuButton:setFillColor( 0.75, 0.78, 1 )
    menuButton:addEventListener( "tap", gotoMenu )
end

There's a lot going on in this code, so let's step through each aspect:

  1. The first new command simply calls our loadScores() function to load the previous scores.

  2. Next, we insert the player's most recent score into the 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() command, which we added to game.lua, for setting a Composer-accessible variable which we can now access in this scene (highscores.lua). Following that, we immediately reset its value to 0 since we won't need further record of it.

  3. At this point, we don't know where our score ranks among the existing high scores. It might not even beat the lowest score among the ten highest! To check this, we sort the table's values from highest to lowest. This will automatically place our score in the correct order. If it's not high enough to make the top ten, it will remain in the 11th slot, but since we'll only show ten scores, you won't see it on the screen.

    To sort the table, we use the Lua table.sort() command. For this to work, you need to 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.

    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 "is a greater than b?" If true, it returns 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.

  4. With the table now sorted, we save the data back out to scores.json by calling our saveScores() function.

  5. Following this, we create the scene background and a text object — nothing special here; by now you're an expert at this!

  6. Next we display the scores using a simple for loop from 1 to 10. For each score, we first set the intended y position by calculating a local yPos variable. This will make the scores run down the screen, evenly spaced apart.

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. Most of this code should be straightforward at this point, 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 right-aligned and each score is left-aligned.

To accomplish this, we can 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 right-aligned with the others, we set 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.

  1. Finally, we create a button (text object) to go back to the menu. This is just like the text buttons you created in the menu scene, and its "tap" event listener will call a basic gotoMenu() function which we'll write next.

Leaving the Scene

Back up in the scene-accessible code area, we need to include the function which handles the "tap" event for the menuButton object. This is straightforward enough:

local function gotoMenu()
    composer.gotoScene( "menu", { time=800, effect="crossFade" } )
end
Action!

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.

Chapter Concepts

We've covered several more important concepts in this chapter:

Command/Property Description
system.pathForFile() Generates an absolute path using system-defined directories as the base.
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 JSON-encoded string.
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.