7 days of WordPress themes, graphics & videos - for free!* Unlimited asset downloads! Start 7-Day Free Trial
  1. Game Development
  2. Programming

Make a Neon Vector Shooter in jMonkeyEngine: Enemies and Sounds

Scroll to top
Read Time: 14 mins
This post is part of a series called Cross-Platform Vector Shooter: jMonkeyEngine.
Make a Neon Vector Shooter in jMonkeyEngine: The Basics
Make a Neon Vector Shooter With jME: HUD and Black Holes

In the first part of this series on building a Geometry Wars-inspired game in jMonkeyEngine, we implemented the player's ship and let it move and shoot. This time, we'll add the enemies and sound effects.


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

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

We'll need some new classes in order to implement the new features:

  • SeekerControl: This is a behavior class for the seeker enemy.
  • WandererControl: This is also a behavior class, this time for the wanderer enemy.
  • Sound: We'll manage the loading and playing of sound effects and music with this.

As you might have guessed, we'll add two types of enemies. The first one is called a seeker; it will actively chase the player until it dies. The other one, the wanderer, just roams around the screen in a random pattern.

Adding Enemies

We'll spawn the enemies at random positions on the screen. In order to give the player some time to react, the enemy won't be active immediately, but rather will fade in slowly. After it has faded in completely, it will begin moving through the world. When it collides with the player, the player dies; when it collides with a bullet, it dies itself.

Spawning Enemies

First of all, we need to create some new variables in the MonkeyBlasterMain class:

We'll get to use the first two soon enough. Before that, we need to initialize the enemyNode in simpleInitApp():

Okay, now on to the real spawning code: we'll override simpleUpdate(float tpf). This method gets called by the engine over and over again, and simply keeps calling the enemy spawning function as long as the player is alive. (We already set the userdata alive to true in the last tutorial.)

And this is how we actually spawn the enemies:

Don't get confused by the enemySpawnCooldown variable. It's not there to make enemies spawn at a decent frequency—17ms would be much too short of an interval.

enemySpawnCooldown is actually there to ensure that the quantity of new enemies is the same on every machine. On faster computers, simpleUpdate(float tpf) gets called much more often than on slower ones. With this variable we check about every 17ms if we should spawn new enemies.But do we want to spawn them every 17ms? We actually want them to spawn in random intervals, so we introduce an if statement:

The smaller the value of enemySpawnChance, the more probable it is that a new enemy will spawn in this 17ms interval, and so the more enemies the player will need to deal with. That's why we subtract a little bit of enemySpawnChance every tick: it means that the game will get more difficult over time.

Creating seekers and wanderers is similar to creating any other object:

We create the spatial, we move it, we add a custom control, we set it non-active, and we attach it to our enemyNode. What? Why non-active? That's because we don't want the enemy to start chasing the player as soon as it spawns; we want to give the player some time to react.

Before we get into the controls, we need to implement the method getSpawnPosition(). The enemy should spawn randomly, but not right next to the player:

We calculate a new random position pos. If it's too close to the player, we calculate a new position, and repeat until it's a decent distance away.

Now we just need to make the enemies set themselves active and start moving. We'll do that in their controls.

Controlling Enemy Behavior

We'll deal with the SeekerControl first:

Let's focus on controlUpdate(float tpf):

First, we need to check whether the enemy is active. If it's not, we need to slowly fade it in.We then check the time that has elapsed since we spawned the enemy and, if it's long enough, we set it active.

Regardless of whether we've just set it active, we need to adjust its color. The local variable spatial contains the spatial that the control has been attached to, but you may remember that we did not attach the control to the actual picture—the picture is a child of the node we attached the control to. (If you don't know what I'm talking about, take a look at the method getSpatial(String name) we implemented last tutorial.)

So; we get the picture as a child of spatial, get its material and set its color to the appropiate value. Nothing special once you're used to the spatials, materials and nodes.

Info: You may wonder why we set the material color to white. (The RGB values are all 1 in our code). Don't we want a yellow and a red enemy?It's because the material mixes the material color with the texture colors, so if we want to display the texture of the enemy as it is, we need to mix it with white.

Now we need to take a look at what we do when the enemy is active. This control is named SeekerControl for a reason: we want enemies with this control attached to follow the player.

In order to achieve that, we calculate the direction from the seeker to the player and add this value to the velocity. After that, we decrease the velocity by 80% so that it can't grow infinitely, and move the seeker accordingly.

The rotation is nothing special: if the seeker is not standing still, we rotate it in the direction of the player. We then rotate it a little more because the seeker in Seeker.png is not pointing upwards, but to the right.

Info: The rotateUpTo(Vector3f direction) method of Spatial rotates a spatial so that its y-axis points in the given direction.

So that was the first enemy. The code of the second enemy, the wanderer, is not much different:

The easy stuff first: fading the enemy in is the same as in the seeker control. In the constructor, we choose a random direction for the wanderer, in which it will fly once activated.

