Monday 6 August 2012

Racing Game - HUD and Timer



As expected, I left the housekeeping for a while and carried on with the fun stuff :) There's a couple of things worth a little mention today:
  1. How to render a heads up display
  2. Introduction of a new timer class

 

Heads Up!


My camera follows the car as it progresses through the level. The HUD, however, needs to remain in the same spot at all times. How to achieve this? A second camera of course! Ok, it wasn't that obvious to me either, but thanks to the wonders of google it's really quite easy to find the solution to your problems. If only there were search engines (and internet access for that matter!) available to me back in the 80's when I was grappling with the finer points of sinclair basic!

This is what set me off in the right direction, it was really straightforward altering that example to fit what was needed in the Race Game. Here are the relevant additions to the GameScreen class:

public class GameScreen implements Screen {
    private OrthographicCamera hudCam;
    private SpriteBatch hudSpriteBatch;
    private BitmapFont font;
    private TextureRegion hudLayoutTextureRegion;
    // lots of unrelated stuff omitted

    public GameScreen(RaceGame game) {
        hudCam = new OrthographicCamera(RaceGame.WIDTH, RaceGame.HEIGHT);
        hudSpriteBatch = new SpriteBatch();
        font = new BitmapFont();
        hudLayoutTextureRegion = atlas.findRegion("hudLayout");
        // lots of unrelated stuff omitted
    }

    public void render(float deltaTime) {
        // camera focus: x-axis middle of level, y-axis middle of level, z-axis ignored
        hudCam.position.set(RaceGame.WIDTH/2, RaceGame.HEIGHT/2, 0.0f);
        // update camera
        hudCam.update();
        hudCam.apply(Gdx.gl10);
        // set the projection matrix
        hudSpriteBatch.setProjectionMatrix(hudCam.combined);
        // draw something
        hudSpriteBatch.begin();
        hudSpriteBatch.draw(hudLayoutTextureRegion, 0, RaceGame.HEIGHT - 42, RaceGame.WIDTH, 42);
        font.setColor(0.0f, 0.0f, 0.0f, 1.0f);
        font.draw(hudSpriteBatch, ((TimeUtils.nanoTime()-level.getStartTime())/1000000)+"ms", 10.0f, RaceGame.HEIGHT - 20);
        font.draw(hudSpriteBatch, level.getTimer().getElapsed() , 10.0f, RaceGame.HEIGHT - 16);
        font.draw(hudSpriteBatch, level.getCar().getMph(), RaceGame.WIDTH/2 - 13, RaceGame.HEIGHT - 4);
        // TODO Replace placeholder string with real best time once implemented
        font.draw(hudSpriteBatch, "01:23:45", RaceGame.WIDTH - 68, RaceGame.HEIGHT - 16);
        // TODO Replace placeholder x-position with value between 10 and 223 (based on progress percent)
        hudSpriteBatch.draw(progressIndicatorTextureRegion, 50, RaceGame.HEIGHT -38, 7, 7);
        hudSpriteBatch.end();
        // lots of unrelated stuff omitted
    }
}


So there we have it, a second (static) camera, with a backgound texture and some text rendered to it, that overlays the standard game camera. Marvellous!

Timer


I've separated the timer functionality out into its own class. The level instantiates a timer, the timer starts on load (later I'll move this to the race start state if I introduce a pre-race animation), the timer is stopped on touching the finish line, and it is reset and then started again on restarting the level. None of this is new, but it's a lot cleaner and tidier now all the timer functionality is encapsulated in its own class. What is new is the formatTime function - this takes a time in milliseconds, splits it into its component parts (minutes, seconds, and hundredths of a second), pads these out with leading zeros if necessary to ensure each value is two digits long at all times, and pulls the whole lot together into a nicely formatted time string for ingame use.

Here's the code:

package com.mrdt.racegame;

public class Timer {
    private final long nanosPerMilli = 1000000;
    private long startTime = 0;
    private long stopTime = 0;
    private boolean running = false;

    // Start measuring
    public void start() {
        this.startTime = System.nanoTime();
        this.running = true;
    }

    // Stop measuring
    public void stop() {
        this.stopTime = System.nanoTime();
        this.running = false;
    }

    // Reset
    public void reset() {
        this.startTime = 0;
        this.stopTime = 0;
        this.running = false;
    }

    // Get elapsed milliseconds
    public long getElapsedMilliseconds() {
        long elapsed;
        if (running) {
             elapsed = (System.nanoTime() - startTime);
        }
        else {
            elapsed = (stopTime - startTime);
        }
        return elapsed / nanosPerMilli;
    }

    // Get formatted elapsed time
    public String getElapsed() {
        String timeFormatted = "";
        timeFormatted = this.formatTime(this.getElapsedMilliseconds());
        return timeFormatted;
    }

    // Helper method splits time into minutes, seconds, hundredths, and formats
    private String formatTime(final long millis) {
        int minutesComponent = (int)(millis / (1000 * 60));
        int secondsComponent = (int)((millis / 1000) % 60);
        int hundredthsComponent = (int)((millis / 10) % 100);
        String paddedMinutes = String.format("%02d", minutesComponent);
        String paddedSeconds = String.format("%02d", secondsComponent);
        String paddedHundredths = String.format("%02d", hundredthsComponent);
        String formattedTime;
        if ((millis>0)&&(millis<3600000)) {
            formattedTime = paddedMinutes+":"+paddedSeconds+":"+paddedHundredths;
        }
        else {
            formattedTime = 59+":"+59+":"+99;
        }
        return formattedTime;
    }
}


And here's the result:



Best time and progress indicator are hard-coded for now, but will be implemented properly soon enough. It definitely feels like the game is starting to take shape :)

As usual, the full source can be found on github. See you next time!


          

1 comment:

  1. Thanks for this article TheInvader360. It was tremendously helpful.

    ReplyDelete