Tutorials‎ > ‎

Creating 3D Dodge Ball Game Using three.js

posted Oct 30, 2016, 11:44 PM by Benedictus Jason Reinhart   [ updated Dec 5, 2016, 6:55 PM by Surya Wang ]

Getting Started

Three.js is a JavaScript library used to create and display animated 3D computer graphics in a web browser. It uses WebGL as the base technology. In this tutorial we are going to create a 3D dodge ball game using the Three.js library and play it a web browser. This tutorial will not cover the basic JavaScript, WebGL, or how to create a multiplayer game.

Before reading this tutorial, make sure you have:

  • ·         Strong basic knowledge of JavaScript
  • ·         Common sense in 3D world (Vectors, Rays, etc.)
  • ·         Text editor for your JavaScript and HTML code (Sublime, Notepad++, VS Code, etc.)
  • ·         Modern browser that supports WebGL (Chrome, Firefox, etc.) to see and test your creation

If you already have those mentioned above, then you should download the Three.js library from here. Before you can use it, you need a HTML to display it, as it’s based on a canvas element.

<!DOCTYPE html> 
<html>
<head> 
    <title>Dodge Ball</title> 
    <style> 
        body { margin: 0; } 
        canvas { width: 100%; height: 100% } 
    </style> 
</head> 
<body> 
    <script src="three.js"></script> 
    <script> 
        // Our JavaScript will go here. 
    </script> 
</body> 
</html>

Creating a Scene

A scene is simply a world where objects “live”. An object such as a cube or sphere needs to be placed in a scene in order to be rendered.

"use strict";
var renderer = null;
var scene = null;
var camera = null;

var balls = [];
var player = null;
var ballsDodged = 0;

var moveLeft = false;
var moveRight = false;

var delta = 0;
var lastUpdate = Date.now();
var spawnTimer = 500; // Delay spawning of the first ball

var init = function() {
    scene = new THREE.Scene(); // Creating a scene.
    camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000); // A scene needs a camera so the player can see things in the scene.

    renderer = new THREE.WebGLRenderer(); // The WebGLRenderer of the browser
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement); // It appends a canvas element to the HTML

    // We are going to create the ground using a box. Creating an object (a mesh, actually) in Three.js needs 2 parameter, shape (we call it geometry here) and material (texture, color, etc.)
    var landGeometry = new THREE.BoxGeometry(40, 1, 70);
    var landTexture = new THREE.TextureLoader().load('textures/land.jpg');
    landTexture.wrapS = THREE.RepeatWrapping;
    landTexture.wrapT = THREE.RepeatWrapping;
    landTexture.repeat.set(2, 2);
    var landMaterial = new THREE.MeshLambertMaterial({
        color: 0xaadd00,
        map: landTexture
    });
    var land = new THREE.Mesh(landGeometry, landMaterial);
    scene.add(land); // Add the land to the scene. Without this method, your land will not be rendered.

    var directionalLight = new THREE.DirectionalLight(0xffffff, 0.7);
    scene.add(directionalLight);

    camera.position.set(-0.3, 9, 40); // x, y, z accordingly
}

var render = function() {
    requestAnimationFrame(render);
    var now = Date.now();
    delta = now - lastUpdate;
    lastUpdate = now;
}

init();
render();

In the above code, we create a world (scene), a camera in it so player can see things, a renderer for the web browser, and a directional light so the terrain won’t be dark. Notice that we load a texture (textures/land.jpg). We can apply a texture for a mesh with the TextureLoader class, so make sure you have a texture in textures/ directory.

Summary:

  • ·         Every Three.js application needs a scene as its world (THREE.Scene)
  • ·         Every scene needs a renderer and a camera to be able to see what is in the world (THREE.WebGLRenderer and THREE.PerspectiveCamera, there are other types of camera but in this tutorial we use the PerspectiveCamera)
  • ·         Every object (mesh) in the world (scene) needs a geometry (THREE.BoxGeometry, there are various types of geometry) and material (THREE.MeshLambertMaterial, there are various types of material)
  • ·         Every object instantiated needs to be added to the scene (via scene.add(object))
  • ·         A scene needs lighting so the textures will have brightness on it and shadows (we’ll get into it in later chapter)
  • ·         We need to call requestAnimationFrame(callback) function. The callback parameter is a function to animate and update things every frame
  • ·         We need to compute delta time in order to maintain a stable game as different CPU might give different delta time