Tip: If you have more than two enemies, or simply want to structure the game more cleanly, you could add a third control: EnemyControl It would handle everything that all enemies had in common: moving the enemy, fading it in, setting it active...

Now to the major differences:

When the enemy is active, we first change its direction a bit, so that the wanderer doesn't move in a straight line all the time. We do this by changing our directionAngle a bit and adding the directionVector to the velocity. We then apply the velocity just like we do in the SeekerControl.

We need to check whether the wanderer is outside of the screen borders and, if so, we change the directionAngle to a more appropiate direction so that it gets applied in the next update.

Finally, we rotate the wanderer a bit. This is just because a spinning enemy looks cooler.

Now that we've finished implementing both of the enemies, you can start the game and play a bit. It gives you a little glance at how the game will play, even though you can't kill the enemies and they can't kill you either. Let's add that next.

Collision Detection

In order to make enemies kill the player, we need to know whether they are colliding. For this, we'll add a new method, handleCollisions, called in simpleUpdate(float tpf):

And now the actual method:

We iterate through all the enemies by gettings the quantity of the children of the node and then getting each one of them. Furthermore we only need to check whether the enemy kills the player when the enemy is actually active. If it isn't, we don't need to care about it. So if he is active, we check whether the player and the enemy collide. We do that in another method, checkCollisoin(Spatial a, Spatial b):

The concept is pretty simple: first, we calculate the distance between the two spatials. Next, we need to know how close the two spatials need to be in order to be considered as having collided, so we get the radius of each spatial and add them. (We set the user data "radius" in getSpatial(String name) in the previous tutorial.) So, if the actual distance is shorter than or equal to this maximum distance, the method returns true, which means they collided.

What now? We need to kill the player. Let's create another method:

First, we detach the player from its parent node, which automatically removes it from the scene. Next, we need to reset the movement in PlayerControl—otherwise, the player might still move when it spawns again.

We then set the userdata alive to false and create a new userdata dieTime. (We'll need that to respawn the player when it's dead.)

Finally, we detach all enemies, as the player would have a hard time fighting the already existing enemies off right when it spawns.

We already mentioned respawning, so let's handle that next. We will, once again, modify the simpleUpdate(float tpf) method:

So, if the player is not alive and has been dead long enough, we set its position to the middle of the screen, add it to the scene, and finally set its userdata alive to true again!

Now may be a good time to start the game and test our new features. You'll have a hard time lasting longer than twenty seconds, though, because your gun is worthless, so let's do something about that.

In order to make bullets kill enemies, we'll add some code to the handleCollisions() method:

The procedure for killing enemies is pretty much the same as for killing the player; we iterate through all enemies and all bullets, check whether they collide and, if they do, we detach both of them.

Now run the game and see how far you get!

Info: Iterating through each enemy and comparing its position with each bullet's position is a very bad way to check for collisions. It's okay in this example for the sake of simplicity, but in a real game you'd have to implement better algorithms to do that, like quadtree collision detection. Fortunately, the jMonkeyEngine uses the Bullet physics engine, so whenever you have complicated 3D physics, you don't need to worry about this.

Now we are finished with the main gameplay. We're still going to implement black holes and display the score and lives of the player, and to make the game more fun and exciting we'll add sound effects and better graphics. The latter will be achieved through the bloom post processing filter, some particle effects and a cool background effect.

Before we consider this part of the series finished, we'll add some audio and the bloom effect.

Playing Sounds and Music

In order to some audio to our game we'll create a new class, simply called Sound:

Here, we start by setting up the necessary AudioNode variables and initialize the arrays.

Next, we load the sounds, and for each sound we do pretty much the same thing. We create a new AudioNode, with the help of the assetManager. Then, we set it not positional and disable reverb. (We don't need the sound to be positional because we don't have stereo output in our 2D game, though you could implement it if you liked.) Disabling the reverb makes the sound be played just like it is in the actual audio file; if we enabled it, we could make jME let the audio sound like we'd be in a cave or dungeon, for example. After that, we set the looping to true for the music and to false for any other sound.

Playing the sounds is pretty simple: we just call soundX.play().

Info: When you simply call play() on some sound, it just plays the sound. But sometimes we want to play the same sound twice or even more times simultaneously. That's what playInstance() is there for: it creates a new instance for every sound so that we can play the same sound multiple times at the same time.

I'll leave the rest of the work up to you: you need to call startMusic, shoot(), explosion() (for dying enemies), and spawn() at the appropriate places in our main class MonkeyBlasterMain().

When you're finished, you'll see that the game is now much more fun; those few sound effects really add to the atmosphere. But let's polish the graphics a bit as well.

Adding the Bloom Post-Processing Filter

Enabling bloom is very simple in the jMonkeyEngine, as all of the necessary code and shaders are already implemented for you. Just go ahead and paste these lines into simpleInitApp():

I've configured the BloomFilter a bit; if you want to know what all these settings are there for, you should check out the jME tutorial on bloom.


Congratulations for finishing the second part. There are three more parts to go, so don't get distracted by playing for too long! Next time, we'll add the GUI and the black holes.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.