Tetris Question

Started by
5 comments, last by The 89 Vision 17 years, 9 months ago
A while ago, I was working on a Tetris remake with DirectX in C#, but I never finished because I didn't like the way it turned out. Now, I'm doing it in C++ with SDL. Anyways, the history isn't important. The way I handled it before was by having a shape class which had block class objects. I handled everything this way, movement, drawing etc. and I had an ArrayList (which is like a Vector) to store all the shape objects. It would cycle through and do all the shape stuff that needed to be done. I had a seperate boolean array that would get updated when cycleing through the shape arraylist, true for "yes there is a block there" and false for "no, that space is empty" and I would use that for collision testing. It seems like there would be a better way to do it, and I'm just asking if anyone knows of one. My way works pretty well, but it just seems kind of complicated...
Advertisement
When making Tetris myself, I realized that once a shape has fallen, there is no need at all to remember that it is a separate shape from all the others that have fallen. All you need to remember is the location of each block in the whole "well" (as I call it). The only shape you ever need to keep track of is the one that is currently falling. So this allows you to simply have a 2-dimensional array of blocks to represent the well, where a block simply stores a bool that says whether the square is full or empty (along with any visual information, such as what color the block is). Collision detection then becomes a lot easier, because instead of having to compare every block from every shape with the falling shape, you simply have to compare every block in the single well with the falling shape.

Also, when you successfully complete a row, there's no need to figure out which shapes are involved, or "break" any shapes because some of their blocks are being removed (resulting in shapes with only one, two, or three blocks). Since it is all in a single 2D array, just shift all the blocks down by one row, starting above the row that was removed.

My first thought originally was that I was going to have to remember all the shapes even after they fell, because when rows were removed, I'd have to remember how shapes interlocked to know how much to make them fall. Then I played a few games and quickly realized that they always fall exactly the number of rows that you removed, so there's no use in keeping track of all the shapes. Very useful revelation it was to me.
"We should have a great fewer disputes in the world if words were taken for what they are, the signs of our ideas only, and not for things themselves." - John Locke
Ohhh, I think I understand. I'll think about it and post if I have any questions. Thanks for sharing your ideas!
How'd you handle the shapes before they hit the ground?
Quote:Original post by Grantyt3
How'd you handle the shapes before they hit the ground?