Note: If you have an error XmlHttpRequest not allowed to load file, you need a web server to host it. It is not mandatory to use a texture in this tutorial, so you can just skip or comment the LoadTexture code and the map attribute on material.

Adding Player Control

For the sake of simplicity, the player will control a cube that can only strafe left or right. Previously, we have declared:

var player = null;

Before we assign a value to the player, let’s create the object first. Add the following code in the init function:

var playerGeometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);
var playerMaterial = new THREE.MeshLambertMaterial({color: 0x4444dd});
var player = new THREE.Mesh(playerGeometry, playerMaterial);
player.receiveShadow = true;
player.castShadow = true;
player.position.set(0, 1.5, 28);
player.isAlive = true;

scene.add(player);

To make things look realistic, we’ll set the player to receive and cast shadow. We also set the player position to a certain coordinate. Because I hardcoded the values, If you want to use different values for the ground and player, you’ll need to adjust things yourself. Don’t forget to add the player to the scene or the player will not be rendered. The isAlive property is a custom property we use to determine whether the player is alive or not. Later we’ll set it to false if the player collides with a ball, ending the game respectively.

For player to be able to move, we need to add keyboard listeners. Add the following code to the init function:

document.onkeydown = function(event) {
    event = event || window.event;

    switch (event.keyCode) {
    case 37:
    case 65:
        moveLeft = true;
        break;
    case 39:
    case 68:
        moveRight = true;
        break;
    }
}

document.onkeyup = function(event) {
    event = event || window.event;

    switch (event.keyCode) {
    case 37:
    case 65:
        moveLeft = false;
        break;
    case 39:
    case 68:
        moveRight = false;
        break;
    }
}

We have declared the moveLeft and moveRight variable previously, and we’ll set it to true or false according to user’s keyboard state. In this case, the keycode I used are A and left arrow to move left and D and right arrow to move right. Then, add the following code in the render() function so the movement will be checked in every frame:

if (moveLeft && player.position.x > -18) player.translateX(-0.01 * delta);
if (moveRight && player.position.x < 18) player.translateX(0.01 * delta);

The hardcoded width of the ground is 40, and the hardcoded player box width is 1, so we need to validate the box to not move beyond the ground’s width. As the x axis of the ground is 0, the ground will span from -20 to 20, so we validate that the box will not move beyond that point.

Spawning and Rolling Balls

In the previous section we have added a computation of delta time (var delta = ...) and a spawn timer. We will use both the spawn timer variable and the delta time to produce balls for every given time.

Before spawning the balls, let’s prepare a function that creates a ball every time it’s called. We will name it createBall(). Add the function outside from any scope so it can be called anywhere:

var createBall = (function() {
    var ballGeometry = new THREE.SphereGeometry(1, 128, 128);
    var ballTexture = new THREE.TextureLoader().load('textures/ball.jpg');
    ballTexture.wrapS = THREE.RepeatWrapping;
    ballTexture.wrapT = THREE.RepeatWrapping;
    var ballMaterial = new THREE.MeshLambertMaterial({map: ballTexture});

    return function() {
        var ball = new THREE.Mesh(ballGeometry, ballMaterial);
        ball.castShadow = true;
        ball.receiveShadow = true;
        ball.position.set((Math.random() - 0.5) * 38, 1.4, -35);
        return ball;
    }
})();

The main reason we create the function and the self-invoking function is because creating a new geometry, texture, and material are an expensive operation while these are actually reusable objects, so we will store those in the self-invoking function variables.

To spawn a ball for every given time, add the following code in the render() function:

spawnTimer -= delta;
if (spawnTimer <= 0) {
    var ball = createBall();
    scene.add(ball);
    balls.push(ball);
    spawnTimer = 100;
}

