Writing a 3D space rail shooter in Three.js, Part 3

Martin Naumann

October 23rd, 2015

In the course of this three part series, you will learn how to write a simple 3D space shooter game with Three.js.

The game will introduce the basic concepts of a Three.js application, how to write modular code and the core principles of a game, such as camera, player motion and collision detection.

In Part 1 we set up our package and created the world of our game. In Part 2, we added the spaceship and the asteroids for our game. In this final Part 3 of the series, we will set the collision detection, add weapons to our craft and add a way to score and manage our game health as well.

Collisions make things go boom

Okay, now we'll need to set up collision detection and shooting. Let's start with collision detection!

We will be using a technique called hitbox, where we'll create bounding boxes for the asteroids and the spaceship and check for intersections. Luckily, Three.js has a THREE.Box3 class to help us with this.

The additions to the Player module:

``````var Player = function(parent) {
this.hitbox = newTHREE.Box3()
this.update = function() {
if(!spaceship) return
this.hitbox.setFromObject(spaceship)
}``````

This adds the hitbox and an update method that updates the hitbox by using the spaceship object to get dimensions and position for the box.

Now we'll adjust the Asteroid module to do the same:

``````var Asteroid = function(rockType) {
var mesh = newTHREE.Object3D(), self = this

// Speed of motion and rotation
mesh.velocity = Math.random() * 2 + 2
mesh.vRotation = newTHREE.Vector3(Math.random(), Math.random(), Math.random())

this.hitbox = newTHREE.Box3()``````

and tweak the update method:

``````this.update = function(z) {
mesh.position.z += mesh.velocity
mesh.rotation.x += mesh.vRotation.x * 0.02;
mesh.rotation.y += mesh.vRotation.y * 0.02;
mesh.rotation.z += mesh.vRotation.z * 0.02;

if(mesh.children.length > 0) this.hitbox.setFromObject(mesh.children[0])

if(mesh.position.z > z) {
this.reset(z)
}
}``````

You may have noticed the reset method that isn't implemented yet. It'll come in handy later - so let's make that method:

``````this.reset = function(z) {
mesh.velocity = Math.random() * 2 + 2
mesh.position.set(-50 + Math.random() * 100, -50 + Math.random() * 100, z - 1500 - Math.random() * 1500)
}``````

This method allows us to quickly push an asteroid back into action whenever we need to.

On to the render loop:

``````function render() {
cam.position.z -= 1
tunnel.update(cam.position.z)
player.update()
for(var i=0;i<NUM_ASTEROIDS;i++) {

asteroids[i].update(cam.position.z)
asteroids[i].reset(cam.position.z)
}
}
}``````

So for each asteroid that is loaded, we're checking if the hitbox of our player is intersecting (i.e. colliding) with the hitbox of the asteroid. If so, we'll reset (i.e. push into the vortex ahead of us) the asteroid, based on the camera offset.

Pew! Pew! Pew!

Now on to get us some weaponry!

Let's create a Shot module:

``````var THREE = require('three')

var shotMtl = newTHREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.5
})

var Shot = function(initialPos) {
this.mesh = newTHREE.Mesh(
newTHREE.SphereGeometry(3, 16, 16),
shotMtl
)

this.mesh.position.copy(initialPos)

this.getMesh = function() {
returnthis.mesh
}

this.update = function(z) {
this.mesh.position.z -= 5

if(Math.abs(this.mesh.position.z - z) > 1000) {
returnfalse
deletethis.mesh
}

returntrue
}

returnthis
}

module.exports = Shot``````

In this module we're creating a translucent, red sphere, spawned at the initial position given to the constructor function.

The update method is a bit different from those we've seen so far as it returns either true (still alive) or false (dead, remove now) based on the position.

Once the shot is too far from the camera, it gets cleaned up.

Now back to our main.js:

``````var shots = []

functionrender() {
cam.position.z -= 1
tunnel.update(cam.position.z)
player.update()

for(var i=0; i<shots.length; i++) {
if(!shots[i].update(cam.position.z)) {
World.getScene().remove(shots[i].getMesh())
shots.splice(i, 1)
}
}``````

This snippet is adding in a loop over all the shots and updates them, removing them if needed.

But we also have to check for collisions with the asteroids:

``````for(var i=0;i<NUM_ASTEROIDS;i++) {

asteroids[i].update(cam.position.z)
asteroids[i].reset(cam.position.z)
}

for(var j=0; j<shots.length; j++) {
if(asteroids[i].hitbox.isIntersectionBox(shots[j].hitbox)) {
asteroids[i].reset(cam.position.z)
World.getScene().remove(shots[j].getMesh())
shots.splice(j, 1)
break
}
}

}``````

Last but not least we need some code to take keyboard input to fire the shots:

``````window.addEventListener('keyup', function(e) {
switch(e.keyCode) {
case32: // Space
var shipPosition = cam.position.clone()
shipPosition.sub(newTHREE.Vector3(0, 25, 100))

var shot = newShot(shipPosition)
shots.push(shot)
break
}
})``````

This code - when the spacebar key is pressed - is adding a new shot to the array, which will then be updated in the render loop.

Move it, move it!

Cool, but while we're at the keyboard handler, let's make things moving a bit more!

``````window.addEventListener('keydown', function(e) {
if(e.keyCode == 37) {
cam.position.x -= 5
} elseif(e.keyCode == 39) {
cam.position.x += 5
}

if(e.keyCode == 38) {
cam.position.y += 5
} elseif(e.keyCode == 40) {
cam.position.y -= 5
}
})``````

This code uses the arrow keys to move the camera around.

Finishing touches

Now the last bits come into play: Score and health management as well.

``var score = 0, health = 100``

and change these values where appropriate:

``````   if(player.loaded && player.hitbox.isIntersectionBox(asteroids[i].hitbox)) {
asteroids[i].reset(cam.position.z)
health -= 20
document.getElementById('health').textContent = health
if(health < 1) {
World.pause()
alert('Game over! You scored ' + score + ' points')
}
}``````

This decreases the health by 20 points whenever the spaceship hits an asteroid and shows a "Game over" box and reloads the game afterwards.

``````   for(var j=0; j<shots.length; j++) {
if(asteroids[i].hitbox.isIntersectionBox(shots[j].hitbox)) {
score += 10
document.getElementById("score").textContent = score
asteroids[i].reset(cam.position.z)
World.getScene().remove(shots[j].getMesh())
shots.splice(j, 1)
break
}
}``````

This increases the score by 10 whenever a shot hits an asteroid.

You may have noticed the two document.getElementById calls that will not work just yet. Those are for two UI elements that we'll add to the index.html to show the player the current health and score situation:

``````<body>
<div id="bar">
Health: <span id="health">100</span>%
&nbsp;&nbsp;
Score: <span id="score">0</span>
</div>
<script src="app.js"></script>
</body>``````

And throw in some CSS, too:

``````@import url(http://fonts.googleapis.com/css?family=Orbitron);

#bar{
font-family: Orbitron, sans-serif;
position:absolute;
left:0;
right:0;
height:1.5em;
background:black;
color:white;
line-height:1.5em;
}``````

Wrap up

With all 3 Parts of this series, we now with the help of Three.js have a basic 3D game running in the browser.

There's a bunch of improvements to be made - the controls, mobile input compatibility and performance, but the basic concepts are in place.

Now have fun playing!