64x64 icon dark hosting
Choose a hosting plan here and get a free year's subscription to Tuts+ (worth $180).

Make a Neon Vector Shooter With jME: HUD and Black Holes


Start a hosting plan from $3.92/mo and get a free year on Tuts+ (normally $180)

This post is part of a series called Cross-Platform Vector Shooter: jMonkeyEngine.
Make a Neon Vector Shooter in jMonkeyEngine: Enemies and Sounds
Make a Neon Vector Shooter With jME: Particle Effects

So far, in this series about building a Geometry Wars-inspired game in jMonkeyEngine, we've implemented most of the gameplay and audio. In this part, we'll finish the gameplay by adding black holes, and we'll add some UI to display the players score.


Here's what we're working towards across the whole series:

...and here's what we'll have by the end of this part:

As well as modifying existing classes, we'll add two new ones:

  • BlackHoleControl: Needless to say, this will handle the behavior of our black holes.
  • Hud: Here we'll store and display the players score, lives, and other UI elements.

Let's start with the black holes.

Black Holes

The black hole is one of the most interesting enemies in Geometry Wars. In MonkeyBlaster, our clone, it's especially cool once we add particle effects and the warping grid in the next two chapters.

Basic Functionality

The black holes will pull in the player's ship, nearby enemies, and (after the next tutorial) particles, but will repel bullets.

There are many possible functions we can use for attraction or repulsion. The simplest is to use a constant force, so that the black hole pulls with the same strength regardless of the object's distance. Another option is to have the force increase linearly from zero, at some maximum distance, to full strength, for objects directly on top of the black hole. And if we'd like to model gravity more realistically, we can use the inverse square of the distance, which means the force of gravity is proportional to 1/(distance*distance).

We'll actually be using each of these three functions to handle different objects. The bullets will be repelled with a constant force, the enemies and the player's ship will be attracted with a linear force, and the particles will use an inverse square function.


We'll start by spawning our black holes. To achieve that we need another varibale in MonkeyBlasterMain:

Next we need to declare a node for the black holes; let's call it blackHoleNode. You can declare and initialize it just like we did the enemyNode in the previous tutorial.

We'll also create a new method, spawnBlackHoles, which we call right after spawnEnemies in simpleUpdate(float tpf). The actual spawning is pretty similar to spawning enemies:

Creating the black hole follows our standard procedure as well:

Once again, we load the spatial, set its position, add a control, set it to non-active, and finally attach it to the appropriate node. When you take a look at BlackHoleControl, you'll notice that it's not much different either.

We'll implement the attraction and repulsion later, in MonkeyBlasterMain, but there is one thing we need to address now. Since the black hole is a strong enemy, we don't want it to go down easily. Therefore, we add a variable, hitpoints, to the BlackHoleControl, and set its initial value to 10 so that it'll die after ten hits.

We're nearly finished with the basic code for the black holes. Before we get to implementing the gravity, we have to take care of the collisions.

When the player or an enemy comes too close to the black hole, it'll die. But when a bullet manages to hit it, the black hole will lose one hitpoint.

Take a look at the following code. It belongs to handleCollisions(). It's basically the same as for all the other collisions:

Well, you can kill the black hole now, but thats not the only time when it should dissappear. Whenever the player dies, all enemies vanish and so should the black hole. To handle this, just add the following line to our killPlayer() method:

Now its time to implement the cool stuff. We'll create another method, handleGravity(float tpf). Just call it with the other methods in simplueUpdate(float tpf).

In this method, we check all entities (players, bullets and enemies) to see whether they near to a black hole—let's say within 250 pixels—and, if they are, we apply the appropriate effect:

To check whether two entities are within a certain distance of each other, we create a method called isNearby() which compares the locations of the two spatials:

Now that we've checked each entity, if it is active and within the specified distance of a black hole we can finally apply the effect of the gravity. To do that, we'll make use of the controls: we create a method in each control, called applyGravity(Vector3f gravity).

Let's take a look at each of them:



SeekerControl and WandererControl:

And now back to the main class, MonkeyBlasterMain. I'll give you the method first and explain the steps underneath it:

The first thing we do is calculate the Vector between the black hole and the target. Next, we calculate the gravitational force. The important thing to note is that we—once again—multiply the force by the time that has gone by since the last update, tpf, in order to achieve the same effect with every frame rate. Finally, we calculate the distance between the target and the black hole.

For each type of target, we need to apply the force in a slightly different way. For the player and for bullets, the force gets stronger the closer they are to the black hole:

