🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

05.03 - Move, Twist and Drop

Started by
6 comments, last by Teej 23 years ago
Preface At this moment, you should have a modified version of BASECODE3 that displays blocks inside of the play area. If you don''t, now is not the time to start any bad habits, like say, laziness or confusion, so it''s really in your best interest to create a working demo from the exercise in 05.02 before proceeding. If you insist that you don''t need to do the work because it''s pathetically beneath your skill level, or you just plain can''t figure it out, I''ve supplied BASECODE3a on my webpage which will bring you right up to speed. In my version of the project, I''ve simply filled a 2-dimensional array with random numbers between 0-7, with 0 being ''blank''. Here''s a quick list of what I did:
  • Since I don''t plan on blitting off-screen (i.e. everything I blit is guaranteed to be completely on the display), I''ve removed the clipper. The reason is simple -- BltFast() doesn''t work with a clipper, and it''s an easier to use (and theoretically faster) method than Blt().
  • Code for loading BACKGROUND.BMP was added. Just look for RESOURCE.BMP code and imitate what you see.
  • I used a typical double-for-loop (x,y) for rendering the play area.
Let''s continue with Blocks ''n Lines. Game Pieces To the best of my knowledge, there are seven unique game pieces in Tetris, and here''s what they look like:

 X  X       XX      XX
 X  X    X  XX       XX
 X  XX   X      XXX      XX
 X      XX       X      XX

(I) (L) (J) (O) (T) (Z) (S) 
I''ve labeled them with letters that ''kind of'' resemble them (the ''Z'' and ''S'' are a bit of a stretch), and I''m sure that you all have seen these pieces in action before. Our next job is to feed the shape of these pieces into our game, and it should seem natural to suggest another array -- an array of pieces. At its highest level, we want to be able to access the correct piece by using the piece number as an array index, like so: PIECE Pieces[7]; What about the PIECE data type? How about this: int PIECE[4][4]; Stick them together and you have: int Pieces[7][4][4]; Make sure that you understand how this array was formed - it''s an array of 4x4 arrays. Let''s assume that I said, "And there you go". While you''re busy drawing in the pieces, it will likely occur to you that you could draw some of these pieces four different ways because they rotate in the game! Hmm, we''d better work out a plan for that...here are two options that come to mind:
  1. Store each piece in all four rotations
  2. Rotate the piece on-the-fly (i.e. as the game is playing)
I''ve done it both ways in the past, so I can tell you that both are fine, but I think we should go with the first approach, so that we don''t have extra game logic to contend with later. Okay, so it''s time to add-in room for the four possible rotations for each piece to our pieces array: int Pieces[7][4][4][4]; Think: Seven pieces; 4 rotations per piece; 4x4 grid for each rotation. If you prefer, we''ll use defines: int Pieces[NUM_PIECES][NUM_ROTATIONS][PIECE_HEIGHT][PIECE_WIDTH]; We''re half-way there -- now we''ve got to fill it! The way arrays work, you either supply the data when you initialize the array, or manually insert the data later. You''re not allowed to do something like this: int array[5]; array = {0,1,2,3,4}; // Not Allowed! The problem is even worse in my case, because I have my variables inside of a structure, and you''re not allowed to initialize variables inside of structures. Darn, and I really wanted to initialize the entire thing directly... oh well, no use complaining! Let''s see; I could cheat and move the array out of the structure, or I could write a little routine to load the piece data from disk. I''ve cheated in the past, so it''s time to try the other approach, and it''s not a bad little lesson either. Loading Piece-by-Piece To start, I''ve created a data file called PIECES.DAT, which is available on my webpage, and (currently) looks like this:

0000 0100 0000 0100
1111 0100 1111 0100
0000 0100 0000 0100
0000 0100 0000 0100
 
0000 0000 0000 0000
0220 0220 0220 0220
0220 0220 0220 0220
0000 0000 0000 0000
 
0000 0000 0000 0000
0300 0300 3330 0300
3330 0330 0300 3300
0000 0300 0000 0300
 
0400 0000 4400 0040
0400 4440 0400 4440
0440 4000 0400 0000
0000 0000 0000 0000
 
0050 0000 0550 0000
0050 0500 0500 5550
0550 0555 0500 0050
0000 0000 0000 0000
 
0000 0600 0000 0600
0660 0660 0660 0660
6600 0060 6600 0060
0000 0000 0000 0000
 
