Skip to content

heads_up_display

JNightRide edited this page Dec 31, 2023 · 5 revisions

Heads up display

The final piece our game needs is a User Interface (UI) to display things like score, a "game over" message, and a restart button.

Create a new class MainSceneAppState in package e.g.dodgethecreeps.screen.

package e.g.dodgethecreeps.screen;

public class MainSceneAppState extends AbstractScreen {
}

To reuse the code, open the AbstractScreen (parent) class. We configure the root container for the user interface.

package e.g.dodgethecreeps.screen;
...
public abstract class AbstractScreen extends BaseAppState {

    /** UI root container. */
    protected Container rootContainer;

    /** Label for messages. */
    protected Label message;

    @Override
    protected void initialize(Application app) {
        AppSettings settings = ((Dodgethecreeps) app).getSettings();
        ControlLayout layout = new ControlLayout(ControlLayout.onCreateRootPane(new Vector3f(settings.getWidth(), settings.getHeight(), 0),
                                                                                new Vector3f(settings.getWidth(), settings.getHeight(), 0)));
        rootContainer = new Container(new ElementId("null"));
        rootContainer.setPreferredSize(new Vector3f(layout.getRootPane().getResolution().clone()));
        rootContainer.setLayout(layout);

        message = layout.addChild(new Label(""), ControlLayout.Alignment.Center);
        message.setTextHAlignment(HAlignment.Center);
        message.setTextVAlignment(VAlignment.Center);
        message.setFont(GuiGlobals.getInstance().loadFont("Interface/Fonts/Xolonium.fnt"));
        message.setColor(new ColorRGBA(1.0F, 1.0F, 1.0F, 1.0F));
        message.setPreferredSize(new Vector3f(rootContainer.getPreferredSize().x, 200, 0));

        layout.setAttribute(ControlLayout.FONT_SIZE, message, 55.0F);
    }

    @Override
    protected void cleanup(Application app) {

    }

    @Override
    protected void onEnable() {
        Dodgethecreeps app = (Dodgethecreeps) getApplication();
        app.getGuiNode().attachChild(rootContainer);

        AppSettings settings = app.getSettings();
        rootContainer.setPreferredSize(new Vector3f(settings.getWidth(), settings.getHeight(), 0));
        rootContainer.setLocalTranslation(0, settings.getHeight(), 0);
    }
    
    @Override
    protected void onDisable() {
        rootContainer.removeFromParent();
    }
}

In state MainSceneAppState we must show a button and the game title. Create a variable of type Button and initialize it by overriding the initialize(Application app) method.

Add a 'click' event to the button, the event is responsible for changing state.

package e.g.dodgethecreeps.screen;
...
public final class MainSceneAppState extends AbstractScreen {

    private Button startButton;

    public MainSceneAppState() { }

    @Override
    protected void initialize(Application app) {        
        super.initialize(app);
        ControlLayout layout = (ControlLayout) rootContainer.getLayout();
        
        startButton = layout.addChild(new Button("Start", new ElementId("StartButton")), ControlLayout.Alignment.CenterBottom);
        startButton.setTextHAlignment(HAlignment.Center);
        startButton.setTextVAlignment(VAlignment.Center);
        startButton.setFont(GuiGlobals.getInstance().loadFont("Interface/Fonts/Xolonium.fnt"));
        startButton.setPreferredSize(new Vector3f(200, 100, 0));
        startButton.addClickCommands((source) -> {
            setEnabled(false);
            getApplication().getStateManager().getState(GameSceneAppState.class).setEnabled(true);
        });
        
        layout.setAttribute(ControlLayout.POSITION, startButton, new Vector3f(0, 100, 0));
        layout.setAttribute(ControlLayout.FONT_SIZE, startButton, 50.0F);
        
        message.setText("Dodge the\nCreeps!");
    }
}

Open class GameSceneAppState, create the following variables:

...
private Label scoreLabel;
...

initialize label scoreLabel in overridden method initialize(Application app):

...
ControlLayout layout = (ControlLayout) rootContainer.getLayout();