Bullets need to be repelled; thats why we multiply their gravitational force by a negative number.

Seekers and Wanderers simply get a force applied thats always the same, regardless of their distance from the black hole.

We are now finished with the implementation of the black holes. We'll add some cool effects in the next chapters, but for now you can test it out!

Tip: Note that this is your game; feel free to modify any parameters you like! You can change the area of effect for the black hole, the speed of the enemies or the player... These things have a tremendous effect on the gameplay. Sometimes it's worth playing a bit with the values.

The Head-Up Display

There is some information that needs to be tracked and displayed to the player. Thats what the HUD (Head-Up Display) is there for. We want to track the players lives, the current score multiplier, and of course the score itself, and show all this to the player.

When the player scores 2,000 points (or 4,000, or 6,000, or ...) the player will get another life. Additionally, we want to save the score after each game and compare it to the current highscore. The multiplier increases every time the player kills an enemy and jumps back to one when the player doesn't kill anything in some time.

We'll create a new class for all that, called Hud. In Hud we have quite a few things to initialize right at the beginning:

That's quite a lot of variables, but most of them are pretty self-explanatory. We need to have a reference to the AssetManager to load text, to the guiNode to add it to the scene, and so on.

Next, there are a few variables we need to track continually, like the multiplier, its expiry time, the maximum possible multiplier, and the player's lives.

And finally we have some BitmapText objects, which store the actual text and display it on the screen. This text is set up in the method setupText(), which is called at the end of the constructor.

In order to load text, we need to load the font first. In our example we use a default font that comes with the jMonkeyEngine.

Tip: Of course, you can create your own fonts, place them somewhere in the assets directory—preferably assets/Interface—and load them. If you want know more, check out this tutorial on loading fonts in jME.

Next, we'll need a method to reset all the values so that we can start over if the player dies too many times:

Resetting the values is simple, but we also need to apply the changes of the variables to the HUD. We do that in a separate method:

During the battle, the player gains points and loses lives. We'll call those methods from MonkeyBlasterMain:

Notable concepts in those methods are:

  • Whenever we add points, we check whether we've already reached the necessary score to get an extra life.
  • Whenever we add points, we need to also increase the multiplier by calling a separate method.
  • Whenever we increase the multiplier, we need to be aware of the maximum possible multiplier and not go beyond that.
  • Whenever the player hits an enemy, we need to reset the multiplierActivationTime.
  • When the player has no lives left to be removed, we return false so that the main class can act accordingly.

There are two things left that we need to handle.

First, we need to reset the multiplier if the player doesn't kill anything for a while. We'll implement an update() method that checks whether it's time to do this:

The last thing we need to take care of is ending the game. When the player has used up all of their lives, the game is over and the final score should be displayed in the middle of the screen. We also need to check whether the current high score is lower than the player's current score and, if so, save the current score as the new high score. (Note that you need to create a file highscore.txt first, or you won't be able to load a score.)

This is how we end the game in Hud:

Finally, we need two last methods: loadHighscore() and saveHighscore():

Tip: As you may have noticed, I did not use the assetManager to load and save the text. We did use it for loading all the sounds and graphics, and the proper jME way to load and save texts actually is using the assetManager for it, but since it does not support text file loading on its own, we'd need to register a TextLoader with the assetManager. You can do that if you want, but in this tutorial I stuck to the default Java way of loading and saving text, for the sake of simplicity.

Now we have a big class that will handle all our HUD-related problems. The only thing we need to do now is add it to the game.

We need to declare the object at the start:

... initialize it in simpleInitApp():

... update the HUD in simpleUpdate(float tpf) (regardless of whether the player is alive):

... add points when the player hits enemies (in checkCollisions()):

Watch out! You need to add the points before you detach the enemies from the scene, or you'll run into problems with enemyNode.getChild(i).

... and remove lives when the player dies (in killPlayer()):

You may have noticed that we introduced a new variable as well, gameOver. We'll set it to false at the beginning:

The player should not spawn any more once the game is over, so we add this condition to simpleUpdate(float tpf)

Now you can start up the game and check whether you've missed something! And your game has got a new goal: beating the highscore. I wish you good luck!

Custom Cursor

Since we have a 2D game there is one more thing to add to perfect our HUD: a custom mouse cursor.
It's nothing special; just insert this line in simpleInitApp():


The gameplay now is completely finished. In the remaining two parts of this series, we'll add some cool graphical effects. This will actually make the game slightly harder, since the enemies may not be as easy to spot any more!