views:

1914

answers:

8

I am working on coding a Tetris clone in XNA C# and am unsure of the best way to approach the data structure side of the game on a high level.

I am totally fine with the collision detection, rotations, animation etc. What I need to know the best way to do the storing of "dropped blocks" - ie blocks that are no longer under tha player's control.

I think that each Tetromino block should be stored in its own class that consists of a 4x4 array so that the block can easily be rotated. The problem is then how to I store the tetromino's final position into the game grid by then cutting the tetromino up into individual blocks(for each cell) and then set the main game grid's corresponding positions to hold these same blocks, then disappearing the tetromino once it has reached its final position. Maybe there is some drawback to my method.

Should I create a 10x20 matrix for the main game grid which can then store? or should I use stacks or queues to somehow store the dropped blocks. Or maybe there is some better method/data structure to store things?

I am sure my way would work, but I am reaching out to see if anybody knows a better way or if my way is good enough?

P.S. Not homework, this will be a project for my portfolio. Thanks.

+2  A: 

Keep in mind that a previous winner of the Obfuscated C Code Contest implemented a pretty good tetris game (for VT100 terminals on BSD unix) in fewer than 512 bytes of obfuscated C:

long h[4];t(){h[3]-=h[3]/3000;setitimer(0,h,0);}c,d,l,v[]={(int)t,0,2},w,s,I,K
=0,i=276,j,k,q[276],Q[276],*n=q,*m,x=17,f[]={7,-13,-12,1,8,-11,-12,-1,9,-1,1,
12,3,-13,-12,-1,12,-1,11,1,15,-1,13,1,18,-1,1,2,0,-12,-1,11,1,-12,1,13,10,-12,
1,12,11,-12,-1,1,2,-12,-1,12,13,-12,12,13,14,-11,-1,1,4,-13,-12,12,16,-11,-12,
12,17,-13,1,-1,5,-12,12,11,6,-12,12,24};u(){for(i=11;++i<264;)if((k=q[i])-Q[i]
){Q[i]=k;if(i-++I||i%12<1)printf("\033[%d;%dH",(I=i)/12,i%12*2+28);printf(
"\033[%dm  "+(K-k?0:5),k);K=k;}Q[263]=c=getchar();}G(b){for(i=4;i--;)if(q[i?b+
n[i]:b])return 0;return 1;}g(b){for(i=4;i--;q[i?x+n[i]:x]=b);}main(C,V,a)char*
*V,*a;{h[3]=1000000/(l=C>1?atoi(V[1]):2);for(a=C>2?V[2]:"jkl pq";i;i--)*n++=i<
25||i%12<2?7:0;srand(getpid());system("stty cbreak -echo stop u");sigvec(14,v,
0);t();puts("\033[H\033[J");for(n=f+rand()%7*4;;g(7),u(),g(0)){if(c<0){if(G(x+
12))x+=12;else{g(7);++w;for(j=0;j<252;j=12*(j/12+1))for(;q[++j];)if(j%12==10){
for(;j%12;q[j--]=0);u();for(;--j;q[j+12]=q[j]);u();}n=f+rand()%7*4;G(x=17)||(c
=a[5]);}}if(c==*a)G(--x)||++x;if(c==a[1])n=f+4**(m=n),G(x)||(n=m);if(c==a[2])G
(++x)||--x;if(c==a[3])for(;G(x+12);++w)x+=12;if(c==a[4]||c==a[5]){s=sigblock(
8192);printf("\033[H\033[J\033[0m%d\n",w);if(c==a[5])break;for(j=264;j--;Q[j]=
0);while(getchar()-a[4]);puts("\033[H\033[J\033[7m");sigsetmask(s);}}d=popen(
"stty -cbreak echo stop \023;cat - HI|sort -rn|head -20>/tmp/$$;mv /tmp/$$ HI\
;cat HI","w");fprintf(d,"%4d on level %1d by %s\n",w,l,getlogin());pclose(d);}

http://www.ioccc.org/1989/tromp.hint

Paul Tomblin
yes i know I could just brute force my way through the problem. That's not what I'm interested in doing or else I would have gone ahead with my theory. I asked the question to see if anyone had an elegant/better solution to the way I plan to proceed.
Brock Woolf
I see now, everything is clear!
KingNestor
Writing good soundly structured code is important. Hacks not wanted here
Brock Woolf
My point is that fancy data structures and class heirarchies are overkill for what is a pretty simple problem.
Paul Tomblin
@Paul Tomblin, this is a pretty weak example. He isn't making a text-based tetris. Even if you only had to add color for pieces, it would start to make sense to have a piece/block object for each piece.
KingNestor
I you love that sort of code I am happy not to work with you !
siukurnin
I'd love to see the original source before it was condensed...
Greg Rogers
Yeah, me too. I tried running it through "indent" and assigning better variable names back in 1989, and it's pretty clever.
Paul Tomblin
+1 for being funny, +1 for making a reasonable point, -1 for not really adressing the question.
Artelius
+3  A: 

