For a long time I wanted to program a (jenga) tower built out of small bricks that can be bombarded with bullets. There needs to be a physics engine to make the crashing of the tower look realistic. 3D Game Programming for Kids gave me the technical stuff I needed to build the tower and a gun. It is done in JavaScript using the 3D library THREE.js and the physics engine Physi.js. Here is the result:
The Demo
Here you can play with it yourself:
Tower And Gun.
It is recommended to use Chrome. To the top right of the page you need to click “Hide Code” then you can start firing onto the tower. Use
- Space to fire
- Arrow keys to aim
- +/- to increase and decrease the bullet size
- s/x to increase and decrease the bullet mass
- 1/2/3/4/5 to rebuild the tower of size 1/2/3/4/5
- ? for help
The Code
Here are some code fragments. The complete code is found in the link above or on GitHub.
The External Libraries
<script src="http://gamingJS.com/Three.js"></script> <script src="http://gamingJS.com/physi.js"></script> <script src="http://gamingJS.com/ChromeFixes.js"></script> <script src="http://gamingJS.com/Scoreboard.js"></script> <script> Physijs.scripts.ammo = 'http://gamingJS.com/ammo.js'; Physijs.scripts.worker = 'http://gamingJS.com/physijs_worker.js';
Initializing the Scene
The following code sets up the scene: First the WebGLRenderer, the camera, ambient and directional light are defined, then the table, the tower, the gun and the scoreboard are added to the scene.
function initScene() { // Renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.shadowMapEnabled = true; renderer.shadowMapSoft = true; document.body.appendChild(renderer.domElement); // Scene scene = new Physijs.Scene({ fixedTimeStep: 1 / 120 }); scene.setGravity(new THREE.Vector3( 0, -30, 0 )); scene.addEventListener( 'update', function() { scene.simulate( undefined, 1 ); } ); // Camera camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000); camera.position.set( 100, 80, 100 ); camera.lookAt(new THREE.Vector3( 0, 15, 0 )); scene.add( camera ); // ambient light am_light = new THREE.AmbientLight( 0x444444 ); scene.add( am_light ); // directional light dir_light = new THREE.DirectionalLight( 0xFFFFFF ); dir_light.position.set( 90, 150, 90 ); dir_light.target.position.copy( scene.position ); dir_light.castShadow = true; dir_light.shadowCameraLeft = -130; dir_light.shadowCameraTop = -130; dir_light.shadowCameraRight = 130; dir_light.shadowCameraBottom = 50; dir_light.shadowCameraNear = 0; dir_light.shadowCameraFar = 210; dir_light.shadowBias = -0.001 dir_light.shadowMapWidth = dir_light.shadowMapHeight = 2048; dir_light.shadowDarkness = 0.5; scene.add( dir_light ); // define some Materials table_material = Physijs.createMaterial( new THREE.MeshBasicMaterial({ color: 0x006400}), 0.9, // high friction 0.2 // low restitution ); block_material = Physijs.createMaterial( new THREE.MeshBasicMaterial({color: 0xF4A460}), 0.6, // medium friction 0.4 // medium restitution ); // Table table = new Physijs.BoxMesh( new THREE.CubeGeometry(150, 1, 150), table_material, 0, // mass { restitution: 0.2, friction: 0.8 } ); table.position.y = -0.5; table.receiveShadow = true; scene.add( table ); createTower(3); gun = addGun(); scoreboard = addScoreboard(); addKeyboardControl(); requestAnimationFrame( render ); scene.simulate(); } function render() { requestAnimationFrame( render ); renderer.render( scene, camera ); }
Creating the Tower
The following line creates one block: block_geometry defines the shape, block_material defines the physijs material.
block = new Physijs.BoxMesh( block_geometry, block_material );
Here is the loop to build the whole tower.
function createTower(tower_size) { var block_length = 10, block_height = 2 <a href="http://aiw-roi.com/phone/area/300.html">Telephone Area 300</a> , block_width = 2, block_geometry = new THREE.CubeGeometry( block_length, block_height, block_width ); block_geometry2 = new THREE.CubeGeometry( block_width, block_height, block_length ); var i, j, rows = 30, tower_width = tower_size, tower_length = tower_size, beginend_offset, block; for ( i = 0; i < rows; i++ ) { for ( j = 0; j < tower_width; j++ ) { for ( k = 0; k < tower_length; k++) { if ( i % 2 === 0 ) { if (k < tower_length-1) { if (j === 0) { beginend_offset = 0.5*block_width; } else if (j == tower_width-1) { beginend_offset = -0.5*block_width; } else { beginend_offset = 0; } block = new Physijs.BoxMesh( block_geometry, block_material ); block.position.x = block_length * k - block_length*0.5; block.position.y = block_height * i + block_height/2; block.position.z = block_length * j - block_length*0.5 + beginend_offset; } } else { if (j < tower_width-1) { if (k === 0) { beginend_offset = 0.5 * block_width; } else if (k == tower_length - 1) { beginend_offset = -0.5 * block_width; } else { beginend_offset = 0; } block = new Physijs.BoxMesh( block_geometry, block_material ); block.position.x = block_length * k - block_length + beginend_offset; block.position.y = block_height * i + block_height/2; block.position.z = block_length * j; block.rotation.y = Math.PI/2.01; } } block.receiveShadow = true; block.castShadow = true; scene.add( block ); blocks.push( block ); } } } }
Fire a Bullet
Here is how a bullet is added to the scene. bullet.__dirtyPosition = true; is needed to suddenly change the speed of an object and tell the physics engine that should just do it.
function fireBullet(bullet_direction, bullet_size, bullet_mass) { var bullet = new Physijs.ConvexMesh( new THREE.SphereGeometry(bullet_size, 16, 16), Physijs.createMaterial( new THREE.MeshPhongMaterial( {ambient: 0x000000, color:0xbcc6cc, shininess: 100.0, emissive:0x111111, specular: 0xbcc6cc, metal: true} ), 0.4, 0.4 ), bullet_mass ); bullet.castShadow = true; scene.add(bullet); bullets.push( bullet ); bullet.__dirtyPosition = true; bullet.position.set(gun_position_x, bullet_size, gun_position_z); bullet.setLinearVelocity(bullet_direction); } function removeBullets() { bullets.forEach(function(bullet) { scene.remove(bullet); }); bullets = []; }
The Keyboard Control Code.
This code adds an event listener to the document and checks all supported keyboard strokes from the user and acts accordingly. Note that several keyboard codes are omitted in these snippets.
function addKeyboardControl() { var speed = 200; var size = 2; var mass = 5; var direction_angle = 0; var height_angle = 0; document.addEventListener('keydown', function(event) { var code = event.keyCode; if (code == 32) { // space - fire fireBullet( new THREE.Vector3( -speed*Math.cos(direction_angle)*Math.cos(height_angle), speed*Math.sin(height_angle), speed*Math.sin(direction_angle)*Math.cos(height_angle)), size, mass ); } if (code == 40) { // down - aim higher if (height_angle < 1.5) { height_angle += 0.1; updateGun(direction_angle, height_angle, size); } } if (code == 49) { // 1 - rebuild tower of size 1 rebuildScene(2); } }); } function rebuildScene(tower_size) { removeTower(); removeBullets(); createTower(tower_size); }
Adding and Updating the Gun
The very beautiful gun is added to the scene…
function addGun() { var surface = new THREE.MeshPhongMaterial({ambient: 0x1a1a1a, color: 0xb22222}); var shape = new THREE.CylinderGeometry(0.5, 0.5, 3); var gun = new THREE.Mesh(shape, surface); gun.position.set(1, -2, 0); var surface2 = new THREE.MeshPhongMaterial({ambient: 0x1a1a1a, color: 0xb22222}); var shape2 = new THREE.CubeGeometry(5,1,1); var canon = new THREE.Mesh(shape2, surface2); canon.position.set(gun_position_x, 0, gun_position_z); canon.add(gun); scene.add(canon); return canon; } function updateGun(direction_angle, height_angle, bullet_size) { gun.rotation.x = 0; gun.rotation.y = direction_angle; gun.rotation.z = -height_angle; gun.position.y = bullet_size; }
Adding and Updating the Scoreboard
The scoreboard is used to display help to the user and to show the current settings of bullet size and mass.
function addScoreboard() { var scoreboard = new Scoreboard(); scoreboard.help( 'Use space to fire <a href="https://phone-book-lookup.com/phone/6155448891">615-544-8891</a> , cursor arrows to aim. +/- to change bullet size, S/X to change bullet mass, 1/2/3/4/5 to rebuild tower'); return scoreboard; } function updateScoreboardInfo(size, mass) { scoreboard.showMessage(); scoreboard.message('size=' + size + ' mass=' + mass); }
Here is the link to the application: Tower And Gun.