Don't overcomplicate things. There isn't even need for any OO as a single "piece" structure or class will do. All that separates one shape from another is its physical layout. Other than that, every piece can be represented by a 4x4 matrix of spots that are either occupied or free.
IMHO there's no need for any class hierarchy for something as simple as a tetris block.
This is how I usually store this kind of information (ripped from an actual Tetris-clone of mine):
	 const int BLOCK_TYPES  = 7;	 const int ROTATIONS	= 4;	 const int BLOCK_X	= 4;         const int BLOCK_Y	= 4;	 typedef int GameBlock[BLOCK_Y][BLOCK_X];	/// indices to our block field:	/// look at the rows to see, which blocks are requested for each rotation	/// look at each row to see the indices of the individual blocks for a single rotation	const int blockIndex[BLOCK_TYPES*ROTATIONS] = {	// line cube S L T etc.		0,  2,  3,  7,  9, 11, 15,		// rotation 0		1,  2,  4,  8, 10, 12, 16,		// rotation 1		0,  2,  5,  7,  9, 13, 17,		// rotation 2		1,  2,  6,  8, 10, 14, 18,		// rotation 3	};	/// unique blocks - since not all blocks really rotate (e.g. the cube doesn't), 	/// only individual blocks are stored in this field ('1' means blocked, '0' means empty)	/// you can re-arrange the arrays to visualise the blocks (:	const GameBlock blocks[] = {		{ {1,0,0,0}, {1,0,0,0}, {1,0,0,0}, {1,0,0,0} },		{ {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {1,1,1,1} },		{ {0,0,0,0}, {0,0,0,0}, {1,1,0,0}, {1,1,0,0} },		{ {0,0,0,0}, {1,1,0,0}, {1,0,0,0}, {1,0,0,0} },		{ {0,0,0,0}, {0,0,0,0}, {1,1,1,0}, {0,0,1,0} },		{ {0,0,0,0}, {0,0,1,0}, {0,0,1,0}, {0,1,1,0} },		{ {0,0,0,0}, {0,0,0,0}, {1,0,0,0}, {1,1,1,0} },		{ {0,0,0,0}, {0,0,0,0}, {0,1,1,0}, {1,1,0,0} },		{ {0,0,0,0}, {0,1,0,0}, {0,1,1,0}, {0,0,1,0} },		{ {0,0,0,0}, {0,0,0,0}, {1,1,0,0}, {0,1,1,0} }, 		{ {0,0,0,0}, {0,0,1,0}, {0,1,1,0}, {0,1,0,0} },		{ {0,0,0,0}, {0,0,0,0}, {0,1,0,0}, {1,1,1,0} },		{ {0,0,0,0}, {0,1,0,0}, {0,1,1,0}, {0,1,0,0} },		{ {0,0,0,0}, {0,0,0,0}, {1,1,1,0}, {0,1,0,0} },		{ {0,0,0,0}, {0,1,0,0}, {1,1,0,0}, {0,1,0,0} },		{ {0,0,0,0}, {1,1,0,0}, {0,1,0,0}, {0,1,0,0} },		{ {0,0,0,0}, {0,0,0,0}, {0,0,1,0}, {1,1,1,0} },		{ {0,0,0,0}, {0,1,0,0}, {0,1,0,0}, {0,1,1,0} },		{ {0,0,0,0}, {0,0,0,0}, {1,1,1,0}, {1,0,0,0} },	};	/// Try to rotate the current block	void RotateBlock(int x, int y) {		// skip to next line in index array		int temp = currentBlock + BLOCK_TYPES;		// adjust on overflow		if (temp >= BLOCK_TYPES*ROTATIONS) {			temp -= BLOCK_TYPES*ROTATIONS;		}		// test if we can do this		if (CanPutBlock(x, y, blocks[blockIndex[temp]])) {			currentBlock = temp;		}	}


That's pretty much all there is wrt block types.

HTH,
Pat.
Oh, I understand. That's kind of what I was thinking, but that clarified my idea. Thanks!
I'd recommend trying to stick with an object oriented approach. I made a tetris clone in a sort of "hack and slash/just get it done" method a few years ago and haven't once looked at the code since.

My point is that you're likely making it to learn, so why not try and practice or pick up some skills that will be useful in more complicated projects down the road?

As for sharing ideas, as I recall, the most challenging part (other than learning a graphics api) was handling shapes rotating, as they don't rotate on any reliable axis.

If I were to do it again now I would start with a base Shape class, which contains blocks(however those would be stored), and an array of configurations (however those would be stored). It would have a rotate function that simply cycles through the configuration array

//something like thisvoid Shape::Rotate( bool bClockwise ){    if( bClockwise )       //add 1 to nCurrentConfigIndex, or go back to 0 (start)    else       //subtract 1 or go to end}


The Update and Render code could also be generic for each shape. Update would change the block position based on the configuration and render would just cycle through the blocks and render them.

Each unique shape would then inherit from the base Shape class and build its own configuration array in its constructor. This way you can add shapes, and configurations, one at a time to make sure they're what you wanted. You could also add new configurations, or shapes that are unique to your clone without making any major changes to the rest of the application. That's the idea anyway.

Oh, and remember to make Shape's destructor virtual.

I also prefer to avoid the 2-dimensional array, since it's just an abstraction anyway as memory is stored in 1-dimension. You can get the index of the array like so.

nIndex = x + y * nRowWidth;

Anyhow, best of luck regardless of how you go about it :)

This topic is closed to new replies.

Advertisement