Build a 3D platform game with three.js (pt 2)

In the second of this two-part series we bring together all the functionality from last time and start playtesting the WebGL platformer


Build a 3D platform game with threejs pt2

In last issue’s tutorial we set the scene up and did all of the preliminary work so that now we can actually put everything together into a working game.

We’re working on creating a platform game that has a simple premise: collect the key and get out before the water rises and touches the player. To force the player into avoiding the water we made the player’s character a robot, as machinery and water don’t tend to mix particularly well – ask anyone whose mobile phone has fallen into a puddle, bath or met some other watery grave!

The game is controlled using cursor keys, with the up cursor key allowing players to jump. It features two levels to demonstrate how to go about creating multi levels, which is why the code in the game is so large. There is a lot of grunt work to do in terms of removing certain scene elements from memory and replacing them and we also have to create a way for the scene to be reset if the player dies without completing the game successfully. It is possible to add even more levels to the game and we give a brief overview of how you can achieve this at the end of the tutorial. Ready? Let’s get started!


Start the project

Either open the folder that you completed last issue or copy the tutorial folder to your web server, your local server or, if you are using Brackets, to anywhere on your hard drive from the cover CD. Open the ‘index.html’ page and before the closing script tag add this code.

001    function jump(){
002        if (jumping == false && inAir == false){
003            player.position.y += 10;
004            vy = -8;
005            jumping = true;
006        }
007    }

Check for collisions

The previous code enables the player to jump if they are not already jumping or already in the air, so we increase the player’s upward velocity, enabling them to jump. Now directly below that add the following code, which will check for collisions when we call this every frame for other objects such as keys.

001    function collisionPlayer (xPos, yPos, Radius){
002        var distX = xPos - player.position.x;
003        var distY = yPos - player.position.y;
004        var distR = Radius + 25;
005        if (distX * distX + distY * distY <= distR * distR){
006            return true;    
007        }
008    }

Update the screen

The previous code works by passing in the current position of an object such as a key and drawing an invisible radius around that point. We do the same with the player and if they overlap then we know a collision has taken place. Our next code calls the renderer to update the screen; this is called every frame of the game.

001    function render() {
002        renderer.clear();
003            composer.render(0.01);
004    }

Extend JavaScript’s array

It’s actually quite difficult in JavaScript to remove an object from the middle of an array as we have to slice the array, remove the object and then stitch it back together. Here we are creating a function to do that, as this will be called when we want to remove the platforms to go from level to level.

001    Array.prototype.remove = function(from, to) {
002        var rest = this.slice((to || from) + 1 || this.length);
003        this.length = from < 0 ? this.length + from : from;
004        return this.push.apply(this, rest);
005    };

Easy insert for the array

Between levels we have to remove our collideable objects from an array. These are placed back in with the new ones so we use the following function to do so, as it makes it a little easier to call this at any other parts of the game. Save the game at this point so that you have your code up to here.

001    Array.prototype.insert = function (index, item) {
002        this.splice(index, 0, item);
003    };

Animate the game

The next function that we add animates the game and runs it at the desired frame rate. Here we call the animate function every frame so this becomes a self-repeating loop. We check that the game is in play and, if it is, we render the scene so it displays for the player. We call the update function so the game works.

001    function animate() {
002        requestAnimationFrame( animate );
003        if (inPlay){
004            render();        
005            update();
006        }
007    } 

Update the game

The main game loop is the update function and here we are about to start that. This is where all the game logic actually happens. The first thing we do here is add gravity, so we always have a downward velocity; if the player is in the air then we limit the downward velocity, or we’d fall too fast and miss the platforms!

