3D Tetris with Three.js Tutorial - Part 1
Join the DZone community and get the full member experience.
Join For FreeLearning 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.
- Part 1: Introduction and game loop
- Part 2: Static blocks and keeping score
- Part 3: Adding and moving a block
- Part 4: Collision detection
- Part 5: Audio and scoring
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
1
|
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.
Opinions expressed by DZone contributors are their own.
Trending
-
Using OpenAI Embeddings Search With SingleStoreDB
-
How To Scan and Validate Image Uploads in Java
-
What Is React? A Complete Guide
-
How to Optimize CPU Performance Through Isolation and System Tuning
Comments