Smashinglabs

Sebastian Poręba's blog

3D Tetris with Three.js tutorial – part 1

Learning Three.js is fairly easy. This series may be not the best tutorial ever, but I’ll share my experiences from writing a game – 3D Tetris. I hope you find it useful.

Preparation

First you need to download Three.js: https://github.com/mrdoob/three.js

I also use Stats from mrdoob: https://github.com/mrdoob/stats.js

In both cases you need only files from build directory.

Blox is a nice font for menu and points: http://www.dafont.com/blox.font

To use it you have to convert it with Cufon: http://cufon.shoqolate.com/generate/

In my project all JS files go to js folder, music into music folder and html in root.

HTML

I’m lazy so all the CSS goes directly into html header. It’s not as bad as it sounds – it’s very short and there is only one page so caching is unnecessary anyway. There is not much to explain, just some CSS for the intro and score counter. Also you have to include all js and init cufon.

<!DOCTYPE html>
<html>
  <head>
    <title>Three.js Tetris</title>
	<style>
		body {margin: 0; padding: 0; overflow: hidden;}
		
		#menu {
			position: absolute;
			width: 200px;
			height: 250px;
			
			top: 50%; left: 50%;
			margin: -125px 0 0 -100px;
			
			text-align: center;
			
			border: 3px solid #fff;
			border-radius: 15px;
			
			background-color: #E36B23;
			box-shadow: 2px 10px 5px #888;
		}
		
    #menu p {font-weight: bold; color: #fff;}
    #menu p a {color: #fff;}

		#menu button {
			width: 80px;
			height: 25px;
			
			background-color: #C44032;
			
			border: 3px solid #fff;
			border-radius: 5px;	

			font-size: 14px;
			font-weight: bold;
			color: #fff;			
		}
		
		#points {
			position: absolute;
			width: 120px;
			height: 16px;	

			padding: 12px;
			
			top: 20px;
			right: 80px;
			
			border: 3px solid #fff;
			border-radius: 15px;
			
			background-color: #E36B23;
			box-shadow: 2px 10px 5px #888;	

			font-size: 14px;
			font-weight: bold;
			color: #fff;	

			text-align: right;
			
			display: none;
		}
	</style>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
	</head>
  <body>
	<div id="menu">
		<h1>Three.js Tetris</h1>
    <p>
      Movement: arrows<br>
      Rotation: AD/SW/QE<br>
      Author: <a href="http://www.smashinglabs.pl">smashinglabs.pl</a>
    </p>
		<button id="play_button">Play</button>
	</div>
  
	<div id="points">
		0
	</div>
	
	<script type="text/javascript" src="js/Three.js"></script>
	<script type="text/javascript" src="js/Stats.js"></script>
	<script type="text/javascript" src="js/tetris.js"></script>
	
	<script src="js/cufon-yui.js" type="text/javascript"></script>
	<script src="js/Blox_400.font.js" type="text/javascript"></script>	
	
	<script type="text/javascript">
		Cufon.replace('#menu h1');
		Cufon.replace('#points');
	</script>	
  </body>
</html>

Basic Structure and Three.js init

There are many ways of organizing your game. If you want to host it on a gaming portal, you may need to use some kind of closure (i.e. not use global variables). If your game is very complex, inheritance becomes useful. But for the simple game working fullscreen all these concepts show as hard to maintain and not without some impact on preformace. The project will use one global namespace

var Tetris = {};

All objects, functions and variables will be a member of Tetris. It’s a very useful way to maintain your code and to avoid usage of “this”. To make things even better, every object I use is a singleton. There are some drawbacks – it’s a classic god object and a mixture of everything, but as long as it’s small I don’t care and you shouldn’t either.

We will focus on this structure later, now lets initialize Three.js. I used a tutorial by Aerotwist which is very simple, but plain and good enough. You should probably read it before you proceed.

Tetris.init = function() {
  // set the scene size
  var WIDTH = window.innerWidth,
      HEIGHT = window.innerHeight;

  // set some camera attributes
  var VIEW_ANGLE = 45,
      ASPECT = WIDTH / HEIGHT,
      NEAR = 0.1,
      FAR = 10000;

  // create a WebGL renderer, camera
  // and a scene
  Tetris.renderer = new THREE.WebGLRenderer();
  Tetris.camera = new THREE.PerspectiveCamera(  VIEW_ANGLE,
                                  ASPECT,
                                  NEAR,
                                  FAR  );
  Tetris.scene = new THREE.Scene();
  
  // the camera starts at 0,0,0 so pull it back
  Tetris.camera.position.z = 600;
  Tetris.scene.add(Tetris.camera);

  // start the renderer
  Tetris.renderer.setSize(WIDTH, HEIGHT);

  // attach the render-supplied DOM element
  document.body.appendChild(Tetris.renderer.domElement);
  
  // to be continued...

The introduced Tetris.init is creating a Three.js objects and store them in global namespace. There is a renderer, a scene and a camera. I want the game to be fullscreen so I use window.innerWidth and window.innerHeight. You may want to experiment with fullscreen API there. The camera needs to be pulled back and an exact distance should be determined by the size and position of the game world you use.

Our game world is a 3D wireframe box with lines showing where boxes can be dropped. When you work on a game it’s important to realize that the final effect is what matters most. Don’t be afraid to cheat! In this case, there is no good reason to draw lines on bounding box. We may as well use a normal box geometry, place a vertex on every line cross and connect vertices with lines instead of usual triangles. And luckily there is a Three.js syntax just for that!

  // configuration object
  var boundingBoxConfig = {
    width: 360,
    height: 360,
    depth: 1200,
    splitX: 6,
    splitY: 6,
    splitZ: 20
  };

  Tetris.boundingBoxConfig = boundingBoxConfig;
  Tetris.blockSize = boundingBoxConfig.width/boundingBoxConfig.splitX;
	
  var boundingBox = new THREE.Mesh(
    new THREE.CubeGeometry(
      boundingBoxConfig.width, boundingBoxConfig.height, boundingBoxConfig.depth, 
      boundingBoxConfig.splitX, boundingBoxConfig.splitY, boundingBoxConfig.splitZ), 
    new THREE.MeshBasicMaterial( { color: 0xffaa00, wireframe: true } )
  );
  Tetris.scene.add(boundingBox);

  // first render
  Tetris.renderer.render(Tetris.scene, Tetris.camera);
  // to be continued...

A Three.js API describes CubeGeometry constructor as:

(width <Number>, height <Number>, depth <Number>, segmentsWidth <Number>, segmentsHeight <Number>, segmentsDepth <Number>, materials <Array>, sides <Object>) 

and we make use of “segmentX” options to define how many boxes can be fitted into our gameboard. We use also MeshBasicMaterial option wireframe to draw lines instead of triangles.

Last things to do in init function:

  Tetris.stats = new Stats();
  Tetris.stats.domElement.style.position = 'absolute';
  Tetris.stats.domElement.style.top = '10px';
  Tetris.stats.domElement.style.left = '10px';
  document.body.appendChild( Tetris.stats.domElement );
  
  document.getElementById("play_button").addEventListener('click', function (event) {
    event.preventDefault();
    Tetris.start();
  });
};

