News

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

In this first of a two-part series, find out how to start creating a stunning 3D platform game in WebGL using the three.js library

Build-a-3D-platform-game-with-threejs-pt2

Build a 3D platform game with threejs

Interactive experiences have taken a bit of a bashing in the last few years; sites that featured heavy content were criticised for being inaccessible and all style with no substance.

Since then, there has been a heavy swing back towards web standards and, while WebGL isn’t yet a fully fledged web standard, it has good browser support coverage, with iOS being the only major browser without support for it. We are going to use WebGL to create an incredibly fast-running game, which is easily achieved since WebGL offloads the graphics to the GPU. Now with sites like the Gravity movie site, interactive experiences are becoming more balanced, since they can make use of WebGL to create a rich interactive environment.
This game is a platform game that uses the cursor keys to move left and right, while the up key controls jumping. The purpose of the game is to get the key and get out through the door while the water rises up, creating pressure. In this part of the tutorial we will be setting the scene up and doing the preliminary work so that next issue we can put it all together into a working game. The game will feature two levels, but it can easily have many more added using the same technique.

DOWNLOAD TUTORIAL FILES

Start the project

From the resource CD, copy the tutorial folder to your web server, your local server or, if you are using Brackets, to anywhere on your hard drive. We have created some of the stylesheets and some of the JavaScript, which is extensively commented. Open ‘start.html’ and add the following just after the body tag.

001    <div id=”blank”><div id=”loading”><p>
<img src=”img/logo.png”></p>
<h3 class=”message”>Loading Please Wait
</h3></div> </div>        
002    <div id=”ThreeJS”></div> 

Set up the variables

The previous HTML sets up a loading screen that sits in place until all the models are loaded – later we’ll use jQuery to change the message to play the game. Now find the opening ‘script’ tag and add this before the ‘loadModels’ function. This sets up variables that we will use in the game.

001    var container, scene, camera
, renderer, composer;
002    var clock = new THREE.Clock();
003    var direction = “”, openDoor = false, level
= 1, inPlay=false;
004    var player, water, door, key, platforms
, room, furniture, enemy, ray, rayUp;
005    var collidableMeshList = []; 

Add more variables

Directly under the last code we add some more variables. Here we set whether the left or right key is being pressed and set the velocity of our player, along with settings for gravity, the speed that the water will rise in the game and the speed of the enemy. Other settings include the shadows and render effects.

001    var lfHeld = false, rtHeld = false;
002    var vy = 0, vx = 0, loaded = 0, gravity 
= 0.3, rise = 0.2, eSpeed =0.6;
003    var jumping = false, inAir = true, falling 
= false;
004    var SHADOW_MAP_WIDTH = 2048, SHADOW_MAP_
HEIGHT = 2048;
005    var renderPass, copyPass, effectFocus, 
composer, hblur, vblur;
006     

jQuery document ready

When all the content on the page has loaded we use a jQuery document ready function and call the ‘init’ function, which sets up the game world for us. ThreeJS uses AJAX to load the models, so we still need our own management structure to check the models have loaded.

001    $(function() {
002        init();
003    });

Set up the scene

The first stage of working in 3D is to create a new scene. Here we do so and find out the width and height of the browser window. We also set up how near and far from the camera we will render content. A camera is set up with those settings and added into our scene.