//----------------------------------------------------------------------
//                              HUD
//----------------------------------------------------------------------
scoreLabel = layout.addChild(new Label("0"), true, ControlLayout.Alignment.CenterTop);
scoreLabel.setTextHAlignment(HAlignment.Center);
scoreLabel.setTextVAlignment(VAlignment.Center);
scoreLabel.setFont(GuiGlobals.getInstance().loadFont("Interface/Fonts/Xolonium.fnt"));
scoreLabel.setColor(new ColorRGBA(1.0F, 1.0F, 1.0F, 1.0F));
scoreLabel.setPreferredSize(new Vector3f(300, 45, 0));

layout.setAttribute(ControlLayout.POSITION, scoreLabel, new Vector3f(0, 10, 0));
layout.setAttribute(ControlLayout.FONT_SIZE, scoreLabel, 45.0F);
...

We now want to display a message temporarily, such as "Get Ready", so we add the following code.

In method onEnable():

...
message.setText("Get Ready!");
scoreLabel.setText("0");
...

We also need to process what happens when the player loses. The following code will display "Game Over" and then return to the home screen, after a short pause.

In method gameOver():

...
player = null;
        
mobTimer.stop();
scoreTimer.stop();
        
message.setText("Game Over");
messageTimer.start();
...

Add the code below to HUD to update the score:

private final TimerTask _on_ScoreTimer_timeout = () -> {
...
    scoreLabel.setText(String.valueOf(score));
...
};

In task _on_StartTimer_timeou, clean the label.

...
private final TimerTask _on_StartTimer_timeou = () -> {
...
    message.setText("");
...
};
...

Connecting HUD to Main

In task _on_MessageTimer_timeout of timer messageTimer add:

...
private final TimerTask _on_MessageTimer_timeout = () -> {
    setEnabled(false);
    messageTimer.stop();
};
....

It's time to prepare for the change between states.

In method onDisable():

@Override
protected void onDisable() {
...
    getState(MainSceneAppState.class).setEnabled(true);
}

In the constructor of this class, disable.

...
public GameSceneAppState() {
    setEnabled(false);
}
...

In the main class Dodgethecreeps, register in the new state.

@Override
public void simpleInitApp() {
...
    // scene states; where the game is managed
    MainSceneAppState sceneAppState = new MainSceneAppState();
    stateManager.attach(gameAppState);
...
}

Code

At the moment your code should look similar to the following:

package e.g.dodgethecreeps.screen;
...
public class GameSceneAppState extends AbstractScreen {

    private Dyn4jAppState<PhysicsBody2D> dyn4jAppState;
    private TimerAppState timerAppState;
    private Player player;
    private Node rootNode;

    private Label scoreLabel;

    private MobSpawnLocation mobSpawnLocation;
    private Timer mobTimer,     // Enemy timer; Manage the spawn time of enemies.
                  scoreTimer,   // Score timer; Manage time to earn a score.
                  startTimer,   // Manage some preparation time before starting the game.
                  messageTimer; // Timer to display a "Game Over" message

    private int score = 0;

    public GameSceneAppState() {
        setEnabled(false);
    }

