Posted on by

highscoreOne frequently-asked question in the forums, especially from those developers new to Corona SDK and Lua, is “How do I keep score?”. Today’s tutorial will walk you through the entire process, including:

  1. Setting up a variable to hold the score.
  2. Displaying the score.
  3. Saving (and retrieving) the score for future use.

The Score Module

Corona does not have a built-in score module, so let’s build one which you can implement in your apps, beginning with a basic “initialization” function:

local M = {}  --create the local module table (this will hold our functions and data)
M.score = 0  --set the initial score to 0

function M.init( options )
   local customOptions = options or {}
   local opt = {}
   opt.fontSize = customOptions.fontSize or 24
   opt.font = customOptions.font or native.systemFontBold
   opt.x = customOptions.x or display.contentCenterX
   opt.y = customOptions.y or opt.fontSize*0.5
   opt.maxDigits = customOptions.maxDigits or 6
   opt.leadingZeros = customOptions.leadingZeros or false
   M.filename = customOptions.filename or "scorefile.txt"

   local prefix = ""
   if ( opt.leadingZeros ) then 
      prefix = "0"
   end
   M.format = "%" .. prefix .. opt.maxDigits .. "d"

   M.scoreText = display.newText( string.format(M.format, 0), opt.x, opt.y, opt.font, opt.fontSize )
   return M.scoreText
end

As you can see, the options we support are:

  • fontSize — The size of the displayed score text.
  • font — The font used for the displayed score text.
  • x — The x location to draw the score display.
  • y — The y location to draw the score display.
  • maxDigits — The estimated number of the max score.
  • leadingZeros true or false: do you want leading zeros?
  • filename — The local filename to save the score to.

Most of these are straightforward, but we could extend this with alignment properties and other settings. For example, we could add options to control the anchor points for left- or right-aligned score text.

The score text uses the string.format() API call to format the number, and the string will either be prefixed by spaces or by zeros, depending on the settings. This format setting (M.format) is also saved to the module for usage in other functions. If any settings aren’t specified, then we fall back to the reasonable defaults, in this case, 24-point Helvetica, centered at the top of the screen, with a maximum of 6 digits. By default, the local save file target is scorefile.txt, but this can be changed to another file name.

Set and Get Functions

Next, let’s write some functions for settinggetting, and adding to the score:

function M.set( value )
   M.score = value
   M.scoreText.text = string.format( M.format, M.score )
end

function M.get()
   return M.score
end

function M.add( amount )
   M.score = M.score + amount
   M.scoreText.text = string.format( M.format, M.score )
end

If we set the score, the display will update and overwrite the current value with the new one. The get method simply returns the current score for some other use. Finally, the add function allows us to add to the current score and update the display. This could be extended to a subtract function, but it’s more efficient to just pass a negative value to the add function.

Saving and Loading the Score

The last thing our module needs is the ability to save and load the score to a file. This is required so that the score can be saved and retrieved between app sessions.

function M.save()
   local path = system.pathForFile( M.filename, system.DocumentsDirectory )
   local file = io.open(path, "w")
   if ( file ) then
      local contents = tostring( M.score )
      file:write( contents )
      io.close( file )
      return true
   else
      print( "Error: could not read ", M.filename, "." )
      return false
   end
end

function M.load()
   local path = system.pathForFile( M.filename, system.DocumentsDirectory )
   local contents = ""
   local file = io.open( path, "r" )
   if ( file ) then
      -- read all contents of file into a string
      local contents = file:read( "*a" )
      local score = tonumber(contents);
      io.close( file )
      return score
   else
      print( "Error: could not read scores from ", M.filename, "." )
   end
   return nil
end

return M

As seen on lines 40 and 54, the score file exists in the system.DocumentsDirectory. We cannot write it to system.ResourceDirectory since it’s read-only, and both system.TemporaryDirectory and system.CachesDirectory are prone to automatic cleanup by the system. So, logically, we create and keep this file inside the documents directory so it persists between app sessions.

Let’s quickly inspect the load() function in more depth. This function opens the file, reads the value into a local variable, and returns it. It does not, by design, update the text value on the screen. In many cases, this function will be used to load the last saved score, compare it to the current score, and check if we have a new “high score.” However, if we need to both load the score and display the last saved value, we can simply call the set() function using the value we just retrieved from the load() function.

Finally, on line 68, we return the module’s M table back to the caller, thus making the functions and data available to the caller.

Putting it to Use

With our module created and the basic functions written, we can now use it within our app.

local score = require( "score" )

local scoreText = score.init({
   fontSize = 20,
   font = "Helvetica",
   x = display.contentCenterX,
   y = 20,
   maxDigits = 7,
   leadingZeros = true,
   filename = "scorefile.txt",
})

The first line is the mandatory require line which includes the module in our project, providing access to all of the functions we wrote above. The second block calls the init() function with a series of basic parameters. This creates the text display, centers it at the top of the screen, and sets it to a zero-filled seven-digit score.