0000 0070 0000 0070
7700 0770 7700 0770
0770 0700 0770 0700
0000 0000 0000 0000 
There are seven sections, each with four 4x4 grids for the piece data. Since the blocks are numbered 1-7 by color, these are the numbers I use in the grids. The trick when drawing each of the rotations for a piece is to try and imagine the piece rotating in-place, and then seeing if it looks natural. At this stage, I''m not sure if they''ll look right, but who cares -- I can always edit this file later, once I see the pieces on the display and try rotating them. For the loading of this data file, we''ll need to read this file a row at a time and make sure that the values end up in the correct section of the Pieces array -- here''s what I mean: Pieces[2][1][y][x] ..would refer to the third piece (0,1,2), second rotation (0,1) (remember, all array indicies start at zero!). I was considering leaving this to you as an exercise, but I''ve got other things in store for you, so here you go:

bool LoadPieces()
{
    FILE *fp;
    char  szBuffer[100];
    int   iCurrPiece    = 0,
          iCurrRotation = 0,
          iCurrRow      = 0,
          iCurrColumn   = 0,
          iCurrPos      = 0;
 
    // Open the file for reading
    fp = fopen("pieces.dat", "r");
    if (!fp) return false;
 
    // Process the file line by line
    while (fgets(szBuffer, sizeof(szBuffer), fp) != NULL)
    {
        // Skip empty lines, or any line that doesn''t start with 0-9
        if (szBuffer[0] < ''0'' || szBuffer[0] > ''9'') continue;
 
        // Process all four sets of digits (piece rotation rows)
        iCurrPos = 0;
        for (iCurrRotation = 0; iCurrRotation < NUM_ROTATIONS; iCurrRotation++)
        {
            // Process each digit in the set
            for (iCurrRow = 0; iCurrRow < PIECE_WIDTH; iCurrRow++)
            {
                // Convert this digit from a character to a number and store
                G.Pieces[iCurrPiece][iCurrRotation][iCurrColumn][iCurrRow]
                        = szBuffer[iCurrPos++] - ''0'';
            }
            
            // Skip the space between rotation rows
            iCurrPos++;
        }
 
        // Move to the next row for the piece
        iCurrColumn++;
        
        // Do we have all four rows for the current piece?
        if (iCurrColumn == PIECE_HEIGHT)
        {
            // We''re done the current piece
            iCurrColumn = 0;
            iCurrPiece++;
        }
    }
 
    // Close the file
    fclose(fp);
 
    return true;
} 
May as well do a quick run-through on the code: The data file was designed to be easy to draw in, which means that the code reading it needs to do a little fancy footwork. The file is being read line-by-line, which means that we''re getting a single row of a game piece for all four rotations. With a little forethought, the proper set of for loops comes into focus -- four rows (outer loop), and four blocks per row (inner loop). You could have just as easily wrapped this up into another couple of for loops for the columns and pieces, but here I decided to read data through a single while loop, and that means manually keeping track, column-by-column, of what column we''re on, and what piece each particular column of data is for. Finally, since there are spaces between rows, I skip these spaces by advancing the current position in the line buffer, and ignore entire columns altogether if they don''t start with a digit. Consequently, you could place comment lines in this file without any problems. Ready to drop this function into your BASECODE3(a)? Here''s the steps:
  1. Drop the function into INITTERM.CPP
  2. Add a prototype for it at the top of INITTERM.CPP so it can be used anywhere in the file
  3. Add a call to this function in Game_Initialize(), after the standard init calls
  4. Add the following defines to GLOBALS.H:
    
    // Play Area Dimensions (blocks)
    #define PLAYAREA_WIDTH     10
    #define PLAYAREA_HEIGHT    20
     
    // Block Dimensions (pixels)
    #define BLOCK_WIDTH     16
    #define BLOCK_HEIGHT    16
     
    // Play Area Position
    #define PLAYAREA_OFFSET_X   241
    #define PLAYAREA_OFFSET_Y   97
     
    // Piece Information
    #define NUM_PIECES      7
    #define PIECE_WIDTH     4
    #define PIECE_HEIGHT    4
    #define NUM_ROTATIONS   4 
  5. Add the following to your G structure:
    
    // Game data
    int Pieces[NUM_PIECES][NUM_ROTATIONS][PIECE_HEIGHT][PIECE_WIDTH];
    int PlayArea[PLAYAREA_HEIGHT][PLAYAREA_WIDTH]; 