    @Override
    protected void initialize(Application app) {
        super.initialize(app);
        Dodgethecreeps application = (Dodgethecreeps) app;
        ControlLayout layout = (ControlLayout) rootContainer.getLayout();

        //----------------------------------------------------------------------
        //                              HUD
        //----------------------------------------------------------------------
        scoreLabel = layout.addChild(new Label("0"), true, ControlLayout.Alignment.CenterTop);
        scoreLabel.setTextHAlignment(HAlignment.Center);
        scoreLabel.setTextVAlignment(VAlignment.Center);
        scoreLabel.setFont(GuiGlobals.getInstance().loadFont("Interface/Fonts/Xolonium.fnt"));
        scoreLabel.setColor(new ColorRGBA(1.0F, 1.0F, 1.0F, 1.0F));
        scoreLabel.setPreferredSize(new Vector3f(300, 45, 0));

        layout.setAttribute(ControlLayout.POSITION, scoreLabel, new Vector3f(0, 10, 0));
        layout.setAttribute(ControlLayout.FONT_SIZE, scoreLabel, 45.0F);

        rootNode = new Node("MyRootNode");
        application.getRootNode().attachChild(rootNode);

        dyn4jAppState = getState(Dyn4jAppState.class);
        timerAppState = getState(TimerAppState.class);

        mobTimer     = timerAppState.attachTimer("MobTimer",     new Timer(0.5F).attachTask(_on_MobTimer_timeout), 0.60F);
        scoreTimer   = timerAppState.attachTimer("ScoreTimer",   new Timer(0.75F).attachTask(_on_ScoreTimer_timeout), 0.60F);
        startTimer   = timerAppState.attachTimer("StartTimer",   new Timer(1.05F).attachTask(_on_StartTimer_timeou), 0.60F);
        messageTimer = timerAppState.attachTimer("MessageTimer", new Timer(1.0F).attachTask(_on_MessageTimer_timeout), 0.60F);

        Camera cam = application.getCamera();
        mobSpawnLocation = new MobSpawnLocation();
        mobSpawnLocation.add(new Vector2(cam.getFrustumLeft(), cam.getFrustumTop()),
                             new Vector2(cam.getFrustumRight(), cam.getFrustumTop()));

        mobSpawnLocation.add(new Vector2(cam.getFrustumRight(), cam.getFrustumTop()),
                             new Vector2(cam.getFrustumRight(), cam.getFrustumBottom()));

        mobSpawnLocation.add(new Vector2(cam.getFrustumRight(), cam.getFrustumBottom()),
                             new Vector2(cam.getFrustumLeft(), cam.getFrustumBottom()));

        mobSpawnLocation.add(new Vector2(cam.getFrustumLeft(), cam.getFrustumBottom()),
                             new Vector2(cam.getFrustumLeft(), cam.getFrustumTop()));
    }

    private final TimerTask _on_MobTimer_timeout = () -> {
        Mob mob = Mob.getNewInstanceMob((Dodgethecreeps) getApplication());

        Vector2 position = mobSpawnLocation.getRandomPath();

        double direction = position.getDirection() + Math.PI;
        direction += ThreadLocalRandom.current().nextDouble(-Math.PI / 4.0, Math.PI / 4.0);

        mob.getTransform().rotate(direction);
        mob.getTransform().setTranslation(position);

        Vector2 velocity = new Vector2(ThreadLocalRandom.current().nextDouble(1.50, 2.50), 0.0);
        mob.setLinearVelocity(velocity.rotate(direction));

        dyn4jAppState.getPhysicsSpace().addBody(mob);
        rootNode.attachChild(mob.getJmeObject());

        mobTimer.reset();
    };

    private final TimerTask _on_ScoreTimer_timeout = () -> {
        score += 1;
        scoreLabel.setText(String.valueOf(score));
        scoreTimer.reset();
    };

    private final TimerTask _on_StartTimer_timeou = () -> {
        player.setEnabled(true);
        message.setText("");

        mobTimer.start();
        scoreTimer.start();

        startTimer.stop();
    };

    private final TimerTask _on_MessageTimer_timeout = () -> {
        setEnabled(false);
        messageTimer.stop();
    };

    public void gameOver() {
        player = null;

        mobTimer.stop();
        scoreTimer.stop();

        message.setText("Game Over");
        messageTimer.start();
    }

    @Override
    protected void onEnable() {
        super.onEnable();
        score = 0;

        player = Player.getNewInstancePlayer((Dodgethecreeps) getApplication());
        player.translate(0, -1.5);
        player.setEnabled(false);

        dyn4jAppState.getPhysicsSpace().addBody(player);
        rootNode.attachChild(player.getJmeObject());

        message.setText("Get Ready!");
        scoreLabel.setText("0");
        startTimer.start();
    }

    @Override
    protected void onDisable() {
        super.onDisable();
        World<PhysicsBody2D> world = dyn4jAppState.getPhysicsSpace().getPhysicsWorld();
        if (world != null) {
            world.removeAllBodies();
        }
        rootNode.detachAllChildren();
        getState(MainSceneAppState.class).setEnabled(true);
    }
}

The game is almost over at this point. In the next and final part, we'll polish it up a bit with looped music and some keyboard shortcuts.


Previous - Next