001    function init() {
002        scene = new THREE.Scene();
003        var SCREEN_WIDTH = window.innerWidth, 
SCREEN_HEIGHT = window.innerHeight;
004        var VIEW_ANGLE = 45, ASPECT = SCREEN_
WIDTH / SCREEN_HEIGHT, NEAR = 0.1, 
FAR = 20000;
005        camera = new THREE.PerspectiveCamera
( VIEW_ANGLE, ASPECT, NEAR, FAR);
006        scene.add(camera); 

Look at me!

Now we add the code that positions the camera slightly up on the y axis and a little further away from the centre of our world on our z axis. The camera is then told to look at the centre of the world. We detect if the user has WebGL available and we render using that, otherwise it’s the canvas renderer.

001    camera.position.set(0,150,400);
002        camera.lookAt(scene.position);
003        if ( Detector.webgl )
004            renderer = new THREE.WebGLRenderer
( {antialias:true} );
005        else
006            renderer = new THREE.CanvasRenderer();
007        

Set the renderer up

The renderer is now set to match the screen width and height so that it fills the browser. The auto clear is set to false because of the post-processing effects that will occur later. We also add soft shadows to the renderer so that we have some depth and shadows in the scene.

001    renderer.setSize(SCREEN_WIDTH, SCREEN_
HEIGHT);
002        renderer.autoClear = false;
003        renderer.shadowMapEnabled = true;
004        renderer.shadowMapSoft = true;
005        

Post-process the scene

ThreeJS comes with a plug-in architecture for various post-processing effects, we are going to take advantage of several of these to give the scene an interesting look. We have to set up a render pass and then we process that to add a bloom pass, which shows up highlights, a film grain pass and a vignette effect on the edges of the screen.

001    var renderModel = new THREE.RenderPass
( scene, camera );
002        var effectBloom = new THREE.BloomPass
( .8 );
003        var effectFilm = new THREE.FilmPass
( .3, .3, 1024,false );
004        var shaderVignette = THREE.
VignetteShader;
005        var effectVignette = new THREE.
ShaderPass( shaderVignette );

Vignette settings

The vignette needs some settings to work properly, so we add an offset to it and set the vignette to have dark edges on the scene. This has the effect of drawing the player’s attention to the action in the centre of the screen. The camera will follow the player so that this is where the action will always be.

001    effectVignette.uniforms[ “offset” ]
.value = 0.95;
002        effectVignette.uniforms
[ “darkness” ].value = 1.8;    
003        effectFilm.renderToScreen = true; 
004        

Add the passes

The next part of getting the post-processing effects to work is to set up a composer to render the passes. Each pass is then added to this so that the desired effect is achieved. This is a good time to save your work, so if you haven’t done already, save your progress.

001    composer = new THREE.EffectComposer
( renderer );
002        composer.addPass( renderModel );
003        composer.addPass(effectVignette);
004        composer.addPass( effectBloom );
005        composer.addPass( effectFilm ); 

Add to webpage

As the rendering is all set up we need to add this to an element in the DOM. Here we select a <div> with the ID ‘ThreeJS’ and add our renderer to this element. To see any element in a 3D scene we also need lights. We are adding a spotlight that will point towards the centre of the world from the front, right and slightly above the scene.

001    container = document.getElementById
( ‘ThreeJS’ );
002        container.appendChild( renderer.
domElement );
003        var light = new THREE.SpotLight
( 0xd6e2ff, 1, 0, Math.PI, 1 );
004        light.position.set( 600, 1000, 1000 );
005        light.target.position.set( 0, 0, 0 ); 

Set the shadow

The light is a very light blue and is set to be the light that will cast the shadow in the scene. It’s possible to have multiple lights that cast shadows, but even though there are going to be multiple lights in our scene, we will only have one that casts a shadow. It’s set up similar to the camera with a near and far field and a field of view.

001    light.castShadow = true;
002        light.shadowCameraNear = 200;
003        light.shadowCameraFar = 1800;
004        light.shadowCameraFov = 45; 
005        

A little bit shady

There are a number of settings that we need for shadows and these control the darkness of the shadows, we want them reasonably dark so that there is some good contrast in the scene, but not too dark that it ends up looking odd. The shadows are set to the height and width that we set earlier on in step 3.

001        light.shadowBias = 0.0005;
002        light.shadowDarkness = .55;
003    light.shadowMapWidth = SHADOW_MAP_WIDTH;
004        light.shadowMapHeight = SHADOW_MAP_
HEIGHT;
005        light.shadowMapSoft = true; 

Another light

We add the first light to the scene and then we add another light to the scene. This light is a slate blue in colour and it is set to a very low brightness while being made to point downwards. This is acting as a specular light to pick up highlights and is added into the scene as well.

001        scene.add( light );
002        var specLight = new THREE.PointLight
( 0x3a6f90, .1, 0, Math.PI, 1 );
003        scene.add(specLight);  
004        

Raycasting for collisions

In 3D it is possible to fire an invisible ray from any direction of an object; this ray can bring a list of all the objects in that direction and how close they are. This is commonly used to check the distance for collisions. In the following code snippet we are sending one down from the player for landing on platforms and one upwards for hitting platforms when jumping.

001        ray = new THREE.Raycaster();
002        ray.ray.direction.set( 0, -1, 0 );
003        rayUp = new THREE.Raycaster();
004        rayUp.ray.direction.set( 0, 1, 0 );

Waters rising

In the game we will have water rising in the room so that the player must constantly seek out higher ground. Here we create a plane that will look like water; we make this an ‘additive’ blue colour (which makes everything appear a light blue below it) and adjust the transparency to create the perfect water effect.

001        var waterGeometry = new THREE.
PlaneGeometry( 1000, 800, 1, 1, 1 );
002        var waterMaterial = new THREE.
MeshBasicMaterial( {color: 0x0042ff, 
transparent: true, opacity: 0.95, 
blending: THREE.AdditiveBlending} ); 

Delayed animation

The water model is made out of the geometry and material. This is rotated 90 degrees on the x axis so that it is flat in the scene. We also name this water and when we do collision detection in the game, we can find out if the model that the player lands on is ‘water’ and therefore end the play. Because the player will be colliding with the water, it gets added to an array.

001        water = new THREE.Mesh(waterGeometry, 
waterMaterial);
002        water.rotation.x = -Math.PI /2;
003        water.name=”Water”;
004    scene.add(water);
005        collidableMeshList.push(water);
006        

The doors of perception

The door in every scene will be opened, so we need a separate model for this – a cube works well. Here we create the cube geometry and use the same material as the water. The cube is then positioned and added to the scene in the right place, ready to be opened when the player has collected the key.

001    var cubeGeometry = new THREE.
CubeGeometry(60,60,10,1,1,1);
002        door = new THREE.Mesh(cubeGeometry, 
waterMaterial);
003        door.position.set(0, 485, -60);
004        door.geometry
005        scene.add( door ); 

User input

The next section of code checks for user input and whether the key is down or not. Using a ‘switch’ statement that is very similar to an ‘if’ statement, we check if the key is number 37, which is the ‘left’ key. If it is, the switch is broken and the code stops running in the function.

001    document.addEventListener(‘keydown’, 
function(e){
002            switch(e.keyCode){
003                case 37: //left
004                    direction = “left”;
005                    break; 

Up and right

The remaining section of the switch statement checks for the ‘up’ key being pressed and if it is, then the jump function is called, moving the player upwards. If the final key is pressed, which is the ‘right’ key, the direction is set to ‘right’. In the game the direction will control the movement on each frame.

001    case 38: //up
002                    jump();
003                    break;
004    case 39: //right
005                    direction = “right”;
006                    break;
007            };
008        }, false); 

Release the keyboard

It is important to find out if the player has released a key as this will determine the responsiveness of the movement. Here we check if the left key is released and set the direction to equal nothing, so movement is ceased.

001    document.addEventListener(‘keyup’, 
function(e){
002            switch(e.keyCode){
003                case 37: //left
004                    direction = “”;
005                    break;

And finally…

We check that the right key is released and likewise set the direction to equal nothing. Save this now and test in the browser. Because the render is not continuously being called the screen doesn’t look right yet, but you can get a sense of the visual style of the game at this early stage.

001    case 39: //right
002                    direction = “”;
003                    break;
004            };
005        }, false);    
006        loadModels();

Improve your web design skills with Web Designer. Download issues direct from GreatDigitalMags.com or buy print issues from ImagineShop

×