Okeedokee? Input Controls Before this article ends, I''d like to be able to give you something constructive to work on, which means that we''re going to need to gather input from the user if we''re going to display/move any game pieces. As far as input is concerned, we already know how to receive immediate keyboard data via DirectInput. We also looked at what buffered data is, and some of the benefits of each. Frankly, I could find a use for both types of data in this game, and although they can both be used in parallel, I''m going to stick with immediate data for this project, at least for the time being. I can''t recall if I''ve mentioned this previously, but you have to be aware of how input data is recorded in immediate mode. Quite frankly, what you end up with is a snapshot of the entire keyboard, which means that any keys pressed at the precise moment DirectInput is asked to gather input ends up in your key array. The other consideration, and this is the kicker, is that you''re reading the state of the keyboard every single frame, which is more than you realize... Take, for example, our game. If you wired the up arrow key so that every time it was pressed the currently active game piece rotated 90 degrees, do you know what will happen? That little bugger''s gonna spin like crazy! The reason is simple -- in the time that it takes you to press and release a key, it gets registered as a ''key down'' for multiple frames, and that means you''re rotating it each of those frames. Time for a little creative coding... Since you''re checking the state of the keyboard every frame, you need some way of keeping track of whether or not you''ve already registered a keypress for a specific key. You''re interested in the first ''key pressed'' state of the key, but you want to ignore any others that are contiguous. For keeping track of this, we''ll use a boolean flag that''s declared static, so that you can monitor the state of the key between frames. Finally, you''ll also need another flag that tells the rest of our game what the ''real'' status of the key is (i.e. once per keypress). Here''s a little piece of code that does the trick:

static bool bRotateToggle = false;
bool bRotatePressed       = false;
 
if (KEYDOWN(DIK_UP))
{
    if (bRotateToggle == false) bRotateToggle = bRotatePressed = true;
}
else bRotateToggle = false; 
Here''s a crude graph showing how it works:

KeyDown:       0 0 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 0 0 0
RotateToggle:  f f T T T T T f f f f T T T T T T f f f
RotatePressed: f f T f f f f f f f f T f f f f f f f f 
Take a moment and go through the code, and keep in mind that bRotatePressed starts off being false every frame; in other words, it only makes it to true if the key wasn''t down on the last frame (bRotateToggle == false), and then reverts to false for the rest of the sequence (until the key is released and pressed again). This code snippit should be repeated for any key that is meant to be read once per press. For the down arrow key, which will allow the player to drop the currently active piece, you don''t want to use this code -- we''ll just set the appropriate flag every frame: if (KEYDOWN(DIK_DOWN)) bDownPressed = true; It looks like we''re in good shape to move our active game piece, so let''s get to the actual piece placement... Piece Movement, Round I We may as well dump in the variables we''re going to be using...add these to your G structure:

int currPiece;          // Current piece
int currX;              // x-Pos of current piece
int currY;              // Y-Pos of current piece
int currRotation;       // Rotation angle of current piece (0-3)
int currSpeed;          // Falling speed
int currTimeElapsed;    // How long current piece has been suspended 
Let''s go over what each variable is to be used for:
  • currPiece: the currently active game piece, numbers 1-7
  • currX, currY: the current position of the game piece in the play area, from (0,0) to PLAYAREA_WIDTH, PLAYAREA_HEIGHT (20, 20)
  • currRotation: the number of the current rotation, 0-3
  • currSpeed: the rate of speed that the block falls, in milliseconds
  • currTimeElapsed: your standard ticker for determining when it''s time for the piece to fall one block downward
Now, I''m sitting here, thinking to myself, do we have all of the information we need to make a little demo? Why I do believe we do! There are some important details to writing the final game that are going to get ironed out with this exercise, so be sure to take the time to write this stuff out! Here''s the deal: place a random piece at the centre of the top of the play area, and allow the user to move the piece with the left, right and down keys. Left and right will move the piece one block over, and the down key will lower the piece (i.e. drop it). The up key will rotate the game piece by selecting the rotated piece data in order: 0,1,2,3,0,1,2,3,... The trickiest part of the exercise is going to be the placement of the game piece in the play area in response to the user''s input. Whether the piece wants to be rotated or moved left, right or down, you need to first verify that the proposed new position/orientation of the piece is legal before updating the currX, currY and currRotation variables. I highly recommend writing a separate function that accepts as parameters the proposed position and orientation of the game piece, and have the function verify that none of the blocks in the piece falls outside of the play area. Think about it -- it''s possible that the 4x4 piece array can fall outside of the play area, yet the physical game piece itself is still legally inside (picture the (I) piece standing vertically against one of the sides of the play area). Here, a double-for-loop would do the trick, ''hint, hint''. If the piece is to move, rotate or fall (either with the down key or because a timer has elapsed), and your verification function says ''all systems GO'', you''ll need to remove the piece from the play area in it''s current position before drawing it in again at it''s new position/orientation, so no cheating -- don''t clear the entire array! Use the fact that you know exactly where the piece currently sits in the play area array, and which elements of the 4x4 array contain blocks. This will come in especially handy later when putting the final game together (there could be block debris around the game piece, for instance). All that the demo needs to do is prove that you can move the piece and rotate it in the play area. Don''t make life harder on yourself by incorporating multiple pieces -- once the game piece is resting on the bottom, you should still be able to move it left and right, as well as rotate it (in some cases). Adding new pieces and letting them pile up is the subject of the next article. And lastly, don''t worry if you get lost/confused with the exercise; I''ll eventually supply my version of the completed code along with a detailed explaination, but until then please give it a shot. Questions? Comments? Please Reply to this topic.
Advertisement
Hi,

