Creating a Playable Game
By the end of this article we will have a "playable" version of the
SameGame. I have playable in quotes because we will have the game in a
state the will allow the player to click to remove blocks and end the
game when there are no more valid moves left. The game won't be very
feature-rich but will be playable. In the remaining articles we'll add
more features to increase the difficulty and allow the game to be
customized a little.
As for this article we'll be looking into event driven programming
and how to get our game to respond to mouse clicks. Once we can respond
to clicks we'll discuss the algorithm we'll use to remove the blocks
and finally, how to tell when the game is over.
Event Driven Programming
Event driven programming, if you've never done it before, is a
complete paradigm change in programming. If this isn't your first
encounter with event driven programming then go ahead and skip to the
next section.
Up till now you've probably only written procedural programs in C++.
The difference between the two types of programming paradigms is that
the flow of control in event driven programming is determined by events
not a predetermined set of steps. It is a reactive program. The user
does something like click on a button and the program reacts to that
event by executing some code. The main loop in an event driven program
simply waits for an event to happen then calls the appropriate event
handler and goes back to wait for another event. An event handler is a
piece of code that is called each time a specific event happens.
Mouse Clicks
The MFC library is inherently event driven and therefore makes it
pretty easy for us to create event handlers and respond to any event
that we want. To set up event handling in MFC, Visual Studio lists all
of the messages that are available to respond to. In this case messages
are synonymous with events. All of the Windows messages are constants
that start with WM_ followed by the message name. To respond to mouse
clicks in the client area of the view there are messages for the left,
right and middle mouse buttons. The event that we will use is the
WM_LBUTTONDOWN. This message is sent by the MFC framework every time
the user clicks the left mouse button down. All we need to do is set up
an event handler to listen for this message to be sent and then
respond. To add an event handler open up the Properties Window from the
CSameGameView header file. Do this by pressing Alt+Enter or from the
menu View->Other Windows->Properties Window. Below is what you'll
see in the properties window. (If it isn't, make sure your cursor is
placed within the class declaration inside the SameGameView.h file.)
In
the screenshot my cursor is hovering over the "Messages" section, click
on it. Look for the WM_LBUTTONDOWN option, click on it, click the
dropdown as shown in the screenshot below and select "<Add>
OnLButtonDown".
This
will add the OnLButtonDown event handler to your view with some default
code in it to call the CView implementation of the function. Here
we'll add the following code to the function body (changes in bold)
Note that this code won't yet compile, but we'll get to that shortly.
That's OK to do—the code won't compile, but it lets us figure out what
needs to be done to make this function work, without worrying yet about
how to write the other functions we will rely on.
Please do wait to compile the resulting code until you've finished
the article, since the changes will cascade; as we go through how to
implement each of the functions we need, we'll discover we need more
functions. But eventually we'll get through all of them.
void CSameGameView::OnLButtonDown(UINT nFlags, CPoint point) { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Get the row and column of the block that was clicked on int row = point.y / pDoc->GetHeight(); int col = point.x / pDoc->GetWidth(); // Delete the blocks from the document int count = pDoc->DeleteBlocks(row, col); // Check if there were any blocks deleted if(count > 0) { // Force the view to redraw Invalidate(); UpdateWindow(); // Check if the game is over if(pDoc->IsGameOver()) { // Get the count remaining int remaining = pDoc->GetRemainingCount(); CString message; message.Format(_T("No more moves left\nBlocks remaining: %d"), remaining); // Display the results to the user MessageBox(message, _T("Game Over"), MB_OK | MB_ICONINFORMATION); } } // Default OnLButtonDown CView::OnLButtonDown(nFlags, point); }
The two arguments to the function are an integer of bit-flags which
can be ignored and a CPoint object. The CPoint object contains the (x,
y) coordinate of where the mouse was clicked within your view. We'll
use this to figure out which block they clicked. The first few lines of
code are familiar to us by now; we are just getting a valid pointer to
the document. To find the row and column of the block that was clicked
we use some simple integer math and divide the x coordinate by the width
of a block and the y by the height.
// Get the row and column of the block that was clicked on int row = point.y / pDoc->GetHeight(); int col = point.x / pDoc->GetWidth();
Since we are using integer division the result is the exact row and column the user clicked on.
Once we have the row and column we will call a function, DeleteBlocks
(we'll add it next) on the document to delete the adjacent blocks.
This function will return the number of blocks that it deleted. If none
are deleted then the function essentially ends. If there were blocks
deleted then we need to force the view to redraw itself now that we've
changed the game board. The function call Invalidate() signals to the
view that the whole client area needs to be redrawn and UpdateWindow()
does that redraw.
int count = pDoc->DeleteBlocks(row, col); // Check if there were any blocks deleted if(count > 0) { // Force the view to redraw Invalidate(); UpdateWindow(); // ... } }
Now that the board has been updated and redrawn we test if the game
is over. In the section entitled "Finishing Condition" we'll go over
exactly how we can tell if the game is over. For now we'll just add a
call to it.
if(pDoc->IsGameOver()) { // Get the count remaining int remaining = pDoc->GetRemainingCount(); CString message; message.Format(_T("No more moves left\nBlocks remaining: %d"), remaining); // Display the results to the user MessageBox(message, _T("Game Over"), MB_OK | MB_ICONINFORMATION); }
If the game is over we get the number of blocks remaining on the
board and report that to the user. We create a CString object which is
MFC's string class and call its built-in format method. The format
method behaves just like sprintf(). Here we use the MFC _T() macro to
allow for different kinds of strings (i.e. ASCII or wide character
formats). We finally call the MessageBox() function that displays a
small dialog with the title "Game Over" and the message that we created
using the format method. The dialog has an OK button (MB_OK) and an
information icon (MB_ICONINFORMATION).
Now that this event handler is in place we need to implement the
three functions on the document that we called, IsGameOver, DeleteBlocks
and GetRemainingCount. These functions are just simple wrappers for
the same functions on the game board. So they can just be added to the
header file for the document just after the DeleteBoard function, like
the following.
bool IsGameOver() { return m_board.IsGameOver(); } int DeleteBlocks(int row, int col) { return m_board.DeleteBlocks(row, col); } int GetRemainingCount() { return m_board.GetRemainingCount(); }
Once we have added these wrapper functions to the document it is time
to modify the game board to take care of these operations. In the
header file for the game board add the following public methods (again
put them right below the DeleteBoard function).
/* Is the game over? */ bool IsGameOver(void) const; /* Get the number of blocks remaining */ int GetRemainingCount(void) const { return m_nRemaining; } /* Function to delete all adjacent blocks */ int DeleteBlocks(int row, int col);
Two of the functions are fairly complex and will require quite a bit
of code but the GetRemainingCount function simply returns the count of
remaining blocks. We'll store that count a member variable called
m_nRemaining. We need to add this to the game board in the private
member section of the class.
/* Number of blocks remaining */ int m_nRemaining;
Since we are adding another data member to our class we need to initialize it in the constructor like so (changes bolded).
CSameGameBoard::CSameGameBoard(void) : m_arrBoard(NULL), m_nColumns(15), m_nRows(15), m_nHeight(35), m_nWidth(35), // <-- don't forget the comma! m_nRemaining(0) { m_arrColors[0] = RGB( 0, 0, 0); m_arrColors[1] = RGB(255, 0, 0); m_arrColors[2] = RGB(255,255, 64); m_arrColors[3] = RGB( 0, 0,255); // Create and setup the board SetupBoard(); }
We also need to update the count of remaining blocks in the SetupBoard method (changes bolded):
void CSameGameBoard::SetupBoard(void) { // Create the board if needed if(m_arrBoard == NULL) CreateBoard(); // Randomly set each square to a color for(int row = 0; row < m_nRows; row++) for(int col = 0; col < m_nColumns; col++) m_arrBoard[row][col] = (rand() % 3) + 1; // Set the number of spaces remaining m_nRemaining = m_nRows * m_nColumns; }
Deleting blocks from the board is a two step process. First we
change all of the same colored, adjacent blocks to the background color,
in essence deleting them, and then we have to move the above blocks
down and the blocks to the right, left. We call this compacting the
board.
Deleting blocks is a prime candidate for the use of recursion. We'll
create a recursive helper function called DeleteNeighborBlocks that is
private that will do the bulk of the work of deleting blocks. In the
private section of the class right after the CreateBoard() function add
the following.
/* Direction enumeration for deleting blocks */ enum Direction { DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT }; /* Recursive helper function for deleting blocks */ int DeleteNeighborBlocks(int row, int col, int color, Direction direction); /* Function to compact the board after blocks are eliminated */ void CompactBoard(void);
We will use the enumeration for direction in the recursive helper function
that will keep us from trying to recurse back to the block we just came from.
Next up is actually implementing the DeleteBlocks algorithm!
No comments:
Post a Comment
if You Need Any Help Then Write Us:We Come Back With Solution