Building an Incrementing Counter

When you complete a level in a game, it sometimes shows your score in an animated fashion. Sometimes this involves the numbers "spinning" such that they count up from the right-most column to the next column over and so on, like an odometer of a car but much faster.

Achieving this effect in Corona involves some simple math. Let's start with some initial setup:

local scoreText = display.newText( "000000", display.contentCenterX, 60, native.systemFont, 48 )

This code is very straightforward — we just create a text object, scoreText, with an initial display of 000000, positioned in the top-center area of the screen.

Controlling the Rate

The first thing to understand is that Corona is a frame-based system. This means that the screen updates essentially at a fixed pace, usually referred to as frames per second. At 60 frames per second, the screen can only update every 1/60th of a second (16.6667 milliseconds), so trying to force a faster update rate is an exercise in futility.

Obviously, game scores can vary greatly — one game may have a maximum score of 100 while another will have a maximum score in the millions. Thus, we don't want the counter to count up by a fixed amount like +1 on each frame update. If so, a score of 1,000,000 would take too far long to update — approximately 16667 seconds at 60 frames per second — while a score like 100 would finish far too quickly (about 1.67 seconds).

A better solution is to determine how long the animation should run and then calculate the amount that it should increment on each frame update.

Determining the Increment

Determining the amount by which to increment the score can be done via a method called linear interpolation. This basically allows you to compute intermediate points between two values based on some criteria. Let's add a function for this to our existing code:

local scoreText = display.newText( "000000", display.contentCenterX, 60, native.systemFont, 48 )

local function lerp( v0, v1, t )
    return v0 + t * (v1 - v0)
end

This lerp() function is straightforward enough. We take the difference between the start and stop values (v1 - v0) and multiply it by the fraction of the time (t) which we want to know the increment for (more on this below). Then we add that to the start value (v0).

Incrementing the Score

Now let's add a incrementScore() function which will do most of the actual work:

local scoreText = display.newText( "000000", display.contentCenterX, 60, native.systemFont, 48 )

local function lerp( v0, v1, t )
    return v0 + t * (v1 - v0)
end

local function incrementScore( target, amount, duration, startValue )

    local newScore = startValue or 0
    local passes = (duration/1000) * display.fps
    local increment = lerp( 0, amount, 1/passes )

    local count = 0
    local function updateText()
        if ( count <= passes ) then
            newScore = newScore + increment
            target.text = string.format( "%06d", newScore )
            count = count + 1
        else
            Runtime:removeEventListener( "enterFrame", updateText )
            target.text = string.format( "%06d", amount + (startValue or 0) )
        end
    end

    Runtime:addEventListener( "enterFrame", updateText )
end

As you can see, this function accepts four parameters:

Inside the function, we essentially take these steps:

  1. We set a temporary value, newScore, with a value of the startValue parameter (or 0 if startValue is not supplied).

  2. Next, we calculate the number of "passes" necessary to reach the final score value. This is determined by the specified duration, converted from milliseconds to seconds, multiplied by the app's frames-per-second setting. For example, if we specify a duration of 4000 (4 seconds) and the app runs at 60 frames per second, that results in 4 * 60 or 240 passes.

  3. Following this, we run the lerp() function using the calculated values. This returns the amount by which to increment the counter on each pass, and we store that as the increment variable.

  4. In the next block, we utilize a basic counter variable (count) and a nested updateText() function. This function will run once per frame (as commanded in step #5) for the total number of passes, adding the increment value to the score each time. That updated value is then set as the text property of the target display object (scoreText) but not before it's formatted using string.format() to appear as 6 total digits with leading zeros.

When the counting duration is complete, we stop the per-frame execution of the updateText() function and then set the score counter to the final value, once again formatted to appear as 6 total digits with leading zeros.

Notes
  • The proper format (string.format()) of the score counter value will depend on your particular scenario. If you're only counting up to a maximum score of 100, you probably shouldn't format it as 6 digits with leading zeros — in that case, a format specification of "%03d" (3 digits) would be better than "%06d". Also note that formatting is entirely optional and you don't need to apply any formatting if it's not desired.

  • For further exploration of string formatting, please see the Formatting String Values tutorial.

  1. On the final line inside the function (line 25), we simply start the updateText() function executing on each runtime frame (as noted above, this process is stopped on line 20 when the count-up is complete).

Running the Process

Running the counting process is simple — just call the incrementScore() function with the required parameters, for example:

incrementScore( scoreText, 750800, 4000 )

This will make the scoreText text object count from 000000 up to 750800 over the total timespan of 4 seconds.

Conclusion

Hopefully this tutorial gets you started with a classic "spinning score counter" feature for your game. With a little creativity, you can adjust this to fit nearly any scenario and any maximum score threshold!