001    function update(){
002        vy+=gravity;
003        if (inAir){    
004            if (vy>5){
005                vy=5;
006            }
007        }

Check the movement

Now we check if the player is moving left or right and if they are within the edges of our room. Our room model just happens to have its walls positioned at -42 for the left and 521 for the right-hand side. If the player is within here, then we move the player in the appropriate direction by applying the velocity on the x axis (vx).

001        if (direction == “left” && player.position.x > -42) {
002            player.position.x+=vx;
003        }
004        if (direction == “right” && player.position.x < 521) {
005            player.position.x+=vx;
006        }

Hitting platforms

In order to check if our player is hitting platforms, we fire an invisible ray downwards and upwards to see if any of the objects in the collidableMeshList are there. This is commonly known as raycasting. This will bring back a list and distance of any objects above or below.

001    var originPoint = player.position.clone();
002    ray.ray.origin.copy( originPoint );
003    rayUp.ray.origin.copy( originPoint );
004    var intersections2 = rayUp.intersectObjects
( collidableMeshList );

Check above the player

We check if there are objects above the player so that if the player hits a platform above their head, they fall back. We get the distance to objects above and then work out if the player is falling. If not, the player must be jumping and, if the distance is relatively close, then we turn off the upward velocity and allow gravity to turn off. This makes the player stop jumping and fall down.

001    if ( intersections2.length > 0 ) {
002            var distance = intersections2
[ 0 ].distance;
003            if ( falling == false && distance > 0 
&& distance <= 23 ) {
004                vy = 0;
005                falling = true;
006            }
007        }

Check below the player

We check if there is anything below the player to work out if they are on a platform or not. This works similarly to the last step except that it applies gravity if the player is greater than a small distance from the platform. We will continue the opposite of this in the next step.

001        var intersections = ray.intersectObjects
( collidableMeshList );
002        if ( intersections.length > 0 ) {
003            var distance = intersections
[ 0 ].distance;
004            if ( distance > 0 && distance > 23 ) {
005                player.position.y -= vy;
006                inAir = true;
007            }else{

A watery grave

If the object below is relatively close, we need to check exactly what that object is. If it’s the water model then we will want to end the gameplay here. We call the levelUp function but pass through ‘false’. The levelUp code then knows that this is just a level reset and not an upgrade to the next level and appropriately resets all objects in this level for the player to try again.

001    if(intersections[ 0 ] ==     “Water”){
002        levelUp(false);
003    }

Stop falling down

As we’ve established that we are now not falling onto water, then the only logical conclusion must be that this is a platform, so we turn off all of our variables that state we are in the air, jumping or falling. We also set our downward velocity to 0 so we actually stop falling.

001                falling = false;
002                inAir = false;
003                vy=0;         
004                jumping = false;
005            }
006        } 

Landing awkwardly

Sometimes we can land and end up being halfway through a platform. If that is the case then we need to use the first ‘if statement’ here in order to push the player back upwards, so they sit on top of the platform. We then check our player’s movement and if they are moving left then we rotate the player to face that direction and apply the appropriate velocity.

001        if (inAir == false && distance < 22.5){
002            player.position.y+=1;
003        }
004        if ( direction == “left” ){
005            vx = -2;
006                player.rotation.y = 3.14;
007        }

Moving right

To finish off our code we complete the right-hand movement. At this point save and test the game on a web server. The game will work and you can move left, right and also jump. The problem is, the camera doesn’t follow the player – and it’s quite hard to play off screen!

001    if ( direction == “right” ){
002            vx = 2;
003                player.rotation.y = 0;
004        }
005    }

Camera follows player

Please note that all the rest of the code for the tutorial has to go inside the last bracket of Step 15. Here we set the camera to look at the player and also to follow the player on the x and y-axis. We also push the water up every frame and rotate the key. Test again and we can see the player at all times.

001        camera.lookAt(player.position)
002        camera.position.x += ((player.position.x
- camera.position.x))* .02;
003        camera.position.y += (((player.position.
y+30 ) - camera.position.y)) * .08 
004        water.position.y += rise;
005    key.rotation.y += 0.02;

The doors of perception

At this stage the player can collide with the water but not the key, enemy or exit. Let’s fix that by colliding with the key first of all. If the player collides with the key then we make that object invisible in the scene and set our open door variable to true. Test this now and you should be able to collect the key.

001    if (collisionPlayer (key.position.x, key.
position.y, 30)){
002            key.traverse( function ( object ) 
{ object.visible = false; } );
003            openDoor = true;
004        }

Exit through the door

Now as the open door animation works we have to let the player out of the door into the next room. Here we check if the player is touching the door and whether they have the key. If so, we increase the level by 1 and the speed of water rising is increased, we then call the levelUp function so we load the new level in.

001        if (collisionPlayer (door.position.x, 
door.position.y, 20)){
002            if (openDoor){
003                level++;
004                rise += 0.1;
005                levelUp(true);
006            }
007        }

Moving the enemy

Testing the game, everything works except the enemy, so let’s start moving the enemy on the platform. We also make the enemy scale up and down so that it pulses as it moves. With the enemy being red, it should be obvious to the player that they should avoid it.

001        var time = * 0.0009;
002        enemy.scale.x = enemy.scale.y = enemy.
scale.z = (0.4 + 0.1 * Math.sin
( 7*time ) );
003        enemy.position.x += eSpeed;

Move between points

We want the enemy to move back and forth so here we check the position of the enemy and reverse the direction when it reaches those points. Save and check the game to see the enemy fully moving now. The last step for us is to make the enemy kill the player if they are touch at any point on screen.

001        if (enemy.position.x < 100 || enemy.
position.x > 400 ){
002                eSpeed=eSpeed * -1;
003            } 

Code finished

We add the collision code for the player and the enemy. All we need to do is call the levelUp function, telling it ‘false’, which makes it reset the current layer rather than the next level. We only set this to true when the player has successfully completed the level – see Step 18 for this.

001    if (collisionPlayer (enemy.position.x,     enemy.position.y, 4)){
002            levelUp(false);
003        }

Game finished

Save the game now and test it in the browser via a web server, which can be a local web host or using the in-built Node.js server with Brackets. The game should be fully working with all collisions between the player and object, the levels should load and, while there are only two levels, the infrastructure is here to add more.