The balls variable is used to store the balls so we don’t have to use findObjectByName(). Now, rolling the ball and removing it after reaching certain point:

for (var ball of balls) { 
    var distance = 0.02 * delta; 
    ball.position.z += distance; 
    ball.rotation.x += distance; 

    if (ball.position.z >= 70) { 
        balls.splice(balls.indexOf(ball), 1); 
        scene.remove(ball); 
        ballsDodged++; 
    } 
}

Checking Collision

Collision is an important feature in this game, as the primary goal of the game is to avoid collision. To check whether 2 objects collide each other or not, we will use a simple collision detection using raycaster.

Raycasting can be described as the process of shooting a ray or line from a point to a certain direction. After raycasting, we detect whether the ray intersects with an object or not. In this game, we will cast a ray from player to 8 point, north, north east, east, south east, and so on. If one of these rays intersects with a ball, the player lose the game. To do so, we must check the collision of each ball in the game. Let’s modify our player code to this:

var playerGeometry = new THREE.BoxGeometry(1.5, 1.5, 1.5); 
var playerMaterial = new THREE.MeshLambertMaterial({color: 0x4444dd}); 
var directions = [
    new THREE.Vector3(0, 0, 1), 
    new THREE.Vector3(1, 0, 1), 
    new THREE.Vector3(1, 0, 0), 
    new THREE.Vector3(1, 0, -1), 
    new THREE.Vector3(0, 0, -1), 
    new THREE.Vector3(-1, 0, -1), 
    new THREE.Vector3(-1, 0, 0), 
    new THREE.Vector3(-1, 0, 1) 
]; 

var player = new THREE.Mesh(playerGeometry, playerMaterial); 
player.receiveShadow = true; 
player.castShadow = true; 
player.position.set(0, 1.5, 28); 
player.isAlive = true; 
player.checkCollision = function(ball) { 
    var playerPosition = this.position.clone(); 
    var raycaster = new THREE.Raycaster(); 
    var collisions = []; 
    for (var direction of directions) { 
        raycaster.set(playerPosition, direction); 
        var collision = raycaster.intersectObject(ball); 
        if (collision.length > 0 && collision[0].distance <= 0.5) { 
            this.isAlive = false;
            break;
        }
    }
}

So we add a directions array which contains a vector to define the 8 directions mentioned above. We also add a checkCollision() function which has a parameter of ball. Now, we will use the function inside the game loop. Modify the balls update logic:

for (var ball of balls) { 
    var distance = 0.02 * delta; 
    ball.position.z += distance; 
    ball.rotation.x += distance; 
    player.checkCollision(ball); 

    if (ball.position.z >= 70) { 
        balls.splice(balls.indexOf(ball), 1); 
        scene.remove(ball); 
        ballsDodged++;
    }
}

Now we add a validation so the game only runs when the player is alive.

if (player.isAlive) {
    requestAnimationFrame(render);
}

Lighting and Shadow

Most 3D games look realistic with the help of shadow. In Three.js, all objects don’t cast or receive shadows by default. To enable it, we can simply modify the castShadow and receiveShadow attribute to true. Let’s modify our objects now by adding the following code in the init() function:

land.receiveShadow = true;
land.castShadow = false;
player.receiveShadow = true;
player.castShadow = true;

In the createBall:

var ball = new THREE.Mesh(ballGeometry, ballMaterial); 
... 
... 
ball.castShadow = true; 
ball.receiveShadow = true;

Then we add a SpotLight as the “sun” in our scene. Add the SpotLight in init():

var light = new THREE.SpotLight(0xffffff);
light.castShadow = true;
light.angle = Math.PI / 4;
light.position.set(-10, 20, 40);
scene.add(light);

That's it, our game is complete and looks okay now. This tutorial does not cover the best practices of using three.js, so if you have questions, suggestions, and critics please leave a comment below.
ċ
dodgeball.zip
(2570k)
Benedictus Jason Reinhart,
Oct 30, 2016, 11:44 PM
Comments