SQUARISM addicted to pixels

Making Tetris

Posted on June 22, 2009

Moving and Rotating a Piece

I wanted to implement these shapes very simply and have Processing handle the rotation for me. This did not work as I wanted it to. When I rotated a piece, it would actually rotate the graphics context so the whole screen would rotate and all the other pieces would get all screwy. So I had to do it myself. I created a local field called rotation and implemented basic Trigonometry to describe the angles and orientation of the blocks. Doing this, I had to assign a block as the pivot point. For example the LPiece starts with block 2, block 1 is 0 blocks left/right(x) and 1 block up/down(y). So I created a constant named blockSize (which is used everywhere so the game can scale). And the psuedocode to make LPiece looks something like this:

1
2
3
// this is just psuedocode.
block[0].x = cos(0 + rotation) * blockSize;  // x
block[0].y = sin(180 + rotation) * blockSize;  // y

At zero degrees rotation, cos(0+0) is 0 and sin(180+0) = 1, so block1 is above block2. Block4 is problematic because it's outside of a circle so the rotational math is more difficult. I solved this by rounding (which is terrible) on blockSize. The block sorta snaps into place and it works all the time. I'd like to fix this someday.

Because this was taking me a while, I worked with LPiece by itself for a few days. Eventually I had the LPiece block moving, rotating and drawing. Next, I needed a gamefield so I could start on motion and the basic game loop of dropping a piece onto the bottom of the gamefield. So I created a few variables that described the gamefield and created drawing routines for a grid pattern that could be used for debugging. The grid looked nice so I kept it. After LPiece worked, I made all the other pieces work in a similar fashion. When the inheritance design was refactored and working right, maintaining all the pieces was quite easy because lots of code was being reused and most editing only needed to be done in one place.

Then I moved onto motion and interactivity. I used processing's keyPressed() method to detect when a key was pressed and then called Piece.setRotation(90) if UP was pressed for example. The left arrow key (key code LEFT) was mapped to Piece.setX(Piece.getX() - blockSize). This worked for now and interactivity was done for now. Later the key presses will be delegated and less hard wired. But for now, there's a proof of concept that works.

I had a test app that showed rotation of a simple piece:
tatris_build_test

The Game State

But now, I needed to set up the game architecture properly. What was a simple test needs to be more organized into New Game / Game Over / Paused and so on. I envisioned "scenes" from a play. Act I would be the intro screen, in Act II all the actors would shuffle off the stage and something else would happen. Unfortunately, this analogy was confusing and slightly misguided. I had been studying Monkey Patrol as mentioned above which was a fairly complicated game to be writing in Processing and I emailed the author. I told him that his structure had been making a lot of sense as I stared and studied it. He explained that it's not "play scenes" but a pattern called The State Pattern where game states can be swapped out at will. This made a lot of sense. A game is a state machine that runs until exit. Game states are objects that by themselves make up things like a pause menu, a game over screen or the main game. The Game State is the metaphor that I needed instead of a scene. It is a scene but it's actually the state of the game and this is key because of how new states are detected.

Firstly, I needed an interface. The interface for GameState would contain a few methods:

The most important one is nextState() which returns itself if we need to stay where we are and returns a new state for whatever's next. For example, if the player has filled up the board we need to return a state of GameOver. If the player is not losing then return PlayState. The main class simply calls the nextState() method of the GameState interface and it doesn't know where the game is, it's just calling nextState() which every GameState is required to have because that's how an interface works. Interfaces are really useful for abstracting away implementation and creating a boundry between inside and outside.

For example, the main game loop is PlayState. It's when the game is playing. If the player presses ESC, the game pauses into the menu screen called MenuState. If the player loses, the game switches to GameOverState. It looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	public GameState nextState() {
		if (inMenu == true) {
			inMenu = false;
			MenuState menuState = new MenuState();
			menuState.setNextState(this);
			menuState.setScreenshot(this);
			return menuState;
		}
 
	    if (gameOver == true) {
			GameOverState gameOverState = new GameOverState();
			gameOverState.setScreenshot(this);
			return gameOverState;
	    }
	    else{
	    	return this;
	    }
 
	}

This code above is the gameState setting up the in-game menu if inMenu is true. Notice that it creates a new MenuState and then sets some local variables on that new object. setNextState() copies the current game into a local variable which is returned and played when the game is unpaused. The setScreenshot method is used to create the illusion of the game paused in the background of the menu text.

