03.12.2013
Tutorial: Tetris with Impact.js
A few weeks back I bought a license for Impact.js, a decent HTML5 game engine I enjoyed working with so far. I bought it to get back into game programming. What I like about HTML5 games is that they are extremely accessible. Everybody has a browser, nothing has to be installed and HTML5 games can be quite engaging.
Get the source code from GitHub
Why Tetris is a good warmup exercise if you’re new to game programming
Developing a game like Tetris is a basic exercise, like playing scales as a musician. Tetris isn’t hard. Some people can write it in 1,024 bytes, including music. But still, Tetris offers a few challenges if you’re new to game programming.
It teaches you the basic concepts of game programming, it teaches you the invaluable lesson of finishing a project, because you’ll notice that you will spend 50% of your time on the last 10% of the game or so.
Tetris doesn’t only consist of moving and rotating pieces. As an absolute minimum, it also has a title screen, a stage selection screen, and a scoreboard, and if you really want to make it right, you have to spend a few hours on polishing the overall experience, mostly the controls – and details matter. I’ll show you later in this tutorial.
Whatever new project you start, whether it’s a game, an app, a website or a desktop application, you have to polish it. People will notice and polishing is what distinguishes the newbie stuff from a pro’s work. A well-polished Tetris can be a significant better experience than a half-baked AAA shooter with thousands of bugs, messy controls, bad sound, imbalances, and no fun core mechanics.
After all, Tetris is a a well-proven game concept and while I developed my version of it, I found myself quite often just playing “this one more round”, even though I only tried to find a small bug in the first place. But it sucked me in. Tetris works.
The most fundamental principle of games and all screen-oriented software
When I was 15, I developed my first game. I didn’t know much about programming back then, but I bought the book “Learning Visual Basic 4”, sat down and taught myself some programming. The result was pretty bad, but I finished it. I even published it in the indie-dev section of a game magazine.
One fundamental problem was that I didn’t grasp the fundamental concept not only of computer games, but of all software that shows data in some way on a screen. It took me a while to figure it out. It’s totally obvious to advanced developers, but I want to explain it for beginners:
A computer game runs entirely in the memory of a computer. Whatever you see on the screen is just a representation of the current game state. All logic is completely independent of what’s shown on the monitor. You could throw the monitor away and still play the game, in memory.
Let me elaborate: Say, you have the Tetris screen. There are walls left and right. If you move a block, you will eventually hit the wall. Whether you hit the wall or not does not depend on the pixels on the screen but only on the internal position of the moving piece and the wall.
The pixels on the screen are not the game. The pixels on the screen only represent this internal state, the objects and their positions.
Note: Obviously, some aspects of a game deal with drawing pixels, but let the game engine do that stuff. If you implement a game, you should hardly be thinking of pixels but rather about the mechanics, objects and abstractions of a game.
This means that you never compare or investigate pixels, the color of pixels, the position of pixels, the velocity of pixels or anything else on the screen. Pixels are drawn according to what’s happening in the computer’s memory.
It’s a one-way street: You only draw stuff on the screen to show the state of a game. You never use information from the screen to affect the game’s state.
You should go so far and even abstract from pixel dimensions most of the time. That means that there shouldn’t be hard-coded pixel dimensions in your code whenever possible, but only relative ones.
Why? Because, say you make a tileset for Tetris and draw them in a 16×16 pixel dimension. Later you decide that these are way too low-res and you want more fancy graphics. You make a new tileset that has 32×32 pixel tiles. If your code contains hard-wired pixel dimensions, you’re kind of screwed and have to change endless lines of codes to make it work again.
It’s better to define your fundamental tile dimension as a constant. The first line of code I wrote for Tetris with Impact.js was this one:
ig.global.TILESIZE = 16;
But you’ll notice that you’ll get lazy from time to time and you put some hard-coded pixel dimensions in your code. I did, I apologize :)
What to Expect from this Tutorial
This article is not a complete tutorial. I provide the entire source code of my Tetris at the end and I wrote quite a few comments to help you understand the code.
In this tutorial, I want to cover some basic aspects of Impact.js if you just started out developing a game with this engine.
You’ll need your own license for Impact.js to run it though. It’s currently 99$ which I consider a good price for what you get.
I had to strip the background-music though, because I licensed it from Lucky Lion Studios and can’t redistribute it. It’s 5$ per song. Go check it out. They have good stuff.
Diving into Tetris with Impact.js
The Main Map
One of the first things you’ll want to do is to set up the basic level layout. Our Tetris consists of only one map. Load up Impact’s level editor Weltmeister to give it the basic layout.
The Tetris play area is a 10×18 field. If you add the border, it’s 12×19. The original version on the Nintendo GameBoy is 12×18, including the border. It doesn’t have a lower and upper border. If you look closely, you’ll notice that my level is actually 20 tiles high. The uppermost row is never used – almost. If you rotate a line block, you’ll see that the very top of the line is placed in the uppermost row. But generally speaking, all blocks spawn in row 3!
Once you’re done, add a second layer, call it collision
to
automatically make it the collision layer for the main level.
Note that in the screenshot, the collision layer is above the main layer. You’d want to sort them such that the collision layer is below the main layer. This is just for demonstration purposes only.
If you want to remove a tile, especially in the collision layer,
just fire up the tileset by hitting SPACE
and click outside the
available tiles. This turns your pointer into an eraser and you can
remove tiles.
Movement & Collision in Impact.js
In Impact.js, entities move by having a velocity > 0. The update()
function updates their positions according to the elapsed time between
two game ticks and their velocity.
Every Jump’n Run game allows the player to move the main character freely. You can move it only 1 pixel if you want. In Tetris, you always want to stay within rows and columns. Every movement must move the piece by at least one tile size.
I found a plugin that solves this problem: GridMovement. There’s a demo, go try it. Or have a look:
However, it’s not exactly what I want. I want to have pieces do a jump into the next column or row. I don’t want to see the smooth movement but have them directly snap to the next position. So I forked the GridMovement plugin and modified it. My version doesn’t use velocity at all, it simply updates the position of an entity directly.
It works well, but leads to a major problem: Collision in Impact.js requires an entity to have a velocity. If two entities are fixed or have a velocity of 0, they will never be checked for collision against each other.
Now we arrived at our first major obstacle in implementing Tetris with Impact.js.
The solution I came up with was to implement collision detection myself. It’s relatively easy, because every entity is always in some sort of grid. If we check directly against the collision map of the main level, collision for the borders are done.
canMoveFromTile: function (tileX, tileY, direction) { var newPos = this.getAdjacentTile(tileX, tileY, direction); return ig.game.collisionMap.data[Math.round(newPos.y)][Math.round(newPos.x)] === 0; },
Now, the question is: How do you check collision against pieces that dropped before?
Once a piece has landed and a short timer expired, the piece gets locked in and a new piece is generated. This lock-in literally means to put all the blocks of that piece directly into the collision map of the main level.
Then, with the new piece, we simply do a collision check against adjacent tiles in the collision map.
Piece Representation
So, what’s a Tetris piece? It’s a container for a bunch of blocks. A block is the most basic entity in Tetris, but a block cannot exist itself. It always comes in a structure with other blocks.
Following OOP principles, a block shouldn’t know anything about other blocks. It’s just a block, has its own properties and methods and that’s it.
What structures a block together with other blocks to a moving Tetris piece, is some sort of container. A container, however, is also just an abstract concept. In Tetris a container itself cannot exist, because a container is a collection of single blocks, but a container doesn’t know anything about how these blocks are shaped to form a piece.
A container is what all pieces have in common. They can rotate, they can move, they can collide with the collision map. They can be locked in. A shape extends a container and implements its actual shape and defines rules on how to rotate itself.
Here’s the code for the L-shaped piece. There’s nothing special about the L, so all methods are in container. The only thing that makes the L different from other pieces is the color of its blocks, its shape, and the rules for rotation.
The rotation is nothing else than a bunch of different shapes that look like you’re rotating them, if you press the button.
EntityBlockShapeL = EntityBlockContainer.extend({
color: EntityBlock.color.COLOR_L,
rotationShapes: [
[[0,0,0],
[1,1,1],
[1,0,0]],
[[1,1,0],
[0,1,0],
[0,1,0]],
[[0,0,1],
[1,1,1],
[0,0,0]],
[[0,1,0],
[0,1,0],
[0,1,1]]
]
});
If you look closely however, you’ll notice that there are rows and columns that contain only zeros. This helps the shape to define a center for rotation. And this is the function to rotate a block. It doesn’t really rotate anything in a mathematical sense, but just realigns all blocks of a shape/container:
adjustBlockPositions: function( shape ) { var entityIndex = 0; for (var row = 0; row < shape.length; row++) { for (var col = 0; col < shape[row].length; col++) { if (shape[row][col] === 1) { var block = this.blocks[entityIndex]; block.pos.x = this.pos.x + (ig.global.TILESIZE * col); block.pos.y = this.pos.y + (ig.global.TILESIZE * row); entityIndex++; } } } },
Line Removal
Line removal should be easy. If a row is full, just remove it. But there’s more. We have to move all upper rows down. If the player clears three rows, just move all upper rows down by three, right?
Wrong. This is something that cost me a bit of time at first, because it’s not entirely obvious. But what happens in this situation?
Row #2 (counted from the bottom) must be moved only one line down, while row #5 and higher must be moved down three lines. Why? Because, if row #2 goes down one line, a gap of three missing lines is created that must be filled.
My solution is average at best from a computational point of view, but it’s not that important right now. I move all rows one by one until nothing can be moved anymore.
Oh and don’t forget to adjust the collision map to the new situation. Otherwise you’re just moving pixels but the collision is deeply disturbed. Guess what happened to me at first :)
How to Make a Row of Blocks Blink
You need to get familiar with a common concept in game programming: state machines. Here are two tutorials that explain them:
http://gameprogrammingpatterns.com/state.html
http://jessewarden.com/2012/07/finite-state-machines-in-game-development.html
If you implement a state machine together with a timer, you can define how long an entity remains in a certain state. For example, the blinking happens only during line removal. You could switch every block in that row to a specific state that tells itself to draw a different animation.
Synchronizing Animations
Speaking of clearing a row: If you play Tetris, the rows that are about to be cleared blink. In Impact.js, every entity has its own timer for animation and they’re not synchronized. If you want to synchronize timers or animations, just define it as a property of the class itself, not as a property of class-instances, like so:
EntityBlock = ig.Entity.extend({ size: ..., animSheet: ..., init: function () { // synchronize the timers of all blocks to use one single timer this.anims.blink.timer = EntityBlock.animationTimer; ... } ... });
EntityBlock.animationTimer = new ig.Timer();
Modules
Not every element in a game is an entity, if you define an entity as
something visible. Every game needs something like managers. In
Tetris, you could play not only once, but two or three rounds to achieve
a different high score. So you need something like a RoundManager
that
deals with keeping track of scores, lines cleared, the current stage the
player is in, the falling speed of the main piece etc. You need
something like a FieldManager
that keeps track of all the changes you
make to the collision map. You have something like a StageSelector
that has nothing to do with a round, but allows the player to select the
starting stage.
With all software, there’s no right or wrong. There are more or less elegant solutions, more or less practical solutions and which solution you implement depends on what you try to achieve.
For example, take the RoundManager
. Is this one guy who has a notebook
and writes down all the statistics of a round. One piece of paper
contains the score, the next piece of paper the stage and so on. If the
player finishes a round, the RoundManager
takes an eraser and removes
everything he wrote down from his papers.
Or does he simply toss the entire notebook and get a new one?
Or do you fire this guy and let a new RoundManager
with his own
notebook enter the stage to keep track of everything that’s going on?
See, there are multiple solutions for the same problem and none is perfect.
- Erasing the papers might be the fastest, but it’s bad if you forget to erase one piece of paper.
- Giving the
RoundManager
a new notebook requires you to have someone else fetch a notebook and tell theRoundManager
that he now has a new one. - Firing the
RoundManager
is a very clean solution, because a newRoundManager
definitely comes with a clean notebook, but affects the most memory.
I decided to dump the RoundManager
and get a new one every time a
round is completed.
Personally, I try to find an acceptable mix of abstractions and concrete
implementations. I don’t need a RoundManagerFactory
, because it’s
definitely not in the scope of my Tetris game. Yes, it would make an
extension of my game more easy, but I don’t plan to do so.
From a practical point of view, most software doesn’t evolve but degenerates. Sometimes it’s even better to throw everything away and rewrite it from scratch and the best maintainability through abstraction won’t help.
On the contrary, if you do not abstract at all, you might find your code too limiting way too soon. Say, if you didn’t think in terms of rounds in Tetris. After one round, you have so many entities, variables, scores, etc. to keep track of and to reset that it cannot be done anymore. Then, your game is limited in such a way that a player can play only one round of Tetris and has to press reload in his browser to play again.
Abstraction and OOP concepts are something that take practice. You’ll get better if you do it often. Generally speaking, I recommend to take some time to think about the architecture of the software or game you’re writing. Try to find and identify not only concrete objects (moving piece, stationary piece, wall), but also concepts in your game.
As an exercise, you could try to identify as many concepts of a board game of your choice as possible, because board games are relatively limited and have a defined scope.
In most games, you’d at least want to think about the following:
There’s a player. Can there be multiple players (split screen) ? Can a
player play multiple rounds? Can a round be paused and continued later
(state must be saved or restored)? Do you want to keep some sort of
statistics over several rounds (if so, you want to keep all
RoundManagers
and not fire them)? Can a round itself be divided in
sub-rounds or other sub-concepts like a FieldManager
? Is there only
one board or are there multiple boards (think Tetris against an AI
player) ?
The best thing to do is to take a concept in a concrete game and make it abstract in such a way that it can be used in multiple games. This is hard, but if you identify such an element or concept, you could write a plugin and have other people use it, too.
Polishing Tetris
Tetris looks pretty basic but requires more polishing than you’ll see at first.
If you have a game with some action – and Tetris is such a game, especially in higher levels when pieces move quite fast – you need to spend a considerable amount of time on fine-tuning the controls. Let me make a claim:
There’s not a single game element that affects the feelings of a player more than the controls.
Why is that? Because if a player has trouble controlling the action on the screen, it leads to a lot of frustration.
On the contrary, snappy and fast controls improve the feeling of the game.
Why do you think are Counter-Strike and Quake such successful games? Partly, because they’re exceptionally well polished in many aspects, but one critical point is that the controls are super direct and fast. As a player, you always have the feeling to be in direct control of your figure and that all of your actions are directly translated into the game world.
Movement
Left/Right Delay
Back to Tetris: If you start out developing Tetris, the movement of your main piece will look like this:
What’s wrong with this picture? If you look closely, the main piece moves instantly once a button is pressed. That’s good. What’s bad is that it is pretty difficult to control a single step to the left or right. The green line shows this clearly: I wanted to place it next to the triangle, but I accidentally placed it on top.
Compare it to this sequence and look closely:
If a button is pressed, the piece moves one step to the left or right, then waits for a tiny period of time and only then moves continuously. As a player, you won’t notice this behavior, but you can feel it. A player feels in maximum control to fine-tune the dropping piece.
Moving a Piece Down
A piece moves down automatically, but only if you do not press the down
button. If you move the piece down automatically while Down
is
pressed, your piece will make an extra jump every 2 or 3 rows or so,
which feels weird.
A timer controls the automatic falling of the piece. You need to reset or pause the timer as long as the move-down button is pressed.
Spawning a New Piece While Holding Down
If a new piece spawns and you still hold the down button, the next piece will also move faster. This unwanted behavior is especially troublesome, if your Tetris building is pretty high already and you cannot allow a single mistake.
With every new piece that spawns, the game must force the player to
release Down
.
Moving Left/Right While Holding Down
In my many test games, I experienced quite often that I pressed several
buttons at the same time. If I kept Down
pushed while moving left and
right, I had a hard time controlling the piece. I figured that Tetris
doesn’t allow multiple buttons to be pressed.
If you keep Down
pressed and then hit left or right, Down
is forced
to be released by the game and down-movement stops.
Delays Before Locking in a Piece
If you “slam down” a piece by having Down
pressed, the piece locks in
instantly. If you let it auto-drop, you can actually move the piece for
a second or so to the left and right to put it underneath another piece
before it gets locked in.
The delay requires a bit of polishing and must feel good.
Color Palette
My game actually has a problem I didn’t fix. The L piece and the “reverse L” are blue and red. These colors are complementary colors and if you look closely, you’ll get the impression that one piece is a bit behind the other piece. This has nothing to do with the actual piece position but only with how the eyes perceive colors.
You could experiment with different color palettes to try to get rid of that effects
The Uppermost Row
See how the uppermost row in my version of Tetris is never used? If the bricks start stacking up and you’re about to loose, you might get a feeling of anxiety, because you feel that there’s more room, but for some reason you cannot fill that room. It’s irritating and hardly noticeable on a conscious level, but it’s there.
It would’ve been better to make the uppermost row look different somehow or delete it altogether.
Sound
There are actually two different sound files for a piece that gets
locked in. If you slam down a piece by holding Down
, you’ll hear a
heavy knock. If you let it auto-drop, the knock is a lighter tap.
This helps on a subconscious level to notice whether you score points
for dropping it fast or not (see the following section Tetris Trivia).
Undocumented Return Button
In the stage select screen, you can actually confirm the stage by
hitting Return
instead of X
or C
, because a first-time player
might not be familiar with rotating a piece at first and wants to
confirm by hitting the most obvious key on the keyboard.
To spare a bit of confusion, you can also press Return
.
Vice versa, I initially allowed only the Return
key to confirm a
stage. This feels weird if you play a couple rounds and have your
fingers on X
and C
anyways.
Isn’t it fascinating how many aspects of a game can be polished?
Tetris Trivia
I took these concepts by analyzing the Game Boy version of Tetris:
- If you keep
Down
pressed until the piece locks in, you’ll get one point per line traveled this way. If you release it in the very last row, you’ll get 0 points. - Clearing one line is 40 points, two is 100, three is 300 and four is 1,200. This is multiplied by the stage. So in stage 10, clearing 4 lines gives you 12,000 points.
- You advance a stage every 10 lines cleared.
- In stage 1, a piece auto-drops a bit more often than every second (0.9s or so).
Adding a Global Scoreboard
If you play my Tetris game, you’ll see a global scoreboard. How do you do this outside of the game?
What I don’t want to do is to combine the game’s code with the website’s code. They should be separated as much as possible.
Since I’m also relatively new to Impact.js, I found a solution that works for me but there might be better solutions (let me know if you have one).
Outside of the game, in the index.html
I define a function:
function setScore( score ) { // do stuff }
You can then use a ImpactMixin
to add a function to the game’s scope:
ImpactMixin = { setScoreCallback: setScore };
Note that the function in the game will be called setScoreCallback
and
not setScore
!
In my game, once the player loses, I have this function:
gameOver: function( roundScore ) { if ( typeof ig.setScoreCallback === 'function' ) { ig.setScoreCallback( roundScore ); } },
What does it do? It checks if the Impact engine has a function called
setScoreCallback
. If so, it calls it. If not, it just passes. If I
wanted to put my game on an iPhone with Impact’s Ejecta, I could simply
take the entire game and there wouldn’t be a scoreboard and no error
would occur.
Another approach would be to make the game itself observable and use the Observer OOP pattern to listen for changes to the score.
Update, 12.12.2013: dmecke from Cunningsoft added B mode, thank you very much.
Resources
Alright, we’re done. Go, play the game and grab the source code for Tetris from GitHub.
More resources:
- A more technical Tetris tutorial
- Some sound effects were made with Bfxr, others were taken from the sound effect library that comes with Apple’s GarageBand
- Graphics are hand-drawn
- Animated GIFs in this article were made with LICEcap
- Recommended read: How do I make games? A Path to Game Development