EREVALD KULLOLLI

Making of Space Invaders with Three.js

I’m a huge fan of retro games mostly for their combination of artistry and simplicity.
Coming across this Space Invaders shot made me want to re-create a playable version using Three.js and Cannon.js.

In this brief tutorial I will only cover the main effects of this shot, let’s get started!

Moving ground

The ground is a large plane with a tileable texture material applied to it which I’m animating to make it appear as if it’s moving.

To start with, here is the tileable texture used for the ground.

I’m repeating the texture as needed to match the original image.

texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(45, 45);

And finally moving it along the y axis.

texture.offset.y += .02;

To make things bounce on the ground I’ve added a physics body with zero mass.

plane = new CANNON.Plane();
groundBody = new CANNON.Body({ mass: 0 });
View Fiddle

Spaceship model

The spaceship is a low poly model and is used for both the player and the enemy ships.
To create the model I have used Clara.io , it’s easy to use and let’s you export as JSON.

Controlling it

All movement transitions are done using the TweenMax animation library.
Mapping the keys I need to control the ship:

controlKeys: {
    87: "forward",
    83: "backward",
    65: "left",
    68: "right"
}

I’m managing all my keys on a keydown event listener.

document.addEventListener("keydown", onKeyDown, false);

function onKeyDown(event) {

    // Move left
    if (player.controlKeys[event.keyCode] == 'left') {
        // Slight tilt
        TweenMax.to(player.spaceshipMesh.rotation, 0.3, {z: -.4});

        TweenMax.to(player.spaceship.position, 2, { 
            x: player.spaceship.position.x - moveDistance, ease: Power2.easeOut,
            onComplete: function() {
                // reset rotation
                TweenMax.to(player.spaceshipMesh.rotation, 0.3, { z: 0 });
            }
        });
    }

    // Move right
    if (player.controlKeys[event.keyCode] == 'right') {
        // Slight tilt
        TweenMax.to(player.spaceshipMesh.rotation, 0.3, {z: +.4});
        // Move on x axis
        TweenMax.to(player.spaceship.position, 2, { 
            x: player.spaceship.position.x + moveDistance, ease: Power2.easeOut,
            onComplete: function() {
                // reset rotation
                TweenMax.to(player.spaceshipMesh.rotation, 0.3, {z: 0});
            }
        });
    }

    // Move forward
    if (player.controlKeys[event.keyCode] == 'forward')
        TweenMax.to(player.spaceship.position, 2, {
            z: player.spaceship.position.z - moveDistance,ease: Power2.easeOut
        });

    // Move backward
    if (player.controlKeys[event.keyCode] == 'backward')
        TweenMax.to(player.spaceship.position, 2, {
            z: player.spaceship.position.z + moveDistance,ease: Power2.easeOut
        });

}

Orientation

I need the player spaceship to always look at where the mouse position is on the ground.
For this I’m raycasting to find the position where the mouse is intersecting with the ground.

raycaster.setFromCamera(mouse, camera);

var intersects = raycaster.intersectObject(ground, true);

// Change spaceship orientation to look at the fire target
if( intersects.length > 0 )
    player.spaceship.lookAt(intersects[0].point);

Firing projectiles

The bullet projectile is a box mesh with a physics body, it is created from a mousedown event.
In the event I’m setting the velocity for the body to make it shoot out at the direction of the player:

body.velocity.set(dir.x * velocity, dir.y * velocity, dir.z * velocity);

To destroy the bullet I use the sleep event that comes with the physics body, this will trigger when the body is inactive.

var _sleepEvent = function(e) {
    // destroy physics body and visual mesh
    body.removeEventListener("sleep", _sleepEvent);
}

body.addEventListener("sleep", _sleepEvent);

Enemy spaceships and formations

In this demo the enemies can be shot down but they won’t collide with the player or fire back.
I have created a formation system where I can control the number and the formation shape for the enemy spaceships.

An example of 2 different formations:

formation: "triangle", // Default formation
formations: {
    triangle: [ [0, 1, 2, 3], [0.5, 1.5, 2.5], [1.5] ],
    square: [ [0, 1, 2], [0, 1, 2], [0, 1, 2] ]
}