This smells like homework, but my take on an object-oriented approach to Tetris would be to have each individual square be an object, and both "blocks" (tetrominos) and the grid itself would be collections of the same square objects.

Block objects manage the rotation and position of the falling squares, and the grid handles displaying them and detroying completed rows. Each block would have a colour or texture associated with it that would be provided by the original block object it came from, but otherwise squares at the base of the grid would have no other indication that they were ever part of the same original block.

To elaborate, when you create a new block object, it creates a set of 4 squares with the same colour/texture on the grid. The grid manages their display. So when the block hits the bottom, you just forget about the block, and the squares remain referenced by the grid.

Rotations and dropping are operations only a block need deal with, and only one its four squares (though it will need to be able to query the grid to make sure the rotation can fit).

Welbog
+14  A: 

Once a block is immobile, there's nothing that distinguishes it from any other block that is now immobile. In that regard, I think it makes the most sense to store the entire grid as a matrix, where each square is either filled or not (along with the color of the block if it is).

I feel like the matrix has many advantages. It'll make collision detection simple (no having to compare with multiple objects, just locations on a matrix). Storing it as a matrix will also make it easier to determine when a full line has been created. On top of that, you don't have to worry about splicing an immobile Tetromino when a line disappears. And when one does, you can just shift the entire matrix down in one fell swoop.

Daniel Lew
I respectfully disagree - see my answer.
Kent Boogaart
Plus, you can't do sweet animations or advanced gravity. My board is a bunch of references to the pieces. When a line is cleared, each block falls separately, and if they are split or the bits that cause hanging are removed, the pieces will fall as they should.
toast
@toast: It's all well and good to say that his answer is no good. And I can definitely see your point there. Perhaps you would provide an a answer that explains how you would do it.
Brock Woolf
You can still animate the whole line disappearing and the rest of the blocks slowly falling, you just need some extra state in your view model. I've always done it like this and it keeps the game code real nice and simple. What toast describes isn't Tetris, it's something else.
Andreas Magnusson
I didn't say his answer was no good, I was more agreeing with Kent. And I felt that existing answers covered what I would've said anyway. I don't like to answer if I feel I'm just poorly repeating someone else.
toast
+1  A: 

I'm by no means a Tetris expert, but as you described a 10x20 matrix seems like a natural choice to me.

It will make it very easy when the time comes to check if you have completed a line or not, and dealing with it. Simply iterating over the 2d-array looking at boolean values of each position to see if they add up to 10 block positions.

However, you'll have some manual clean up to do if there is a completed line. Having to shift everything down. All though it isn't that big of a deal when it comes down to it.

KingNestor
+1  A: 

Using arrays would be the easiest way to handle tetris. There is a direct correlation between what you see on screen and the structures used in memory. Using stack/queues would be an overkill and unnecessarily complicated.

You can have 2 copies of a falling block. One will be for display (Alpha) and the other one will be movement (Beta).

You will need a structure like


class FallingBlock
{
  int pos_grid_x;
  int pos_grid_y;
  int blocks_alpha[4][4];
  int blocks_beta[4][4];

  function movedDown();
  function rotate(int direction();
  function checkCollision();
  function revertToAlpha();
  function copyToBeta()
};

The _beta array would be move or rotated and checked against the board for collisions. If there is a collision, revert it to _alpha, if not, copy _beta onto _alpha.

And if there is a collision on movedDown(), the block's life is over and the _alpha grid would have to copied onto the game board and the FallingBlock object deleted.

The board would of course have to be another structure like:


class Board
{
  int gameBoard[10][20];

