WebGL - SPLessons

WebGL Creating Three.js Frogger

Home > Lesson > Chapter 4
SPLessons 5 Steps, 3 Clicks
5 Steps - 3 Clicks

WebGL Creating Three.js Frogger

WebGL Creating Three.js Frogger

shape Introduction

This chapter demonstrates about the WebGL Creating Three.js Frogger which is the creating most complex example i.e classic game Frogger by using the WebGL, following are the concepts are covered in this chapter.
  • Three.js Frogger

Three.js Frogger

shape Description

Actually Frogger game which consist one Frog and player have to get across from one side to another and hopefully user can get across the lily pad on the other side. In order to create the game properly user need to have the 3D Models for the various elements in the scenes. In creating WebGL Frogger Game player can control the characters using the arrow keys so users can them left and right and forwards and backwards. Here some pointer lock implmented which allows to rotate the camera around the scene to obtain the differnt view points Components
  • User need to create the scene objects from Three.js primitives. In the game trucks are made with the cube and cylinder geometries.
  • Physijs physics engine used for some fun collisions.
  • HTML and CSS used for the overlaying the rendering area.
  • Pointer Lock is used for the camera placement.
Vehicle Movement
  • The scene is made up of a couple of traffic lenses with vehicles moving with different speeds and rather then create lots of vehicles  just reset a vehicle position once it move outside the players point of view.
Three.js Clock
  • Need to take the frame rate into an account.
  • clock.getDelta(); which defines the time between the each frame.
  • clock.getElapsedTime(); Which defines the total Elapsed time.

shape Examples