Within a formation there’s a list of arrays, each array is a row of ships and each value controls the x offset position in the row.

var
zPos = 0,
xPos = 0,
distance = 160,
offset = (formations[formation][0].length * distance) / 2;

for (var col in formations[formation]) {
    
    // Calculate Z position
    zPos = col * distance;
    
    for (var row in formations[formation][col]) {
        
        // Calculate x position
        xPos = formations[formation][col][row] * distance;
        xPos -= offset - (150 / 2);
        
        // Create mesh
        mesh = new THREE.Mesh(player.mesh, material);
        
        // Set mesh position
        mesh.position.set(xPos - 100, -20, zPos - 400);

        // Create physics body for the bullet collision
        body = new CANNON.Body({ mass: 0 });
        body.addShape(shape);
        world.add(body);
        
    }
}

Explosions

Each enemy spaceship body has a collide event that will trigger when a bullet intersects with it. The explosion itself is made of 4 physics boxes which are only created when we have a collision.

body.addEventListener("collide", _collideEvent);

_collideEvent = function(e) {
    e.target.removeEventListener("collide", _collideEvent);
    
    // Trigger the explosion at the current body
    explosion.trigger(e.target.position);
    
}

trigger: function(position) {

    var body, mesh;
    
    // Create 4 physics boxes
    for (var i = 0; i < 4; i++) {

        // Physics body
        body = new CANNON.Body({ mass: 1 });
        body.addShape(explosion.shape);
        body.position.copy(position);
        body.position.x -= 12 * i;
        body.position.y -= 12 * i;
        body.position.z -= 12 * i;
        
        // Set velocity with force direction
        body.velocity.set(
            direction.x * (Math.random() * 350),
            Math.random() * 400,
            direction.z * (Math.random() * 350)
        );
        world.add(body);
        
        // Visual mesh
        mesh = new THREE.Mesh(geo, material);
        mesh.position.copy(body.position);
        
        // Animate scale from 1 to 0.1 in 3 second duration
        TweenMax.to(mesh.scale, 3, { x: 0.1, y: 0.1, z: 0.1 });
        
    }
}

Asteroids

For the asteroids I’ve created an emitter that generates meshes at random positions within it’s volume like illustrated below.

Asteroid emitter

It comes with a set of parameteres for volume size, position in the scene, asteroid count and the obj used for the mesh.

createEmitter: function(opts) {
    var parent = new THREE.Object3D(), mesh;

    for (var i = 0; i < opts.count; i++) {
        mesh = opts.obj.mesh.clone();
        // Initial positions
        mesh.position.x = randomRange(0, opts.size.x);
        mesh.position.y = randomRange(0, opts.size.y);
        mesh.position.z = randomRange(0, opts.size.z);
        // Sizes
        var meshSize = randomRange(opts.obj.size[0], opts.obj.size[1]);
        mesh.scale.set(meshSize, meshSize, meshSize);
        parent.add(mesh);
    }

    parent.position.set(opts.pos.x, opts.pos.y, opts.pos.z);
    parent.boxLength = opts.size.z;
    parent.boxheight = opts.size.y;

    scene.add(parent);
    return parent;
}

Then on every frame, I change the position of each mesh to move forward. To get a continuous flow, meshes are moved back to the beginning of the volume when they reach the distance of size.z.

timer += 0.03;
asteroids.emitters.forEach(function(emitter) {
    for (var i = 0; i < emitter.children.length; i++) {
        // Moving loop
        if (emitter.children[i].position.z < emitter.boxLength) {
            emitter.children[i].position.z += 5;
        } else {
            // Move mesh back to the beginning of the volume
            emitter.children[i].position.z -= emitter.boxLength;
        }
        // Wobble
        emitter.children[i].position.y += Math.cos(timer + i);
        // Rotate
        emitter.children[i].rotation.x -= 0.02;
        emitter.children[i].rotation.z -= 0.02;
    }
});
View Fiddle
Topic
Threejs Gaming Webgl