We add FPS stats and bind Tetris.start() to the play button. What should be done in start()?

Tetris.start = function() {
   document.getElementById("menu").style.display = "none";
   Tetris.pointsDOM = document.getElementById("points");
   Tetris.pointsDOM.style.display = "block";

   Tetris.animate();
};

Hide instructions, show score box and start first animate() function.

Game loop

You may wonder why there was no setInterval for animation. There is a much better function for that – requestAnimationFrame(). It calls specified functions when the browser is not busy, but no more than 60 times per second. It means that you will have exactly the number of FPS that is possible to render. No need to worry about calculating the best time step for setInterval or clots if you try to render more FPS than it’s possible to calculate. The function is still something new, so on top of our script we will place a compatibility code:

if ( !window.requestAnimationFrame ) {
  window.requestAnimationFrame = ( function() {
    return window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.oRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
    function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) {
      window.setTimeout( callback, 1000 / 60 );
    };
  })();
}

Now we need to write animate() function. Tetris is real-time, but a step of game is quite long, like one z-axis move per second. We need to calculate when to move our block forward, so some time-related variables have to be introduced:

Tetris.gameStepTime = 1000;

Tetris.frameTime = 0; // ms
Tetris.cumulatedFrameTime = 0; // ms
Tetris._lastFrameTime = Date.now(); // timestamp

Tetris.gameOver = false;

Now, the animate function in simplified version has to call Three.js render method and call itself again using requestAnimationFrame. This would make our game static, so we need to calculate a passage of time and act if it was long enough.

Tetris.animate = function() {
  var time = Date.now();
  Tetris.frameTime = time - Tetris._lastFrameTime;
  Tetris._lastFrameTime = time;
  Tetris.cumulatedFrameTime += Tetris.frameTime;

  while(Tetris.cumulatedFrameTime > Tetris.gameStepTime) {
    // block movement will go here
    Tetris.cumulatedFrameTime -= Tetris.gameStepTime;
  }
	
  Tetris.renderer.render(Tetris.scene, Tetris.camera);
	
  Tetris.stats.update();
	
  if(!Tetris.gameOver) window.requestAnimationFrame(Tetris.animate);
}

If you noticed stats update, you may think it’s a little redundant. Stats are obviously counting FPS and we do the same thing. You may want to dig into stats code and re-use some data, but I’ve found it simpler to write my own few lines of code. Stats were not intended for such usage and the overhead is close to zero. Besides, if you’d like to remove FPS counter from your game when it’s released, it could be a problem. The less dependencies the better.

And, at last, we should call init() when the page is loaded:

window.addEventListener("load", Tetris.init);

After this tutorial you should:

  • Know how to setup a renderer and scene with Three.js.
  • Understand what a game loop is and
  • why requestAnimationFrame is great.
  • Know that if it looks good, it’s good.

Grab source from github
If you have trouble with any of these, check tutorial again or ask a question in the comments below.

5 Responses so far.

  1. […] 3D Tetris with Three.js tutorial – part 1 : Smashinglabs […]

  2. […] From Sebastian Poręba, a tutorial on creating a 3D Tetris game using Three.js. […]

  3. Rory says:

    Thanks for sharing.
    Phew ! I’ve got some laerning to do

  4. I want to ask that how you deal with the collision using bounding box.


  • RSS
  • Facebook
  • Twitter

FAQ about Wordpress

This came as a surprise for me but gMap is ...

gMap 3.3.3 released

It was a looong time since I last visited gMap. ...

Talks for Google Dev

Two new slide decks appeared in lectures tab. This time with ...

Talks and lectures w

Every now and then I spend a weekend watching various ...

3D Tetris with Three

In the fifth part of tutorial we add some final ...

FAQ about Wordpress

This came as a surprise for me but gMap is ...

gMap 3.3.0 released

Christmas came early! New version of gMap is ready!

Lecture for GTUG: Ja

Today I gave a lecture for GTUG Krakow about optimizations in ...

Unit testing for jQu

In part 1 I described basics of unit testing in ...

Unit testing for jQu

In part 1 I described some basic concepts behind unit ...