t3

Jenga Tower and Gun in JavaScript using THREE.js and Physi.js

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
t1 t2 t3
 t4  t5  t6

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, 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, 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.

Leave a Reply

Your email address will not be published. Required fields are marked *