Reviewing BASECODE3B
In case you''re wondering, after I propose an exercise, I proceed to do the same work that I''m asking you to do. In other words, I grab the latest BASECODE, turn up the music, and start thinking/typing away. And let me tell you, it doesn''t matter how good you are -- there''s still a mental process that makes the code evolve, and it does take time to identify and iron-out all of the kinks. The result of my efforts is called BASECODE3B, and is available wherever other tutorial resources are (the site has changed so many times I''m afraid to take a guess at the time of this writing -- check the NEWS section of the forum for up-to-date location information). You have the option of continuing with your own code, or picking mine up and using that as your new foundation for the next exercise -- whatever works and makes you happy, but I''ll tell you now that I''ve gone to great lengths to comment the heck out of my version of the exercise, and it would serve you well if some things are still a little unclear.
The first thing I did was set up some variables to handle the player''s input. I used toggle flags (on/off switches made from booleans) to throttle the keyboard state so that a keypress only registered once, regardless of how long the key was pressed, on everything except for the DOWN key -- they can hold that one down to let the piece fall to the bottom. In the end, I used four variables for the final inputs that are used by the rest of the game (
bLeftPressed, bRightPressed, bDownPressed, bRotPressed).
Next, I took a look at these keypress flags in order to determine where the player wanted the piece to move/rotate to (if at all). At this point, I had two sets of variables:
G.currX, G.currY, G.currRot - current piece position/orientation
newX, newY, newRot - proposed piece position/orientation
If the piece isn''t moving this frame, the new variables will contain the same values as the current variables. Otherwise, one of the new variables has a modified value. I decided to only allow one direction input per frame in order to simplify movement processing, but I could have just as easily allowed a combination of left/right, down and rotate.
Now for the fun part. I wrote a function called
CanPlacePiece() that took as input the proposed position/orientation, and returned either TRUE or FALSE, depending on whether or not the move/rotation was legal. Now, call it intuition, or call it the fact that I''ve written this before, but I realized that eventually this function would also have to take into account other blocks in the play area, and not just play area borders, so I went ahead and incorporated the collision detection for the blocks as well:
bool CanPlacePiece(int xPos, int yPos, int rot)
{
int x, y;
bool bFits = true;
for (y = 0; y < PIECE_HEIGHT; y++)
{
for (x = 0; x < PIECE_WIDTH; x++)
{
if (G.Pieces[G.currPiece][rot][y][x])
{
// If any block falls outside of the play area,
// we can''t move there
if ( (xPos + x) < 0 ||
(xPos + x) >= PLAYAREA_WIDTH ||
(yPos + y) >= PLAYAREA_HEIGHT )
{
bFits = false;
}
// If there''s already a block where we''d like to place
// one, the piece can''t be moved here
else if (G.PlayArea[yPos + y][xPos + x]) bFits = false;
if (!bFits) break;
}
}
if (!bFits) break;
}
return bFits;
}
The first if statement in the double for-loop ensures that we''re only doing hit-testing on sections of the 4x4 game peice array that actually contain blocks. Just as there are transparent pixels in an image, there are transparent sections in our 4x4 game piece array -- if you need to, try removing this if statement and playing with BASECODE3B to see what I mean. The interior logic (first
if) states, "If ANY block in the game piece falls outside of the play area bounds, the entire piece can''t be moved here". There''s an if-break statement a little farther down that gets us out of the loops if this happens. The second
if states, "Also, if there''s already a block in this position on the play area, the entire piece can''t be moved here". Once again, the cascading break statements get us out of the function as soon as we come to a bad conclusion.
As a stand-alone function, it works just fine. Unfortunately, if you try using it in our existing BASECODE3A, it will always register a collision. Why? Quite simply, the current game piece is already in the play area, so it''s always hitting itself! You need to first remove the current game piece from the play area before checking the validity of the new game piece''s position/orientation. The solution here is to
- Remove the game piece from the play area
- Check the validity of the new position/orientation against the play area
- Replace the game piece into the play area, either in its new position/orientation (i.e. it fits), or back at its original position/orientation (the move was illegal)
If you follow this algorithm, you have to be sure to draw the piece in the end at all times, since you unconditionally removed it in order to do the collision testing.
So long as the proposed position/orientation of the game piece is valid, it''s then time to update our current game piece variables. Finally, when the game piece is redrawn to the play area array, it''s drawn in its proper place.
You have to remember to initialize all members of your G structure before using them, so I threw a block of code into
Game_Initialize():
// Start a piece at the center of the top of the play area,
// falling one level per second
G.currPiece = 0;
G.currX = 5;
G.currY = 0;
G.currRot = 0;
G.currSpeed = 1000;
G.currTimeElapsed = 0;
// Place some random blocks in the play area to check our
// collision testing
for (int i = 0; i < 10; i++)
{
G.PlayArea[(rand() % 10) + 5][rand() % 20] = (rand()%7)+1;
}
The last little loop was an extra so that I could test the game piece collision against some random blocks in the play area -- the fancy
rand() calls are there to make sure that there aren''t any blocks in the first 5 rows of the play area (that could make it hard to move in the beginning), and that I always drop in a block (1-7).
At this point, I was still a little uncontent, so I went ahead and added some code to make the game piece fall naturally. This wasn''t asked of you in the previous exercise, but I figure, what the heck! The code is nothing new to you; timers and animation rates were covered in previous articles, but here it is for your perusal:
//------------------------------------------------------------------------
// FALLING PIECE LOGIC
//------------------------------------------------------------------------
G.currTimeElapsed += G.diffTickCount;
// Suspended time elapsed?
if (G.currTimeElapsed >= G.currSpeed)
{
// Time to lower the next piece. We accomplish this by forcing
// a DOWN keypress. When it comes time to move the piece (see
// MOVEMENT PROCESSING below), this is the first direction that
// is checked, ensuring that a falling piece overrides any other
// keypresses.
G.currTimeElapsed -= G.currSpeed;
bDownPressed = true;
}
As you can probably deduce, I''m pretending that the player touched the DOWN key every time the piece''s falling timer elapses (in BASECODE3 I set currSpeed to 1000 (msec. = 1 sec.). As I''ve stated in the comments, I later make sure that if the piece wants to move down, I disregard whatever the player wanted the piece to do for this frame. Simple as pie.
This concludes the review of BASECODE3B -- everything in this article from this point onwards will be work towards BASECODE3C, which we''re about to start on!
Cut, Copy, Pase, Slice and Dice
So far, we''ve been hacking together proof-of-concept demos in order to demonstrate our ability to code isolated areas of our game project, and our basecode is starting to slowly, but, eventually, resemble the game we''re trying to develope. At some point, however, we have to be able to step back and organize our code a little better, before it ends up being one huge
Game_Main() function with a mess of code blocks.
I''m going to propose some changes in the next few paragraphs, as they pertain to BASECODE3B. You can either adopt similar changes in your own work (where applicable), or have the default BASECODE3B ready to work with. As the title of this section suggests, we''re going to be moving some code around, making slight changes here and there, and generally shaking things up a bit so that when the dust settles, we have a foundation for the actual complete game and not a simple demo.
Game State of Denial
Our first order of business is to keep
Game_Main() from becoming the recepticle of the entire game''s code, and move the actual gameplay to a single game state, so that we can incorporate some of the standard ''phases'' such as a main menu or game over screen. Alright, so let''s define some game states:
// Game States
enum GameState {GAMESTATE_MENU,
GAMESTATE_STARTING,
GAMESTATE_RUNNING,
GAMESTATE_GAMEOVER};
Now, we''ll build a function for each of these states:
// In GAMEMAIN.CPP
void GS_Menu()
{
}
void GS_Starting()
{
}
void GS_Running()
{
}
void GS_GameOver()
{
}
Hmm...can''t forget the actual variables that hold the game states...
// In GLOBALS.H
struct
{
// ... Other stuff
GameState currGameState,
prevGameState;
} G;
I''ll show you how to use the prevGameState variable in a moment. For now, let''s continue by re-shaping our
Game_Main():
void Game_Main()
{
HRESULT hRet;
// Timing
static DWORD lastTickCount = timeGetTime();
DWORD thisTickCount;
//------------------------------------------------------------------------
// TIMING
//------------------------------------------------------------------------
thisTickCount = timeGetTime();
G.diffTickCount = thisTickCount - lastTickCount;
lastTickCount = thisTickCount;
//------------------------------------------------------------------------
// KEYBOARD INPUT
//------------------------------------------------------------------------
// Get current keyboard state
while (hRet = G.lpDIKeyboard->GetDeviceState(256, G.KeyState)
== DIERR_INPUTLOST)
{
if (FAILED(hRet = G.lpDIKeyboard->Acquire())) break;
}
//------------------------------------------------------------------------
// GAME STATE PROCESSING
//------------------------------------------------------------------------
switch (G.currGameState)
{
case GAMESTATE_MENU:
GS_Menu();
G.prevGameState = GAMESTATE_MENU;
break;
case GAMESTATE_STARTING:
GS_Starting();
G.prevGameState = GAMESTATE_STARTING;
break;
case GAMESTATE_RUNNING:
G.prevGameState = GAMESTATE_RUNNING;
GS_Running();
break;
case GAMESTATE_GAMEOVER:
GS_GameOver();
G.prevGameState = GAMESTATE_GAMEOVER;
break;
}
//------------------------------------------------------------------------
// RENDERING
//
// Flip the surfaces. If we''ve somehow lost our surface memory,
// restore all surfaces and reload our images. If we''ve lost our
// surfaces, there''s no reason to flip anymore this frame...
//------------------------------------------------------------------------
hRet = G.lpDDSPrimary->Flip(NULL, 0);
if (FAILED(hRet)) RestoreAllSurfaces();
}
Boy, isn''t that nice and neat looking! Now that''s what I call organization. Here, we''ve handled timing and keyboard input first, so that any other game state can take advantage of these values (hey, now you know why
diffTickCount and
KeyState are in the G structure...). Then, it''s off to the switch statement, which sends the execution to the proper game state function. It is assumed that each game state function is responsible for preparing the backbuffer in any way they see fit, so upon completing the appropriate game state function it''s time to render -- flip the surface to the display.
Second verse, same as the first...
Just in case I didn''t give the use of prevGameState a good mention earlier, I''ll tackle it again here. First, I''ll illustrate what a typical set of executions looks like for a game state:
From within
GS_Starting():
First Run: currGameState = GAMESTATE_STARTING, prevGameState = GAMESTATE_MENU
Second Run: currGameState = GAMESTATE_STARTING, prevGameState = GAMESTATE_STARTING
Third Run: currGameState = GAMESTATE_STARTING, prevGameState = GAMESTATE_STARTING
Fourth Run: currGameState = GAMESTATE_STARTING, prevGameState = GAMESTATE_STARTING
.
.
.
For the first time in each game state function, and for the first time only, prevGameState contains the name of the game state we''ve come from. This allows us to write code like this:
void GS_Starting()
{
if (G.prevGameState != GAMESTATE_STARTING)
{
// One-time initialization goes here
}
// Rest of function...
}
This one-time initialization block could be use for starting timers or initializing other static variables, as it''s guaranteed to be executed only once, and at the very first frame this game state has possession of the processor. One example would be if you wanted to display "Get Ready Player 1" for a period of one second -- here''s a possible implementation:
void GS_Starting()
{
static readyTickCount;
if (G.prevGameState != GAMESTATE_STARTING)
{
readyTickCount = 0;
}
// ...
// Display "Get Ready Player 1"...
// ...
readyTickCount += G.diffTickCount;
if (readyTickCount >= 1000) G.currGameState = GAMESTATE_RUNNING;
}
This game state function will correctly display "Get Ready Player 1" for one second, regardless of from which game state was active previously, or how many times throughout the execution of the program this game state becomes active.
C''est bon, n''est pas?
For BASECODE3B, all you need to do is move all of the game-related code to
GS_Running(), and move the initialization code from INITTERM.CPP relating to the game data into
GS_Starting().
Here''s what I threw into
GS_Menu():
void GS_Menu()
{
HDC hDC;
if (KEYDOWN(DIK_1)) G.currGameState = GAMESTATE_STARTING;
EraseBackground();
G.lpDDSBack->GetDC(&hDC);
SetTextColor(hDC, RGB(255, 255, 255));
SetBkColor(hDC, RGB(0, 0, 0));
TextOut(hDC, 260, 100, "Blocks ''n Lines!", 16);
TextOut(hDC, 255, 130, "Press ''1'' to begin.", 19);
TextOut(hDC, 240, 160, "(LEFT, RIGHT) Move the piece", 28);
TextOut(hDC, 240, 180, "(UP) Rotate the piece", 30);
TextOut(hDC, 240, 200, "(DOWN) Hold to lower", 20);
TextOut(hDC, 240, 220, "(P) PAUSE", 9);
TextOut(hDC, 240, 240, "(ESC) EXIT", 10);
G.lpDDSBack->ReleaseDC(hDC);
}
See how clean things get when you organize a bit? Hmm... I seem to be promising a PAUSE feature, so I may as well flush that out now:
//------------------------------------------------------------------------
// PAUSE MECHANISM
//------------------------------------------------------------------------
if (KEYDOWN(DIK_P))
{
if (bPauseToggle == false)
{
bPauseToggle = true;
// Pressing ''P'' causes G.bPaused to toggle on/off
G.bPaused = !G.bPaused;
}
}
else bPauseToggle = false;
// If paused, display a message on the screen
if (G.bPaused)
{
G.lpDDSBack->GetDC(&hDC);
SetTextColor(hDC, RGB(255, 200, 200));
SetBkColor(hDC, RGB(0, 0, 0));
TextOut(hDC, 260, 200, "-- PAUSED --", 12);
G.lpDDSBack->ReleaseDC(hDC);
// We''re done for this frame
return;
}
This code block belongs to the very beginning of
GS_Running(), and stops the rest of the function from running if we''re paused. The code is straightforward...actually, a little too straightforward. When I tried pausing my program and task-switching to another application and back again, I found nothing on the display but "--- PAUSED ---" and some other garbage pixels. Even worse, when I unpaused the game, the background image was gone! Sure enough, I had forgotten to reload BACKGROUND.BMP in
RestoreAllSurfaces(), which is called whenever there''s a problem (like people task-switching out) and DirectX loses surface memory. So, I added in the proper call to reload the background bitmap, and threw in a couple of calls to the pause code:
if (G.bPaused)
{
EraseBackground();
DrawPlayArea();
// Rest is same as above...
}
Folks, this is what game development is all about; evolving the code. So, now my pause works great, and I move on to
GS_Starting():
void GS_Starting()
{
// Start a piece at the center of the top of the play area,
// falling one level per second
G.currPiece = 0;
G.currX = 5;
G.currY = 0;
G.currRot = 0;
G.currSpeed = 1000;
G.currTimeElapsed = 0;
G.currScore = 0;
// And the next piece is...
G.nextPiece = 3;
// Clear the game array
memset(G.PlayArea, 0, sizeof(int) * PLAYAREA_WIDTH * PLAYAREA_HEIGHT);
// Place some random blocks in the play area to check our
// collision testing
for (int i = 0; i < 10; i++)
{
G.PlayArea[(rand() % 10) + 5][rand() % 20] = (rand()%7)+1;
}
EraseBackground();
G.currGameState = GAMESTATE_RUNNING;
}
Since we don''t have a proper game yet, I need to create some default values for the game variables... I could have just as easily threw this code into
GS_Running() and used the one-time initialization (ala prevGameState), by the way.
Well, only one game state left, and that''s
GS_GameOver():
void GS_GameOver()
{
HDC hDC;
char szText[80];
// ''SPACE'' restarts the game
if (KEYDOWN(DIK_SPACE)) G.currGameState = GAMESTATE_MENU;
EraseBackground();
G.lpDDSBack->GetDC(&hDC);
// White text on a black background
SetTextColor(hDC, RGB(255, 255, 255));
SetBkColor(hDC, RGB(0, 0, 0));
wsprintf(szText, "Final Score: %d", G.currScore);
TextOut(hDC, 240, 60, szText, strlen(szText));
TextOut(hDC, 200, 100, "Game Over! Hit SPACE to restart.", 33);
G.lpDDSBack->ReleaseDC(hDC);
}
I know, I know, there''s no scoring yet! Well, we''ll remember to create a score variable in our G structure at least.
Now, I''m not saying that you could paste all of this code in and run it yet, but we''re getting close...
Continuity
It''s important to make sure that your game has a way of getting to each game state.
GS_Menu() moves along to
GS_Starting() as soon as the ''1'' key is pressed, and
GS_Starting() hands the torch over to
GS_Running() right away, but what about
GS_Running()? How does
GS_GameOver() ever get called?
For the time being, let''s decide that the game (
umm, demo) is over when a block can move no farther down. This would coincide with when a game piece comes to rest and a new game piece is introduced in the original Tetris. Obviously, this means we''ll have to go hunting through our existing code and see if we can pinpoint the information we need to make this statement true. To begin with, arm yourself with a flag:
bool bStopped = false;
In English, we can describe this situation as, "A game piece has stopped when it attempts to move downward but cannot". In BASECODE3B, that scenario is tested with the call to
CanPlacePiece(). Here''s the before and after:
Before:
// Can we move the piece legally?
if (CanPlacePiece(newX, newY, newRot) == false) bMoving = false;
After:
// Can we move the piece legally?
if (CanPlacePiece(newX, newY, newRot) == false)
{
bMoving = false;
if (bDownPressed)
{
bStopped = true;
G.currGameState = GAMESTATE_GAMEOVER;
}
}
Well, the bStopped flag is pretty useless to us here, but it will come in handy soon, so we''ll leave it in. Now we have complete game continuity, through all game states.
Putting it All Together
You''re lucky I''m such a nice guy -- BASECODE3C is available for download, and contains all of the updates in this article, up to this point, including:
- a routine to display the next game piece to play (preview piece)
- the play area has been enlarged four rows upwards to make room for new game pieces entering
- probably other little things I''ve since forgotten
I figure I''ll save you from fiddling with all of the code snippits in this article...
But, sometimes I''m
not such a nice guy. That freebee is going to cost you -- the payment is:
"Finish the basic gameplay."
Muuu ha ha ha ha ha ha ha....
It''s actually not as bad as you think! Here''s what you''ll need to do:
- Grab BASECODE3C (unless you''re using your own creation)
- Use the bStopped flag to trigger new random pieces to start falling once the old one comes to a rest
- Also with the bStopped flag, implement a function that counts and removes completed lines from the bottom of the play area
- Tally the number of completed lines using G.currScore (make up some scoring system)
- Determine the end-game condition. In English, it''s when a game piece is stopped and all (or part) of it is over the top row of the play area.
The way I see it, the only interesting part is the calculation and removal of completed lines. If your brain happens to think a certain way, the routine becomes easy. If your brain leads you in the wrong direction, it will cost you in both time and lines of code. This is creative programming, and there are no rules, so do whatever it takes. Later, we''ll all compare algorithms.
Well, don''t just sit there, get going!
Questions? Comments? Screaming cries for help? Reply to this topic.