Blog 3 - Precision Timing and Framerates
When we found out that On A Roll 3D had been accepted for release on Steam, we blew the virtual dust off the source code and jumped straight back into development. Of course, the game was already written, but with a ton of Steam functionality to take advantage of through their API, there was much work to be done. One such feature that we were keen to implement was leaderboards. From our own gaming experience being able to spend hours retrying a level to perfect your score or time, then comparing it with other players (often only to find that there are a lot of people who can both score more and go faster than you) is one of the many joys of gaming.
Steam supports leaderboards for both scores (or any other numeric values) and times. With On A Roll 3D having a scoring system, the scores leaderboard was easy - we simply set up a single leaderboard to store each player's best score in a single playthrough. This is the main leaderboard for the game and the one most players use to determine how "good" they are compared to others. With each area of each level being timed, we also decided to set up "time" leaderboards for each of the 24 areas in the game, and this is where we hit a bit of a conundrum.
Steam's time leaderboards can go down to a precision of milliseconds, or thousandths of a second. On A Roll 3D runs at a typical 60 frames per second, which means the logic that updates the game every frame does so 60 times each second. Part of this logic is to detect whether the player has completed the current area, and to calculate and record a time if they have (and post it to the relevant leaderboard). This is all well and good until you realise that times will only be posted down to an accuracy of 1/60th of a second, which seems a shame if a precision of 1/1000th of a second is allowed. It also increases the chances of duplicate times, which could be recorded as such even if one of the players was slightly (less than 1/60th of a second) faster.
For example, consider the update logic that checks when a player has crossed the finish line to complete an area. Starting from, for argument's sake, frame 12345:
Frame Time (min:sec) Area Finished? Action
12345 3:25.750 No Still not finished area, check again next frame...
12346 3:25.767 No Still not finished area, check again next frame...
12347 3:25.783 No Still not finished area, check again next frame...
12348 3:25.800 Yes Area complete! Record time of 3:25.800 to leaderboard and move onto next area...

This works, but the time of 3:25.800 is not necessarily as accurate as it could be. What this time represents is a time somewhere between 3:25.783 and 3:25.800. But how can we be more specific if we're only checking every 0.017 seconds?
Frame 12346
3:25.767
Frame 12347
3:25.783
Frame 12348
3:25.800
32% 68%

This clever diagram explains how! The red lines indicate the position of the character on each frame, and the blue line represents the line that the user must cross to complete the area (this is invisible in the game, but it exists nonetheless). By knowing the position of the character on the previous and current frames and the position of the finish line when the character crosses it, it is possible to work out how far through the previous frame the character crossed the line. Character positions are stored as floating-point values, which are very accurate, and certainly allow enough precision to calculate times to the nearest millisecond.
So, we now know that the character didn't cross the finish line right on frame 12348, they crossed the line on frame 12347 plus 32% of a frame. From this we can calculate a far more accurate time of 3:25.789. It is this time that is then posted to the relevant leaderboard.
We've used this technique before for Avatar Grand Prix 2 (we hadn't started blogging back then!). In fact, it is arguably even more important for this game because two players can be racing on the same screen. During the development of the game before we implemented this technique we did see instances where a car that finished marginally ahead was awarded the same time as the car just behind it.