From this point, the following actions can be executed with just one line:

  • score.set( value ) — sets the value.
  • local myscore = score.get() — gets the current value.
  • score.add( value ) — add value to the current score.
  • score.save() — save the score.
  • local highscore = score.load() — load the previously-saved score.

To see our code in action, please download a version of the ManyCrates sample app that has been modified to work with this module. When an object is touched, the score increases and the object is removed from the display. For variety, each object has a value set for it: 50 points for a small crate, 100 points for a large crate, and 500 points for a green container. Click on each object and watch the score increase. Two buttons have been added to showcase the save() and load() functions. To test it, save the score, reload the app (Control-R or Cmd-R), and then load the score that was just saved. Notice that the text display updates to that value!

Hopefully this tutorial exhibits just how easy it is to work with scores in Corona SDK, including saving and loading scores to the device for persistence between app sessions.


Posted by . Thanks for reading...

27 Responses to “Tutorial: How to Display and Save Scores”

  1. Bryan Mitchell

    Is there a way for Corona to hook into the NSUserDefaults? I’d like to update my game Geared to the corona engine but I saved all of my players data using NSUserDefaults. This was a silly mistake (I was pretty new to game development when I started that game in 2009), but the data was also super simple. (A level is either completed or its not).

    Reply
  2. Joel

    Hi Corona,

    Thanks for the article..

    Say in my game , I save the user data like currentLevel, currentScore etc .. when I want the users to update to a new version – will this data be erased ?.. how is this handled in Corona SDK , for Android ..

    regards,
    Joel

    Reply
    • Rob Miracle

      When people upgrade, IOS and Android preserve the app’s sandbox files ( system.DocumentsDirectory, system.CachesDirectory and probably system.TemporaryDirectory). The only thing that gets replaced is the app’s bundle itself, so your data files are protected unless they delete the app. If they delete the app, the data goes away too.

      But the upgrade process is non-destructive.

      Reply
  3. Arun

    I have used the code in this tutorial and it works great!

    But how do I show the highscore? When i put the highscore code in, it doesn’t come up with an error, but it doesn’t seem to do anything! How can I show the highscore on screen?

    thanks.

    Reply
  4. lORI

    I am a “Newbie” to programming and I am having trouble saving my score. Initially I set it up using the widget buttons and I was able to make it work; I’ve now removed the buttons. I’ve structured my game using Composer and I am using the code from the tutorial “How to Display and Save Scores posted on December 10, 2013. I’ve written the first level and the actual game works fine but I am having trouble saving the score. I am able to display the final game score which is the current game score plus a calculated bonus score; however, I am not able to save the score and then retrieve it in future sessions to determine if the current score is higher than the previous score and therefore should overwrite the previous score. Below is the code related to the scoring that I am using; I only included the line adding the score with the bonus score as everythng is working. Every time I load the previous score, the code converts the “nil” to zero which I assume means I am not writing the score to the file correctly. I would really appreciate some guidance on what I am doing wrong. Ultimate goal: save the score for the first game and then overwrite the saved score for that level if the score is surpassed in future attempts. Thanks!

    –level1.lua

    local score = require (“score”)

    local gameScore = 0

    local scoreText = score.init({
    fontSize = 50,
    font = “Helvetica”,
    x = display.contentCenterX,
    y = 820,
    maxDigits = 7,
    leadingZeros = true,
    filename = “scorefile.txt”,
    })

    ———————————————————————————
    –Calculates the current Game Score
    ———————————————————————————
    function currentGameScoreCalc()

    print (“function currentGameScoreCalc”)

    gameScore = solidTotal + bonusScore — this total appears on the screen so I can see that it is being calculated correctly

    print (“—————————”)
    print (“……..Current Game Score………. = “..gameScore) — the gameScore prints on the scoreen
    print (“—————————”)

    loadHighScore()

    end

    ———————————————————————————
    –LOAD HIGH SCORE FOR THIS LEVEL
    ———————————————————————————
    function loadHighScore()
    score.load(“scorefile.txt”)
    score = tonumber(score)
    if score == nil then score = 0 end
    print (“The score loaded = “..score) — it always prints zero so I assume that means that I am not writing the final score to the table in the code below
    highScore = score
    compareScore()
    end

    ———————————————————————————–
    –COMPARE SCORES and SAVE
    ————————————————————————————
    function compareScore()

    if gameScore > highScore then
    highScore = gameScore
    print (“the new high score = “..highScore)

    score.add()
    score.set()
    score.save()

    print(“^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^”)
    print (“Game Score is NEW HIGH SCORE = “..gameScore)
    print (“The High Score = “..highScore)
    print(“^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^”)

    end
    ————————————————————————————————————————————————————

    ————————————–
    – Score Module
    ————————————-

    local M = {} — create our local M

    M.score = 0

    function M.init( options )
    local customOptions = options or {}
    local opt = {}
    opt.fontSize = customOptions.fontSize or 50
    opt.font = customOptions.font or native.systemFontBold
    opt.x = customOptions.x or display.contentCenterX
    opt.y = customOptions.y or opt.fontSize * 0.5
    opt.maxDigits = customOptions.maxDigits or 6
    opt.leadingZeros = customOptions.leadingZeros or false
    M.filename = customOptions.filename or “scorefile.txt”

    local prefix = “”
    if opt.leadingZeros then
    prefix = “0″
    end
    M.format = “%” .. prefix .. opt.maxDigits .. “d”

    M.scoreText = display.newText(string.format(M.format, 0), opt.x, opt.y, opt.font, opt.fontSize)
    return M.scoreText
    end
    —————————————————
    –SET FUNCTION
    —————————————————
    function M.set()
    M.score = value
    M.scoreText.text = string.format(M.format, M.score)

    end

    —————————————————
    –GET FUNCTION
    —————————————————
    function M.get()
    return M.score
    end
    —————————————————
    –ADD FUNCTION
    —————————————————
    function M.add()
    M.score = M.score + highScore
    M.scoreText.text = string.format(M.format, M.score)
    end

    —————————————————
    –SAVE FUNCTION
    —————————————————
    function M.save()
    local path = system.pathForFile( M.filename, system.DocumentsDirectory)
    local file = io.open(path, “w”) — io.open opens a file at path. returns nil if no file found
    if file then
    local contents = tostring( M.score )
    file:write( contents ) — write game score to the text file
    io.close( file )
    return true
    else
    print(“Error: could not read “, M.filename, “.”)
    return false
    end
    end

    —————————————————
    –LOAD FUNCTION
    —————————————————
    function M.load()
    local path = system.pathForFile( M.filename, system.DocumentsDirectory)
    local contents = “”
    local file = io.open( path, “r” )
    if file then
    — read all contents of file into a string
    local contents = file:read( “*a” )
    local score = tonumber(contents);
    io.close( file )
    return score

    else
    file = io.open (path, “w”)
    file:write(“0″)
    io.close(file)
    return “0″
    ——————————————————-
    end
    print(“Could not read scores from “, M.filename, “.”)
    return nil

    end

    return M

    Reply
    • Rob Miracle

      You can increase the score any way you feel like. Scores don’t have to go up, they could go down (think Golf, Trivia games where you start at 1500 points and loose 100 points per second in the 15 seconds you have to answer). What value you use for the score and how you calculate it are all up to you. This module only deals with giving you a display object to show the score and a way to save it.

      Reply
  5. ToeKnee

    Hi,
    I’m new to modules.

    local scoreText = score.init({…..})

    So this creates a local “name” attached to the result of the function running within the module.
    Now when used within composer and the scene. Can we just…

    sceneGroup:insert( scoreText )

    to add it to the scene – or because it was created ( display.newText(…) ) in the score module does it need to be handled a different way when combined with composer/storyboard??

    Thanks
    T.

    Reply
    • Rob Miracle

      The function returns the display.newText() that holds the score, so you can insert it directly into a group.

      Rob

      Reply
    • Rob Miracle

      I hate answering a question with a question, but do you want the player to be able to resent the high score or you do want to just for testing purposes?

      If you want the player to, you will have to give them some kind of button that executes some code to set the score to 0. These two lines should do the reset. You of course will need to provide the button and the handler function.

      score.set( 0 )
      score.save()

      Now if you want to just reset things to test, from the Corona Simulator menu at the top, do: File->Show Project Sandbox and open the highlighted folder. Go into the Documents folder there and delete your scores.json file. Relaunch the simulator.

      Rob

      Reply
  6. Semir

    This seems nice. When I get a high score it saves it and displays it like it’s suppose to. However, when I exit the app and open it another time, the high scores are not there anymore. Can you please tell me why it does that and how I can fix this problem?

    Reply
  7. Jeff Zivkovic

    Hello,

    Thank you for this fine tutorial, Rob. I got a lot out of it. I’m trying to modify this module to save a separate score for each level. My efforts so far havebeen to change the initial variable assignment from

    M.score = 0

    to

    M.score = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24}.

    So that each entry in the table represents the current high score for that level Then, every time a score module is called, I also pass an additional parameter, currentLevel. Such as,

    score.add(1, currentLevel)

    Of course, I’ve adjusted every instance of “score” in the module to “score[currentLevel]“. The add module appears to be working, but the “get” module does not. I’m sure I’m missing something simple to make this work properly.

    Thanks again,

    Jeff

    Reply
    • Rob Miracle

      Jeff, can I get you to ask this over in the forums. The comments munge up code displays, so posting code here isn’t very good. Also, very few people look at the comments. You will have the whole community be able to offer you suggestions in the forums.

      Thanks
      Rob

      Reply

Leave a Reply

  • (Will Not Be Published)