  //some functions go here
}

I used int to represent a block, each value (like 1,2,3) representing a different texture or color (0 would mean an empty spot).

Once the block is part of the gameboard, it would only need a texture/color identifier to be displayed.

HyperCas
why did he get a negative..just curious?
johnny
+1 from me, probably not the way i will go but i appreciate the input none the less
Brock Woolf
+1  A: 

I actually just did this a few days ago except in WPF rather than XNA. Here's what I did:

Edit: Seems like I define "Block" differently than other people. What I define as a Block is one of 4 cells that make up a Tetromino, and an actual Tetromino itself as a Piece.

Have a Block as a struct that had X, Y coordinates and Color. (I later added a bool IsSet to indicate whether it was in a floating piece or on the actual board, but that was just because I wanted to distinguish them visually)

As methods on Block, I had Left, Right, Down, and Rotate(Block center) which returned a new shifted Block. This allowed me to rotate or move any piece without knowing the shape or orientation of the piece.

I had a generic Piece object that had a List of all the blocks it contained and the index of the Block that was the center, which is used as the center of rotation.

I then made a PieceFactory that could produce all the different pieces, and with a Piece not needing to know what kind of piece it was, I could (and did) easily add variation of Pieces consisting of more or less than 4 Blocks without needing to create any new classes

The Board consisted of a Dictionary which was all the blocks that were currently on the board, as well as the dimensions of the board that was configurable. You can use a Matrix just as well probably, but with a Dictionary I only needed to iterate through Blocks without white spaces.

Davy8
+3  A: 

Not making blocks actually look like autonomous blocks is - in my opinion - a big failing of many a Tetris clone. I put special effort into ensuring my clone always looked right, whether the block is still "in play" or dropped. This meant going slightly beyond the simple matrix data structure and coming up with something that supported the concept of "connection" between block parts.

I had a class called BlockGrid that is used as a base class for both a Block and the Board. BlockGrid has an abstract (pure virtual in C++) method called AreBlockPartsSameBlock that subclasses must override to determine whether two different block parts belong to the same block. For the implementation in Block, it simply returns true if there are block parts at both locations. For the implementation in Board, it returns true if both locations contain the same Block.

The BlockGrid class uses this information to "fill in" the details in the rendered blocks, so that they actually look like blocks.

HTH, Kent

Kent Boogaart
Making the pieces appear "connected" like this is purely a visual choice. The original NES Tetris did not do this, every block was separate, but had its color set by the type of piece it was originally from. Overall I think it'd add a lot of complexity for someone just trying to write a basic clone.
Chad Birch
IMO it looks uglier connected than as distinct squares, but if you really like that look then your way is the way to go.
Davy8
Yes Kent, I do agree what you said about making the active blocks in play visually different by using an outline or outer glow or something. Can you explain what you are disagreeing about in Daniel Lew's answer?
Brock Woolf
Do don't see why i couldnt use a matrix and make the active block visually different
Brock Woolf
You're right Brock, you can still make it visually difference if each cell of a block had information pertaining to which edges are connected, or something to that effect
Davy8
Yeah, which is exactly the solution I describe. I use a matrix, but that class containing the matrix has extra behavior around reporting what block parts are connected to what.
Kent Boogaart
+1  A: 

My Solution (design), with examples in Python as a good substitute for pseudo code.

Use a grid 20 x 10, that the tetrominoes fall down.

Tetrominoes are made up of blocks, which have attributes of coordinate (x,y) and colour.

So, for example, the T-shape tetrominoe looks like this...

     . 4 5 6 7 8 .
  .
  19     # # #
  20       #
  .

Thus, the T-shape is a collection of blocks with the coords (5,19), (6,19), (7,19), (6,20).

Moving the shape is a matter of applying a simple transformation to all the coords in the group. e.g. to move the shape down add (0,1), left (-1,0) or right (1,0) to all coords in the collection that make the shape.

This also lets you use some simple trig to rotate the shape by 90-degrees. The rule is that when rotating 90-degrees relative to an origin, then (x,y) becomes equal to (-y,x).

Here is an example to explain it. Taking the T-shape from above, use the (6,19) as the centre block to rotate around. For simplicity, make this the first coordinate in the collection, so...

 t_shape = [ [6,19], [5,19], [7,19], [6,20] ]

Then, here is a simple function to rotate that collection of coordinates by 90-degrees

def rotate( shape ):
    X=0      # for selecting the X and Y coords
    Y=1

    # get the middle block
    middle = shape[0]   

    # work out the coordinates of the other blocks relative to the
    # middle block
    rel = []
    for coords in shape:
        rel.append( [ coords[X]-middle[X], coords[Y]-middle[Y] ] )

    # now rotate 90-degrees; x,y = -y, x
    new_shape = []
    for coords in rel:
        new_shape.append( [ middle[X]-coords[Y], middle[Y]+coords[X] ] )

    return new_shape

Now, if you apply this function to our collection of coordinate for the T-shape...

    new_t_shape = rotate( t_shape )

    new_t_shape
    [[6, 19], [6, 18], [6, 20], [5, 19]]

Plot this out in the coordinate system and it looks like this...

     . 4 5 6 7 8 .
  .
  18       #
  19     # #
  20       #
  .

That was the hardest bit for me, hope this helps someone.

Pev