The game over screen follows a similar pattern except there's no coming back or unpausing from game over. GameOverState creates a new game and displays a "sorry buddy" type message.

Collision Detection

Then I did collision detection. This meant checking for a few cases:

  • A piece hits the left or right wall? Clamp to the game field.
  • A piece rotates into the wall? Prevent the rotation.
  • A piece hits the stationary tetris blocks (deadGrid[])? Piece sticks and then new piece.
  • A piece rotates into the Dead Grid? Prevent the rotation.
  • A piece moves left or right into the Dead Grid? Prevent the movement. So the active piece can rub up against the pile of blocks but only sticks if it falls down on one.

Developing these checks took a lot of time. It was an iterative process that turned out to be non trivial. I created a few methods that did the various checks. First, in PlayState:

1
2
3
4
5
	// check for blocks on X, -1 is left, 1 is right
	boolean gridCollideX(Block checkBlocks[], int direction) {
 
	// check for blocks below current piece				
	boolean gridCollideY(Block checkBlocks[]) {

In my design, I could have created a DeadGrid object. This would represent the blocks that are done. Like, the blocks on the board that the player has dropped, the pile. The DeadGrid would then be responsible for doing a check to see if the current piece has hit itself. I chose not to do this just because. Creating a DeadGrid object would be much cleaner, but at this point the PlayState has the DeadGrid object represented as an array of blocks. So it's implemented kind of simply now but this seems to work. A better design would be to move everything into a DeadGrid object or move everything into the Piece object. I might change this later.

So these above methods check collision. gridCollideX() is called everytime the player presses left/right (the X axis). gridCollideY() is called every time a player presses down or when the automatic timed downward movement of the piece happens (the Y axis). If it returns true, then the piece collided and some action is taken, else the move is allowed. That "some action is taken" depends on what part of the game called it. The piece could hit on the Y and that means it landed downward on the DeadGrid so it needs to freeze in place and be added to the grid. If a piece hits on the X then it just means it shouldn't go through the DeadGrid so just prevent the movement. That way the piece appears to collide with the DeadGrid and blocks don't go through each other. checkBlocks[] is an array of Block objects which usually make up the current piece. You have to check all 4 blocks in a piece to see if any of them have hit something.

In addition to these checks, a Piece does some collision detection by itself:

1
2
3
4
5
6
7
8
9
10
11
12
// checks to see if piece is near grid to prevent unneccessary checks
public boolean nearGrid(Block deadGrid[][], 
  Point2d playField[], int gridSizeX, int gridSizeY)
 
// does common collision detection for all piece shapes
public boolean rotateCollideHit(Block deadGrid[][], 
  int gridSizeX, int gridSizeY, float testOffsetX[],
  float testOffsetY[], Point2d playField[])
 
// superclass method, never used, to be overriden by piece subclasses
public boolean rotateCollide(Block deadGrid[][], 
  Point2d playField[], int gridSizeX, int gridSizeY)

These checks involve the DeadGrid array. The Piece superclass handles nearGrid(). nearGrid() returns true is a piece is within 2 or so blocks of the DeadGrid. A piece can't collide beyond that so this is an attempt at optimization. The Piece superclass also implements a rotateCollideHit() method which actually does the collision detection. This is confusing but it's like this for a reason. Each piece has a different shape but eventually all the pieces do this rotateCollideHit() function. I put this in the superclass to employ code reuse. The testOffsetX[] and testOffsetY[] arrays are specific to the Piece's shape, in other words, this is like the model of the piece. So rotateCollide() actually just returns false in the Piece superclass. It's never used, it's meant to be overridden.

Then there are some wall checks. The following methods in the Piece object check to see if the piece has gone off the screen. It's pretty straight forward. If a piece's X is greater than the wall width then clamp to width. The piece stays on the play field this way.

1
2
3
4
5
public boolean rotateCollideX(float wallStart, float wallWidth)
 
public boolean rotateCollideY(float roomStart, float roomWidth)
 
public boolean rotateCollideYHit(float roomStart, float roomWidth)

I didn't include all the code for readability. It's pretty simple, when these return true the piece doesn't move. So the "clamping" makes the piece appear to stay on the screen.

Comments (1) Trackbacks (1)
  1. Awesome write up. It is rare to get this much detail about the creative & technical process of making a game. Thanks for sharing!


Leave a comment