Now, before anyone screems bloddy hell, it''s true that currently there''s no webpage to get the following from:
  • BASECODE3a

  • PIECES.DAT


I''m working on it -- in the meantime, you''ll only need BASECODE3a if you didn''t get through the exercise in 05.02, and you could easily paste the numbers in the article to a text file for your own PIECES.DAT.

Sorry ''bout the delay...will post the moment everything is available...

Teej
Hey Teej, if you want to email me the files (dbrokens@sfu.ca) I will put them up on my site along with the other files I have been mirroring. Just a temporary fix but it could help people out.



"Fall seven times, stand up eight."
-Japanese Proverb

Edited by - Ible on July 18, 2001 1:42:56 AM

Edited by - Ible on July 18, 2001 1:44:09 AM
Ible: I didn''t download that directx7HelpFile.zip(don''t wanna
waste yer bandwidth ~,^), but if that''s just the help file that
came with dx7, it''s kinda unnecessary, given that it''s on the
Dx8 sdk download site on microsoft.com. Under extras or
something.

-Hyatus
"int mycents=100/50;"
Don''t feel bad, it''s the university''s bandwith not mine After all, I have to get *something* for all the money I am throwing at them
Hey folks!

Great job, Teej, i'm learning a lot, but i think your LoadPieces() rutine is a bit wrong, i pasted it to my code and there was a problem somewhere. I was a bit confused of all the iCurrStuff, but i find out that this is really good approach. I worked out this rutine with some changes (cosmetic). I think the worst was it to manage all the iCurr's. Here's the code


bool LoadPieces(){    FILE *fp;    char  szBuffer[100];    int   iCurrPiece    = 0,	  iCurrRotation = 0,          iCurrRow      = 0,          iCurrColumn   = 0,          iCurrPos      = 0;	    // Open the file for reading    fp = fopen("Pieces.dat", "r");    if (!fp) return false;		// Process the file line by line	while (fgets(szBuffer, sizeof(szBuffer), fp) != NULL) {        // Skip empty lines, or any line that doesn't start with 0-9        if (szBuffer[0] < '0' || szBuffer[0] > '9') continue;		// Process all four sets of digits (piece rotations in one row)		iCurrPos = 0;		for (iCurrRotation = 0; iCurrRotation < NUM_ROTATIONS; iCurrRotation++) {            // Process each digit in the set			for (iCurrColumn = 0; iCurrColumn < PIECE_WIDTH; iCurrColumn++)                // Convert this digit from a character to a number and store				G.Pieces[iCurrPiece][iCurrRotation][iCurrRow][iCurrColumn]					= szBuffer[iCurrPos++] - '0';			// Skip the space between rotations in a row			iCurrPos++;		}		// Move to the next row for the piece		iCurrRow++;		// Do we have all four rows for the current piece?		if (iCurrRow == PIECE_HEIGHT) {			iCurrRow = 0;			iCurrPiece++;		}	}		// Close the file	fclose(fp);	return true;}  


Yes, it's not too different, but it works! So guys, try either this or the original and give comments...
See ya,


"Sweet child in time
you see the line
line that's strong between
the good and bad..."
- Deep Purple

Edited by - Moondog on July 19, 2001 4:25:12 PM
"Sweet child in timeyou see the lineline that's strong betweenthe good and bad..." - Deep Purple
Hello everybody, that''s me again!

I''ve done my input validation this time, it seems to fully operate, but: and that''s the reason i''m posting, some key presses are still ignored, wouldn''t it be better to work with buffered input? i think this is the matter of that ''variables toggling'' with non-buffered input. Am i right?
See ya all,

P.S. i mean-> i press, for example, left arrow twice or thrice, and not every key press is caught by program, but it is not the case of bad keyboard input validation, i''m sure

"Sweet child in time
you see the line
line that''s strong between
the good and bad..."
- Deep Purple
"Sweet child in timeyou see the lineline that's strong betweenthe good and bad..." - Deep Purple
Haha Moondog! I did the exact same thing: switched the rows & cols right off the bat (it was bothering me , hehe)& then saw your post. Haha, makes no difference I suppose since the pieces are square.

This topic is closed to new replies.

Advertisement