Chapter 6 — Implementing High Scores

Previous | Next

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.

Composer-Accessible Data

For the most part, when you use Composer, scenes remain self-contained, 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 new 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:

Armed with these commands, let's update the endGame() function:

  1. Open your game.lua file.

  2. 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 ), 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 second command simply redirects the app to the highscores.lua scene instead of the menu scene.

  1. Save your modified 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.

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:

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

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 Solar2D 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, add the following 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:

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

Saving data is just as easy as reading data. Following the previous function in the scene-accessible code area, add a 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:

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

  1. Before we can do anything with score data, we need to load the existing scores. Let's do so by calling the 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
  1. Next, let's 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 — 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.
    -- 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
  1. 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, let's 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, 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 "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.

  1. With the table now sorted, let's save the data back out to 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
  1. Following this, let's create the scene background and a text object — nothing special here; by now you're an expert at this!
    -- 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
  1. Next, let's display the scores using a simple 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, Solar2D 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, 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 right-aligned with the others, we set anchorX to 1, and for each score number, we set anchorX to 0 for left alignment.

Naturally, Solar2D 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, let's 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.
        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

Leaving the Scene

Back up in the scene-accessible code area, we need to add 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


-- -----------------------------------------------------------------------------------
-- Scene event functions
-- -----------------------------------------------------------------------------------

Scene Cleanup

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.

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.