The code below demonstrates the designing the Frogger.js by using the WebGL.
The code below demonstrates the Index.html which is the simple layout of the program which import the various JavaScript files and utilized in the applications and here two libraries are using those are the Pointer Lock Controls from Three.js examples and physi.js the code as shown below. [c] <!DOCTYPE html> <html> <head> <title>ThreeJS Frogger</title> <style> body { padding: 0; margin: 0; } #control-panel { position: absolute; right: 0; bottom: 0; background: #ccc; opacity: 0.5; width: 150px; height: 50px; } #control-panel { margin-top: 10px; font-family : arial; font-size: 36px; } </style> </head> <body> <div id="control-panel"> <label>Lives:</label> <span id="numberOfLives">3</span> </div> <div id="webgl-container"> </div> <script src="scripts/three.js"></script> <script src="fonts/helvetiker_regular.typeface.js"></script> <script src="scripts/support.js"></script> <script src="scripts/PointerLockControls.js"></script> <script src="scripts/physi.js"></script> <script src="scripts/sceneSetup.js"></script> <script src="scripts/gameControls.js"></script> <script src="scripts/enemy.js"></script> <script src="scripts/text.js"></script> <script src="scripts/player.js"></script> <script src="scripts/pointerLockSetup.js"></script> <script src="scripts/_game.js"></script> </body> </html> [/c]
game.js is an essential script in our game. The game begins the window .onload event occurs and the game .init method is called. in game.js there are some typical scenes and initialization logics which also have some specific functionality such as maintaining the number of lives the player has. In the init method setup the player and enemies the code as shown below. [c] var game = (function () { "use strict"; Physijs.scripts.worker = 'scripts/physijs_worker.js'; Physijs.scripts.ammo = 'ammo.js'; var scene = new Physijs.Scene(), camera, clock = new THREE.Clock(), width = window.innerWidth, height = window.innerHeight - 10, playerBox, renderer = new THREE.WebGLRenderer(), playerActive = true, lives = 3, controls; renderer.setSize(width, height); renderer.setClearColor(0xE0EEEE); document.getElementById("webgl-container").appendChild(renderer.domElement); camera = new THREE.PerspectiveCamera( 35, width / height, 1, 1000 ); scene.add(camera); scene.fog = new THREE.Fog(0xE0EEEE, 250, 600); scene.setGravity(new THREE.Vector3(0, -100, 0)); function init() { resetScene(); pointerLock.init(camera, scene); sceneSetup.addSceneObjects(); enemy.init(); player.createPlayer(); gameControls.init(); render(); } function resetScene() { camera.position.set(0, 20, 200); camera.rotation.set(0, 0, 0); } function removeLife() { lives -= 1; document.getElementById("numberOfLives").innerHTML = lives; if (lives == 0) { alert('game over'); } } function render() { scene.simulate(); pointerLock.controls.update(); var delta = clock.getDelta(); enemy.update(delta); if (game.wintext) { game.wintext.rotation.y += 0.01; } renderer.render(scene, camera); requestAnimationFrame(render); } return { scene: scene, camera: camera, playerBox: playerBox, init: init, controls: controls, playerActive: playerActive, resetScene: resetScene, lives: lives, removeLife: removeLife } })(); window.onload = game.init(); [/c]
Which is responsible for setting up the static in the scene and is responsible for creating a road, lake trees, and ground the code as shown below. [c] var sceneSetup = (function () { "use strict"; var treeTexture= THREE.ImageUtils.loadTexture('content/tree.jpg'); //http://opengameart.org/node/8149 function createRoad(zPos) { var road = new THREE.Mesh( new THREE.BoxGeometry(2000, 1, 250), new THREE.MeshLambertMaterial({ map: THREE.ImageUtils.loadTexture('content/road.jpg') }), //http://opengameart.org/sites/default/files/oga-textures/tunnel_road.jpg 0 ); road.name = "road"; road.position.y = 1; road.position.z = zPos; game.scene.add(road); } function createLake(zPos) { var lake = new THREE.Mesh( new THREE.BoxGeometry(2000, 1, 250), new THREE.MeshLambertMaterial({ map: THREE.ImageUtils.loadTexture('content/water.jpg') }), //http://opengameart.org/node/10510 0 ); lake.name = "lake"; lake.position.y = 1; lake.position.z = zPos; game.scene.add(lake); } function createTree(x, z) { //lets have some variety with our trees var treeBaseWidth = support.getRand(15, 22); var tree = new THREE.Mesh( new THREE.CylinderGeometry(1, treeBaseWidth, 60, 9, 9, false), new THREE.MeshLambertMaterial({ ambient: 0x003311 * support.getRand(0, 5), map: treeTexture }), 0 ); var stump = new THREE.Mesh( new THREE.CylinderGeometry(5, 5, 20, 9, 9, false), new THREE.MeshLambertMaterial({ ambient: 0x552211 }), 0 ); tree.add(stump); stump.position.y = -40; tree.name = "tree"; tree.position.set(x, 40, z); game.scene.add(tree); } function addSceneObjects() { //ground var grassTexture = THREE.ImageUtils.loadTexture('content/grass.png'); //http://opengameart.org/sites/default/files/grass_0_0.png grassTexture.wrapS = grassTexture.wrapT = THREE.RepeatWrapping; grassTexture.repeat.set(25, 25); var material = Physijs.createMaterial( new THREE.MeshLambertMaterial({ map: grassTexture }), 0.9, 0.1 ); var ground = new Physijs.BoxMesh( new THREE.BoxGeometry(2000, 1, 2000), material, 0 ); ground.name = "ground"; ground.position.y = 0; game.scene.add(ground); //first road createRoad(-100); //trees for (var i = 0; i < 20; i++) { createTree(support.getRand(-500, 500), support.getRand(-250, -320)); } //second road createRoad(-500); createLake(-900); setupSceneLighting(); } function setupSceneLighting(){ var ambientLight = new THREE.AmbientLight(0xcccccc); game.scene.add(ambientLight); var spotLight = new THREE.SpotLight(0xffffff); spotLight.position.set(0, 200, -50); game.scene.add(spotLight); } return { addSceneObjects: addSceneObjects } })(); [/c]
Game controls is the responsible for moving the player in response to keyboard events. Here make the use of the window.onkeydown event and then use various methods on the player object the code as shown below. [c] var gameControls=(function(){ function checkKey(e) { if(!game.playerActive){ return; } var left = 37, up = 38, right = 39, down = 40, increment = 2; e = e || window.event; if (e.keyCode == up) { player.moveZ(-increment); } else if (e.keyCode == down) { player.moveZ(increment); } else if (e.keyCode == left) { player.moveX(-increment); } else if (e.keyCode == right) { player.moveX(increment); } } function init(){ window.onkeydown = checkKey; } return{ init: init } })(); [/c]
Which is the responsible for creating the player out of Three.js primitives. Additionally which contains a number of methods for moving the player around the scene. If the player reached the end, in order to know whether they have won the game the code as shown below. [c] var player=(function(){ "use strict"; var movementRate = 2, playerBox, playerBoxMaterial, finishLineZPos=-920; function init(){ } function moveX(movement) { game.camera.position.x += movementRate * movement; game.camera.__dirtyPosition = true; playerBox.position.x += movementRate * movement; playerBox.__dirtyPosition = true; } function moveZ(movement) { game.camera.position.z += movementRate * movement; game.camera.__dirtyPosition = true; playerBox.position.z += movementRate * movement; playerBox.__dirtyPosition = true; checkIfPlayerAtFinish(); } function checkIfPlayerAtFinish(){ if(playerBox.position.z<=finishLineZPos){ if(game.wintext){ //dont display win text twice! return; } text.createText('You win!', 'winText'); } } function createPlayer(){ playerBoxMaterial = Physijs.createMaterial( new THREE.MeshBasicMaterial({ visible: false }), 0.1, //friction 0.5 //restitution/bounciness ); var personMaterial = new THREE.MeshPhongMaterial({ ambient: 0x0dd00, transparent: true, }); var playerBody = new Physijs.BoxMesh( new THREE.BoxGeometry(8, 5, 5), personMaterial, 0.1 ); playerBody.position.y = -8; var playerLLeg = new Physijs.BoxMesh( new THREE.BoxGeometry(2, 5, 5), personMaterial, 0.1 ); playerBody.add(playerLLeg); playerLLeg.position.y = -5; playerLLeg.position.x = -2; var playerRLeg = new Physijs.BoxMesh( new THREE.BoxGeometry(2, 5, 5), personMaterial, 0.1 ); playerBody.add(playerRLeg); playerRLeg.position.y = -5; playerRLeg.position.x = 2; playerBox = new Physijs.BoxMesh( new THREE.CubeGeometry(10, 10, 10), playerBoxMaterial, 0.1 ); playerBox.position.set(0, 15, 50); playerBox.name = "playerBox"; playerBox.add(playerBody); playerBody.position.y = 2; game.scene.add(playerBox); } return{ init: init, createPlayer: createPlayer, moveX: moveX, moveZ: moveZ } })(); [/c]
Which contains a method to get a random number between an upper and lower limit which is used to generate the Tree line the code as shown below. [c] var support = (function () { "use strict"; function getRand(min, max) { return Math.random() * (max - min) + min; } return { getRand: getRand } })(); [/c]
Which allows to display the text on the screen. Remember to import the font geometry, otherwise which is wont see too much on the screen the code as shown below. [c] var text = (function () { "use strict"; function createText(textContent, name) { var text3d = new THREE.TextGeometry(textContent, { size: 50, height: 20, curveSegments: 2, font: "helvetiker" }); var textMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000}); var textMesh = new THREE.Mesh(text3d, textMaterial); textMesh.position.set(-120, 60, -1200); textMesh.name = textContent; game.scene.add(textMesh); game.wintext = textMesh; } return{ createText: createText } })(); [/c]
PointerLock Controls are generated from the PointerLock in the scene. In order to intialize the PointerLock browser needs to click on event occur then user will get pointer lock controls from the Three.js examples the code as shown below. [c] THREE.PointerLockControls = function ( camera ) { var scope = this; camera.rotation.set( 0, 0, 0 ); var pitchObject = new THREE.Object3D(); pitchObject.add( camera ); var yawObject = new THREE.Object3D(); yawObject.position.y = 10; yawObject.add( pitchObject ); var moveForward = false; var moveBackward = false; var moveLeft = false; var moveRight = false; var isOnObject = false; var canJump = false; var prevTime = performance.now(); var velocity = new THREE.Vector3(); var PI_2 = Math.PI / 2; var onMouseMove = function ( event ) { if ( scope.enabled === false ) return; var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; //stops player rotating under the ground if(pitchObject.rotation.x > 0.13){ pitchObject.rotation.x = 0.13; } yawObject.rotation.y -= movementX * 0.002; pitchObject.rotation.x -= movementY * 0.002; pitchObject.rotation.x = Math.max( - PI_2, Math.min( PI_2, pitchObject.rotation.x ) ); console.log(pitchObject.rotation.x) }; var onKeyDown = function ( event ) { switch ( event.keyCode ) { //case 38: // up case 87: // w moveForward = true; break; //case 37: // left case 65: // a moveLeft = true; break; //case 40: // down case 83: // s moveBackward = true; break; //case 39: // right case 68: // d moveRight = true; break; case 32: // space if ( canJump === true ) velocity.y += 350; canJump = false; break; } }; var onKeyUp = function ( event ) { switch( event.keyCode ) { case 38: // up case 87: // w moveForward = false; break; case 37: // left case 65: // a moveLeft = false; break; case 40: // down case 83: // s moveBackward = false; break; case 39: // right case 68: // d moveRight = false; break; } }; document.addEventListener( 'mousemove', onMouseMove, false ); document.addEventListener( 'keydown', onKeyDown, false ); document.addEventListener( 'keyup', onKeyUp, false ); this.enabled = false; this.getObject = function () { return yawObject; }; this.isOnObject = function ( boolean ) { isOnObject = boolean; canJump = boolean; }; this.getDirection = function() { // assumes the camera itself is not rotated var direction = new THREE.Vector3( 0, 0, -1 ); var rotation = new THREE.Euler( 0, 0, 0, "YXZ" ); return function( v ) { rotation.set( pitchObject.rotation.x, yawObject.rotation.y, 0 ); v.copy( direction ).applyEuler( rotation ); return v; } }(); this.update = function () { if ( scope.enabled === false ) return; var time = performance.now(); var delta = ( time - prevTime ) / 1000; velocity.x -= velocity.x * 10.0 * delta; velocity.z -= velocity.z * 10.0 * delta; velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass if ( moveForward ) velocity.z -= 400.0 * delta; if ( moveBackward ) velocity.z += 400.0 * delta; if ( moveLeft ) velocity.x -= 400.0 * delta; if ( moveRight ) velocity.x += 400.0 * delta; if ( isOnObject === true ) { velocity.y = Math.max( 0, velocity.y ); } yawObject.translateX( velocity.x * delta ); yawObject.translateY( velocity.y * delta ); yawObject.translateZ( velocity.z * delta ); if ( yawObject.position.y < 10 ) { velocity.y = 0; yawObject.position.y = 10; canJump = true; } prevTime = time; }; }; [/c]
PointerLockSetup which is the setup of the PointerLock as shown below. [c] var pointerLock = (function () { var controls; function init(camera, scene){ this.controls = new THREE.PointerLockControls(camera); scene.add(this.controls.getObject()); //pointerlock needs click to activate document.addEventListener('click', function (event) { var element = document.getElementsByTagName("canvas")[0]; function pointerlockchange(event) { if (document.pointerLockElement === element || document.mozPointerLockElement === element || document.webkitPointerLockElement === element) { pointerLock.controls.enabled = true; } else { pointerLock.controls.enabled = false; } } document.addEventListener('pointerlockchange', pointerlockchange, false); document.addEventListener('mozpointerlockchange', pointerlockchange, false); document.addEventListener('webkitpointerlockchange', pointerlockchange, false); element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock; element.requestPointerLock(); }, false); } return{ init: init, controls: controls } })(); [/c]
enemy.js is one of the more complicated script in the game which starts when init function is called then which calls createEnemies method. In createEnemies method intilaize the trucks in the various lanes of the road, passing in a number of parameters. The create enemies function accepts the number of different parameters such as which side a truck originated from its speed, its startPos, and also its zPos. [c] var enemy = (function () { "use strict"; var enemies = []; function createEnemy(origin, speed, startPos, zPos) { var enemy = new Physijs.BoxMesh( new THREE.CubeGeometry(50, 30, 25), Physijs.createMaterial( new THREE.MeshPhongMaterial({ ambient: Math.random() * 0xffffff }), 0, //friction 0 //restitution/bounciness ), 100 ); var tyre1 = new Physijs.BoxMesh(new THREE.CylinderGeometry(8, 8, 26, 12, 12, false), new THREE.MeshBasicMaterial({color: 0x000000 }), 0); tyre1.position.x = -17; tyre1.position.y = -10; tyre1.rotation.x = 90 * (Math.PI / 180); var tyre2 = new Physijs.BoxMesh(new THREE.CylinderGeometry(8, 8, 26, 12, 12, false), new THREE.MeshBasicMaterial({color: 0x000000 }), 0); tyre2.position.x = 17; tyre2.position.y = -10; tyre2.rotation.x = 90 * (Math.PI / 180); enemy.add(tyre1); enemy.add(tyre2); startPos = origin == 'right' ? startPos : -startPos; enemy.position.set(startPos, 18, zPos); enemy.userData = { origin: origin, speed: speed, startPos: startPos, zPos: zPos } enemy.name = 'enemy'; enemy.addEventListener('collision', handleCollision); enemies.push(enemy); game.scene.add(enemy); } function handleCollision(objectCollidedWith) { if (objectCollidedWith.name == "playerBox") { pointerLock.controls.enabled = false; game.playerActive = false; game.removeLife(); setTimeout(handlePlayerKilled, 2000); } } function createEnemies() { //road 1 createEnemy('right', 0.5, -200, -5); createEnemy('right', 0.5, 200, -5); //road 2 createEnemy('left', 0.2, -350, -60); createEnemy('left', 0.2, 0, -60); //road 3 createEnemy('right', 1, -200, -130); createEnemy('left', 1, -300, -190); //road 4 createEnemy('right', 1, -200, -520); } function handlePlayerKilled() { var playerBox = game.scene.getObjectByName('playerBox'); game.scene.remove(playerBox); game.resetScene(); player.createPlayer(); game.playerActive = true; } function update(delta) { if (enemies.length == 0) return; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var movement = enemy.userData.origin == 'right' ? 200 : -200; enemy.position.x -= movement * (delta * enemy.userData.speed); if ((enemy.userData.origin == 'right' && enemy.position.x < -400) || (enemy.userData.origin == 'left' && enemy.position.x > 400)) { //restart enemy over other side enemy.position.x = enemy.userData.origin == 'right' ? 400 : -400; } else { //rotate tyres enemy.children[0].rotation.y += 1; enemy.children[1].rotation.y += 1; enemy.__dirtyPosition = true; } } } function init() { createEnemies(); } return { init: init, update: update } })(); [/c]

Summary

shape Key Points

  • Physi.js is used for the Collisions.
  • PointerLock used for the camera poisition.
  • User need to use Three.js libraries.