In this five part series, we'll be creating a version of a game called SameGame using the Microsoft Foundation Class library from start to finish. We'll include features beyond the simple removing of blocks in the game. We'll implement an undo/redo subsystem and some user configuration dialogs. We'll show you step by step with not only source code but screenshots how to build a fun game from start to finish and how to use Microsoft's MFC classes. Every article comes with complete source code, so you can build and run the game yourself.
The rules to the SameGame are quite simple, you try to remove all of the colored blocks from the playing field. In order to remove a block the player must click on any block that is next to, vertically or horizontally, another with the same color. When this happens all of the blocks of that color that are adjacent to the clicked block are removed. All of the blocks above the ones removed then fall down to take their place. When an entire column is removed, all columns to the right are shifted to the left to fill that space. The blocks aren't shifted individually but as a column. The game is over when there are no more valid moves remaining. The goal is to end with an empty board in as little time as possible. Some versions of the SameGame use a scoring algorithm that can be implemented as an additional exercise for the user.
You'll need to have a basic C++ knowledge of functions, recursion, classes, and inheritance. The code was written using Visual Studio 2005 on Windows XP although later versions of Visual Studio should be fine although the screenshots will look slightly different from what you will see. You must use the Standard or Professional edition of Visual Studio; Visual Studio Express cannot be used, because it does not come with MFC.
If you are student, you may be able to get the Professional Version of Visual Studio for FREE from Microsoft DreamSpark.
If you are not a student, and you do not have the full version of Visual Studio, can work through this tutorial using a trial version of Visual Studio.
First and foremost, you'll learn some basics of how to create your own game. You'll learn about the Microsoft Foundation Class library and some basic usage and the Document/View architecture paradigm.
Here's an outline of the series, with links to each article:
MFC is an easy-to-use library, especially for a simple game like the one we want to make. It will make it easy to create an application with a true Windows look-and-feel.
In this article we'll be using Visual Studio 2005 to create our game. The following instructions can easily be adapted to all other versions of Visual Studio. First start up Visual Studio and create a new project. The type of project is "Visual C++" -> "MFC" -> "MFC Application".
Next the MFC application wizard will appear. If you do not choose the name SameGame, then the names of your classes will be slightly different than those that appear in this article. This allows you to select quite a few options that the resulting generated code will include. For our simple game we can disable quite a few of these options. The following graphics show which options to select in order to get the project just the way we want it.
Selecting "Single document" allows the application to use the document/view architecture when multiple documents aren't necessary. The last setting of interest on this page is "Use of MFC". The two options are for a shared DLL or as a static library. Using a DLL means that your users must have the MFC DLLs installed on their computer, which most computers do. The static library option links the MFC library right into your application. The executable that is produced will be larger in size but will work on any Windows machine.
Advance through the next three pages, taking the defaults until the following page is displayed.
(If you are using Visual 2010, this screen does not have a "None" option for Toolbars. Just choose "Use a Classic Menu" without checking either toolbar.) A thick frame allows the user to resize the window. Since our game is a static size, un-check this option. A maximize box isn't needed, nor is a status bar or a toolbar. Advancing to the next page will bring you to the "Advanced Features" page.
Turn off printing, ActiveX controls and set the number of recent files to zero. Since we won't actually be loading any files, this option won't be necessary. The last page of the MFC Application Wizard presents you with a list of generated classes.
Four classes that will be generated for you are the basis for the game. The first on the list is the view class, here it is called CSameGameView. I will come back to this class in a minute. The next class in the list is the application class. This class is a wrapper for the entire application and a main function is provided for your application by this class. The base class isn't selectable and must be CWinApp.
The next class in the list is the document class, CSameGameDoc based on the CDocument class. The document class is where all of the application data is stored. Again the base class cannot be changed.
The last class is the CMainFrame class. This CFrameWnd based class is the wrapper class for the actual window. The main frame class contains the menu and the client area view. The client area is where the actual game will be drawn.
Now back to the view class. The base class is a dropdown with a list of views that are generally available, each with its own use and application. The default view type is CView, which is a generic view where all of the display and interaction with the user must be done manually. This is the one that we want to select.
I will quickly go down the list and explain what each view type is used for, just for your information. The CEditView is a generic view which consists of a simple text box. The CFormView allows the developer to insert other common controls into it, i.e. edit boxes, combo boxes, buttons, etc. The CHtmlEditView has an HTML editor built into the view. The CHtmlView embeds the Internet Explorer browser control into the view. The CListView has an area similar to an Explorer window with lists and icons. The CRichEditView is similar to WordPad; it allows text entry but also text formatting, colors and stuff like that. A CScrollView is a generic view similar to CView but allows scrolling. Finally the CTreeView embeds a tree control into the view.
Finishing the MFC Application Wizard will produce a running MFC application. Since we haven't written any code yet it is a very generic window with nothing in it, but it is a fully functioning application all the same. Below is a screenshot of what your generic application ought to look like. To build your application, you can go to the Debug menu, and select Start without Debugging. Visual Studio may prompt you to rebuild the project—select "Yes".
Notice it has a default menu (File, Edit and Help) and an empty client area. Before we get to actual coding I'd like to explain a little about the document/view architecture that is used in MFC applications and how we are going to apply it to our game.
The document/view architecture is an interesting paradigm where we separate the actual application data from the displaying of that data to the user. The document contains all of the data while the view gets the data from the document and displays it to the user is some fashion. Here our data is the actual game board, the time it takes to complete the game and other related information. Our view displays the game board as colored blocks and allows the users to click them. The view handles the user interaction and modifies the game data in the document accordingly, then the view is updated to reflect the changes and the cycle continues.
By selecting the document/view architecture option in the MFC Application Wizard, the code base is generated along with all of the mechanics relating the two.
It is finally time to get started coding. Before we can display anything on the screen we need data to back it up so we'll start with developing the document portion of the application followed by the display of that data.
First we'll create a class that represents our game board, let's call it CSameGameBoard. Create a new class by right-clicking on the SameGame project in the Solution Explorer and selecting "Add -> Class..." or "Add Class..." from the Project menu. We want a C++ class from the C++ group and click "Add". This will bring up the Generic C++ Class Wizard. Fill it out with the class name that we chose as it appears below.
Now let's fill in the game board class. Here is the code for the header file.
#pragma once class CSameGameBoard { public: /* Default Constructor */ CSameGameBoard(void); /* Destructor */ ~CSameGameBoard(void); /* Function to randomly setup the board */ void SetupBoard(void); /* Get the color at a particular location */ COLORREF GetBoardSpace(int row, int col); /* Accessor functions to get board size information */ int GetWidth(void) const { return m_nWidth; } int GetHeight(void) const { return m_nHeight; } int GetColumns(void) const { return m_nColumns; } int GetRows(void) const { return m_nRows; } /* Function to delete the board and free memory */ void DeleteBoard(void); private: /* Function to create the board and allocate memory */ void CreateBoard(void); /* 2D array pointer */ int** m_arrBoard; /* List of colors, 0 is background and 1-3 are piece colors */ COLORREF m_arrColors[4]; /* Board size information */ int m_nColumns; int m_nRows; int m_nHeight; int m_nWidth; };
This class is conceptually quite simple. It contains a pointer, called m_arrBoard, to a two dimensional array of integers that represent empty (0) or one of the three colors (1-3). We add member variables to keep track of the rows (m_nRows), columns (m_nColumns), pixel width (m_nHeight) and height (m_nHeight). There are also functions to create, set up and delete the board.
The create method needs to allocate the two dimensional array to store the game board and initializes all of the blocks to empty. The setup method will reset the game board by randomly choosing a color for each space on the board. Finally the delete method de-allocates the memory that we are using for the game board to eliminate memory leaks.
In the board there is also an array COLORREF types. A COLORREF is simply a 32-bit unsigned integer that contains RGBA color values for MFC applications. This array contains the colors for the background, at index zero, and the block colors at indices one through three. These indices are the same numbers that are held in the two dimensional array of integers. In the constructor below we are using the RGB macro to create a COLORREF value from three integers representing the red, green and blue values.
Below is the implementation of the CSameGameBoard class, in SameGameBoard.cpp.
#include "StdAfx.h" #include "SameGameBoard.h" CSameGameBoard::CSameGameBoard(void) : m_arrBoard(NULL), m_nColumns(15), m_nRows(15), m_nHeight(35), m_nWidth(35) { 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); } CSameGameBoard::~CSameGameBoard(void) { // Simply delete the board DeleteBoard(); } 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; } COLORREF CSameGameBoard::GetBoardSpace(int row, int col) { // Check the bounds of the array if(row < 0 || row >= m_nRows || col < 0 || col >= m_nColumns) return m_arrColors[0]; return m_arrColors[m_arrBoard[row][col]]; } void CSameGameBoard::DeleteBoard(void) { // Don't delete a NULL board if(m_arrBoard != NULL) { for(int row = 0; row < m_nRows; row++) { if(m_arrBoard[row] != NULL) { // Delete each row first delete [] m_arrBoard[row]; m_arrBoard[row] = NULL; } } // Finally delete the array of rows delete [] m_arrBoard; m_arrBoard = NULL; } } void CSameGameBoard::CreateBoard(void) { // If there is already a board, delete it if(m_arrBoard != NULL) DeleteBoard(); // Create the array of rows m_arrBoard = new int*[m_nRows]; // Create each row for(int row = 0; row < m_nRows; row++) { m_arrBoard[row] = new int[m_nColumns]; // Set each square to be empty for(int col = 0; col < m_nColumns; col++) m_arrBoard[row][col] = 0; } }
Now that we have our game board encapsulated into an object we can create an instance of that object in the document class. Remember that the document class contains all of our game data and it is separated from the view or display code. We will then set up the document as follows. Here is the header file, SameGameDoc.h (changes bolded).
#pragma once #include "SameGameBoard.h" class CSameGameDoc : public CDocument { protected: // create from serialization only CSameGameDoc(); virtual ~CSameGameDoc(); DECLARE_DYNCREATE(CSameGameDoc) // Attributes public: // Operations public: /* Functions for accessing the game board */ COLORREF GetBoardSpace(int row, int col) { return m_board.GetBoardSpace(row, col); } void SetupBoard(void) { m_board.SetupBoard(); } int GetWidth(void) { return m_board.GetWidth(); } int GetHeight(void) { return m_board.GetHeight(); } int GetColumns(void) { return m_board.GetColumns(); } int GetRows(void) { return m_board.GetRows(); } void DeleteBoard(void) { m_board.DeleteBoard(); } // Overrides public: virtual BOOL OnNewDocument(); protected: /* Instance of the game board */ CSameGameBoard m_board; // Generated message map functions protected: DECLARE_MESSAGE_MAP() };
Most of this code should look familiar to you except for a few things that are MFC specific. For now we can ignore the DECLARE_DYNCREATE and DECLARE_MESSAGE_MAP lines, they are boilerplate MFC directives.
At this point the document is actually a very simple wrapper to the game board class. In later articles we'll add more functionality that will require changes to the document but for now it is fairly simple. We added an instance of the game board class and then seven functions that call similar functions on the board. This will allow the view to access the board information via the document. The source file for the document (SameGameDoc.cpp) is also very simple since all of the functions that we added have their implementation in-line (changes bolded).
#include "stdafx.h" #include "SameGame.h" #include "SameGameDoc.h" #ifdef _DEBUG #define new DEBUG_NEW #endif // CSameGameDoc IMPLEMENT_DYNCREATE(CSameGameDoc, CDocument) BEGIN_MESSAGE_MAP(CSameGameDoc, CDocument) END_MESSAGE_MAP() // CSameGameDoc construction/destruction CSameGameDoc::CSameGameDoc() { } CSameGameDoc::~CSameGameDoc() { } BOOL CSameGameDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // Set (or reset) the game board m_board.SetupBoard(); return TRUE; }
Really all we added was the call to the SetupBoard function in the OnNewDocument handler in the document. All this does is allows the user to start a new game with the built-in accelerator Ctrl+N or from the menu File->New.
As we continue through the series of articles we'll be adding new functions to both the game board and the document to implement different features for the game but for now we are done with the document and are ready to display this information in the view.
Now that the document contains an initialized game board object we need to display this information to the user. This is where we can actually start to see our game come to life.
The first step is to add code to resize the window to the correct size. Right now the window is a default size that isn't what we want. We'll do this in the OnInitialUpdate override. The view class inherits a default OnInitialUpdate that sets up the view and we want to override it so that we can resize the window when the view is initially updated. This can be achieved by opening up the Properties Window from the CSameGameView header file (which will actually be called SameGameView.h). Do this by pressing Alt+Enter or from the menu View-> Properties Window (on some versions of Visual Studio, it will be View-> Other Windows -> Properties Window). Below is what you'll see in the properties window.
In the screenshot my cursor is hovering over the "Overrides" section; click on it. Look for the OnInitialUpdate option, click on it, click the dropdown as shown in the screenshot below and select "<Add> OnInitialUpdate".
This will add the OnInitialUpdate override to your view with some default code in it to call the CView implementation of the function. Then we just add a call to the ResizeWindow function that we will write. So this leaves us with the following in the header file (changes bolded).
#pragma once class CSameGameView : public CView { protected: // create from serialization only CSameGameView(); DECLARE_DYNCREATE(CSameGameView) // Attributes public: CSameGameDoc* GetDocument() const; // Overrides public: virtual void OnDraw(CDC* pDC); // overridden to draw this view virtual BOOL PreCreateWindow(CREATESTRUCT& cs); protected: // Implementation public: void ResizeWindow(); virtual ~CSameGameView(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif // Generated message map functions protected: DECLARE_MESSAGE_MAP() public: virtual void OnInitialUpdate(); }; #ifndef _DEBUG // debug version in SameGameView.cpp inline CSameGameDoc* CSameGameView::GetDocument() const { return reinterpret_cast<CSameGameDoc*>(m_pDocument); } #endif
While we're adding in the resize code, we also need to add drawing code to the CSameGameView class. The header and source files for the view already contain a function override called OnDraw. This is where we'll put the drawing code. Here is the full source code for the view (changes bolded).
#include "stdafx.h" #include "SameGame.h" #include "SameGameDoc.h" #include "SameGameView.h" #ifdef _DEBUG #define new DEBUG_NEW #endif // CSameGameView IMPLEMENT_DYNCREATE(CSameGameView, CView) BEGIN_MESSAGE_MAP(CSameGameView, CView) END_MESSAGE_MAP() // CSameGameView construction/destruction CSameGameView::CSameGameView() { } CSameGameView::~CSameGameView() { } BOOL CSameGameView::PreCreateWindow(CREATESTRUCT& cs) { return CView::PreCreateWindow(cs); } // CSameGameView drawing void CSameGameView::OnDraw(CDC* pDC) // MFC will comment out the argument name by default; uncomment it { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Save the current state of the device context int nDCSave = pDC->SaveDC(); // Get the client rectangle CRect rcClient; GetClientRect(&rcClient); // Get the background color of the board COLORREF clr = pDoc->GetBoardSpace(-1, -1); // Draw the background first pDC->FillSolidRect(&rcClient, clr); // Create the brush for drawing CBrush br; br.CreateStockObject(HOLLOW_BRUSH); CBrush* pbrOld = pDC->SelectObject(&br); // Draw the squares for(int row = 0; row < pDoc->GetRows(); row++) { for(int col = 0; col < pDoc->GetColumns(); col++) { // Get the color for this board space clr = pDoc->GetBoardSpace(row, col); // Calculate the size and position of this space CRect rcBlock; rcBlock.top = row * pDoc->GetHeight(); rcBlock.left = col * pDoc->GetWidth(); rcBlock.right = rcBlock.left + pDoc->GetWidth(); rcBlock.bottom = rcBlock.top + pDoc->GetHeight(); // Fill in the block with the correct color pDC->FillSolidRect(&rcBlock, clr); // Draw the block outline pDC->Rectangle(&rcBlock); } } // Restore the device context settings pDC->RestoreDC(nDCSave); br.DeleteObject(); } // CSameGameView diagnostics #ifdef _DEBUG void CSameGameView::AssertValid() const { CView::AssertValid(); } void CSameGameView::Dump(CDumpContext& dc) const { CView::Dump(dc); } // non-debug version is inline CSameGameDoc* CSameGameView::GetDocument() const { ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CSameGameDoc))); return (CSameGameDoc*)m_pDocument; } #endif //_DEBUG void CSameGameView::OnInitialUpdate() { CView::OnInitialUpdate(); // Resize the window ResizeWindow(); } void CSameGameView::ResizeWindow() { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Get the size of the client area and the window CRect rcClient, rcWindow; GetClientRect(&rcClient); GetParentFrame()->GetWindowRect(&rcWindow); // Calculate the difference int nWidthDiff = rcWindow.Width() - rcClient.Width(); int nHeightDiff = rcWindow.Height() - rcClient.Height(); // Change the window size based on the size of the game board rcWindow.right = rcWindow.left + pDoc->GetWidth() * pDoc->GetColumns() + nWidthDiff; rcWindow.bottom = rcWindow.top + pDoc->GetHeight() * pDoc->GetRows() + nHeightDiff; // The MoveWindow function resizes the frame window GetParentFrame()->MoveWindow(&rcWindow); }
It is very simple to draw the game board, we are just going to loop through each row, column by column, and draw a colored rectangle. There is one argument to the OnDraw function and that is a CDC pointer. The CDC class is the base class for all device contexts. A device context is the generic interface to a device such as the screen or a printer. Here we'll use it to draw to the screen.
We first start the function by getting a pointer to the document so that we can get the board information. Next we call the SaveDC function from the device context. This function saves the state of the device context so that we can restore it after we are done.
// Get the client rectangle CRect rcClient; GetClientRect(&rcClient); // Get the background color of the board COLORREF clr = pDoc->GetBoardSpace(-1, -1); // Draw the background first pDC->FillSolidRect(&rcClient, clr);
Next we need to color the background of the client area black so we get the dimensions of the client area by calling GetClientRect. A call to GetBoardSpace(-1,-1) on the document will return the background color and FillSolidRect will fill the client area with that background color.
// Create the brush for drawing CBrush br; br.CreateStockObject(HOLLOW_BRUSH); CBrush* pbrOld = pDC->SelectObject(&br); ... // Restore the device context settings pDC->RestoreDC(nDCSave); br.DeleteObject();
Now it is time to draw the individual rectangles. This is accomplished by drawing a colored rectangle and then drawing a black outline around it. We now have to create a brush object to do the outline. The type of brush we are creating, HOLLOW_BRUSH, is called hollow because when we draw a rectangle MFC will want to fill in the middle with some pattern. We don't want this so we'll use a hollow brush so that the previously drawn colored rectangle will show through. Creating a brush allocates GDI memory that we have to later delete so that we don't leak GDI resources.
// Draw the squares for(int row = 0; row < pDoc->GetRows(); row++) { for(int col = 0; col < pDoc->GetColumns(); col++) { // Get the color for this board space clr = pDoc->GetBoardSpace(row, col); // Calculate the size and position of this space CRect rcBlock; rcBlock.top = row * pDoc->GetHeight(); rcBlock.left = col * pDoc->GetWidth(); rcBlock.right = rcBlock.left + pDoc->GetWidth(); rcBlock.bottom = rcBlock.top + pDoc->GetHeight(); // Fill in the block with the correct color pDC->FillSolidRect(&rcBlock, clr); // Draw the block outline pDC->Rectangle(&rcBlock); } }
The nested for loops are very simple, they iterate row by row, column by column, getting the color of the corresponding board space from the document using the GetBoardSpace function from the document, calculating the size of the rectangle to color and then drawing the block. The drawing uses two functions, FillSolidRect() to fill in the colored portion of the block and Rectangle() to draw the outline of the block. This is what draws all of the blocks in the client area of our view.
The last function that we've inserted into the view is one to resize the window based on the dimensions of the game board. In later articles we'll allow the user to change the number of blocks and size of the blocks so this function will come in handy then too. Again we start by getting a pointer to the document followed by getting the size of the current client area and the current window.
// Get the size of the client area and the window CRect rcClient, rcWindow; GetClientRect(&rcClient); GetParentFrame()->GetWindowRect(&rcWindow);
Finding the difference between these two gives us the amount of space used by the title bar, menu and borders of the window. We can then add the differences back onto the size of the desired client area (# of blocks by # of pixels per block) to get the new window size.
// Calculate the difference int nWidthDiff = rcWindow.Width() - rcClient.Width(); int nHeightDiff = rcWindow.Height() - rcClient.Height(); // Change the window size based on the size of the game board rcWindow.right = rcWindow.left + pDoc->GetWidth() * pDoc->GetColumns() + nWidthDiff; rcWindow.bottom = rcWindow.top + pDoc->GetHeight() * pDoc->GetRows() + nHeightDiff; // The MoveWindow function resizes the frame window
Finally the GetParentFrame function returns a pointer to the CMainFrame class that is the actual window for our game and we resize the window by calling MoveWindow.
GetParentFrame()->MoveWindow(&rcWindow);
Your game should now look similar to this:
In this article we've gone over some of the basics of the Microsoft Foundation Classes and the Document/View architecture that it provides. We've assembled a game board object that contains our game data and constructed a view that renders the data to the user. In our next article we'll go over event driven programming, respond to events from the user such as mouse clicks and finish up with a "playable" version of our 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, 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.
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!
The algorithm for deleting blocks is rather simple, start with the row and column and make sure there is an adjacent block with the same color. If so, change the color value to the background color. Then go in each direction and delete the adjacent block if it is the same. This process is repeated with each block recursively. Here is the DeleteBlocks function in its entirety.
int CSameGameBoard::DeleteBlocks(int row, int col) { // Make sure that the row and column are valid if(row < 0 || row >= m_nRows || col %lt; 0 || col >= m_nColumns) return -1; // Can't delete background blocks int nColor = m_arrBoard[row][col]; if(nColor == 0) return -1; // First check if there are any of the adjacent sides // with the same color int nCount = -1; if((row - 1 >= 0 && m_arrBoard[row - 1][col] == nColor) || (row + 1 < m_nRows && m_arrBoard[row + 1][col] == nColor) || (col - 1 >= 0 && m_arrBoard[row][col - 1] == nColor) || (col + 1 < m_nColumns && m_arrBoard[row][col + 1] == nColor)) { // Then call the recursive function to eliminate all // other touching blocks with same color m_arrBoard[row][col] = 0; nCount = 1; // Recursive call for up nCount += DeleteNeighborBlocks(row - 1, col, nColor, DIRECTION_DOWN); // Recursive call for down nCount += DeleteNeighborBlocks(row + 1,col, nColor, DIRECTION_UP); // Recursive call for left nCount += DeleteNeighborBlocks(row, col - 1, nColor, DIRECTION_RIGHT); // Recursive call for right nCount += DeleteNeighborBlocks(row, col + 1, nColor, DIRECTION_LEFT); // Finally compact the board CompactBoard(); // Remove the count from the number remaining m_nRemaining -= nCount; } // Return the total number of pieces deleted return nCount; }
The first sections are to ensure that the row and column are valid and that the block selected isn't already a background block. Next comes a check if there is at least one adjacent block with the same color above, below, left or right of the block. If there is then the selected block is set to the background color (0) and the count is set to one. This is followed by four calls to DeleteNeighborBlocks(). The first call passes the row above (row - 1) in the same column with the color and then DIRECTION_DOWN. We pass this in because it tells the recursive function to skip the down direction because that is where the execution came from. This just cuts off a few extra steps and speeds the process up. This could be left out and the algorithm would still work correctly, but a little less efficiently. Once all four directions are checked the board is compacted and the number of blocks that were removed is subtracted from the total number of blocks remaining.
The DeleteNeighborBlocks function is very similar to the DeleteBlocks function. Again the first few lines make sure that the row and column are valid and that the specified block is the same color as the original block. After that we make three recursive calls to delete neighbors. We use the direction argument to decide which direction we came from and then skip this direction. This is just a little optimization that eliminates a frivolous recursive call.
int CSameGameBoard::DeleteNeighborBlocks(int row, int col, int color, Direction direction) { // Check if it is on the board if(row < 0 || row >= m_nRows || col %lt; 0 || col >= m_nColumns) return 0; // Check if it has the same color if(m_arrBoard[row][col] != color) return 0; int nCount = 1; m_arrBoard[row][col] = 0; // If we weren't told to not go back up, check up if(direction != DIRECTION_UP) nCount += DeleteNeighborBlocks(row - 1, col, color, DIRECTION_DOWN); // If we weren't told to not go back down, check down if(direction != DIRECTION_DOWN) nCount += DeleteNeighborBlocks(row + 1, col, color, DIRECTION_UP); // If we weren't told to not go back left, check left if(direction != DIRECTION_LEFT) nCount += DeleteNeighborBlocks(row, col - 1, color, DIRECTION_RIGHT); // If we weren't told to not go back right, check right if(direction != DIRECTION_RIGHT) nCount += DeleteNeighborBlocks(row, col + 1, color, DIRECTION_LEFT); // Return the total number of pieces deleted return nCount; }
At this point the adjacent, same colored blocks have been eliminated and changed to the background color so all that is left is to compact the board by moving all of the blocks down and the columns to the left.
void CSameGameBoard::CompactBoard(void) { // First move everything down for(int col = 0; col < m_nColumns; col++) { int nNextEmptyRow = m_nRows - 1; int nNextOccupiedRow = nNextEmptyRow; while(nNextOccupiedRow >= 0 && nNextEmptyRow >= 0) { // First find the next empty row while(nNextEmptyRow >= 0 && m_arrBoard[nNextEmptyRow][col] != 0) nNextEmptyRow--; if(nNextEmptyRow >= 0) { // Then find the next occupied row from the next empty row nNextOccupiedRow = nNextEmptyRow - 1; while(nNextOccupiedRow >= 0 && m_arrBoard[nNextOccupiedRow][col] == 0) nNextOccupiedRow--; if(nNextOccupiedRow >= 0) { // Now move the block from occupied to empty m_arrBoard[nNextEmptyRow][col] = m_arrBoard[nNextOccupiedRow][col]; m_arrBoard[nNextOccupiedRow][col] = 0; } } } } // Then move everything from right to left int nNextEmptyCol = 0; int nNextOccupiedCol = nNextEmptyCol; while(nNextEmptyCol < m_nColumns && nNextOccupiedCol %lt; m_nColumns) { // First find the next empty column while(nNextEmptyCol < m_nColumns && m_arrBoard[m_nRows - 1][nNextEmptyCol] != 0) nNextEmptyCol++; if(nNextEmptyCol < m_nColumns) { // Then find the next column with something in it nNextOccupiedCol = nNextEmptyCol + 1; while(nNextOccupiedCol < m_nColumns && m_arrBoard[m_nRows - 1][nNextOccupiedCol] == 0) nNextOccupiedCol++; if(nNextOccupiedCol < m_nColumns) { // Move entire column to the left for(int row = 0; row < m_nRows; row++) { m_arrBoard[row][nNextEmptyCol] = m_arrBoard[row][nNextOccupiedCol]; m_arrBoard[row][nNextOccupiedCol] = 0; } } } } }
First we go column by column moving things down. Starting at the bottom row (m_nRows - 1) we loop looking for the next empty row. Once that is found, there is another loop that searches for the next occupied row. Once these two are located then the next empty row is filled with the next occupied row. This process is repeated until there are no more blocks to move down.
The second part of the function is almost identical to the first part except for the outer for loop. The reason we can eliminate the outer loop is that we only have to look at the bottom row in each column, if it is empty then the whole column is empty and we can move something into its place.
There is only one step left to accomplish our task of creating a "playable" game. That is to implement the IsGameOver function. The function checks for a valid move which is to say it checks each block to see if there is an adjacent block with the same color. When the first one is found, the function short-circuits and returns false immediately. There is no need to continue checking. The only way to tell in the game is actually over is to actually do a full search and ensure that there aren't any moves left.
bool CSameGameBoard::IsGameOver(void) const { // Go column by column, left to right for(int col = 0; col < m_nColumns; col++) { // Row by row, bottom to top for(int row = m_nRows - 1; row >= 0; row--) { int nColor = m_arrBoard[row][col]; // Once we hit background, this column is done if(nColor == 0) break; else { // Check above and right if(row - 1 >= 0 && m_arrBoard[row - 1][col] == nColor) return false; else if(col + 1 < m_nColumns && m_arrBoard[row][col + 1] == nColor) return false; } } } // No two found adjacent return true; }
The two for loops allow us to search column by column and row by row for valid moves. Because we search left to right we don't have to check to the left for adjacent blocks of the same color. Searching from bottom to top eliminates the need to check below for valid moves. This order of searching also allows us to optimize the IsGameOver function a little further. Once the color of the block is the background color we can skip the rest of the column because anything above it will be empty also (thanks to the CompactBoard function).
You should now be able to play the full game to completion and it should look something like this:
In this article we've gone from a game that didn't do anything other than draw the game board to a playable version of the SameGame. We discussed event driven programming as well as how to implement that using MFC. We created an event handler for the left mouse click and responded with the main game algorithm. In the next article we'll be adding more features to our game through the menus.
Welcome back! With our playable game in hand, we are now ready to polish off the game. In this article we'll focus on adding customizability to the game through menus, including adding difficulty levels. To do this, we'll update the menu that was included by the MFC Application Wizard, add new commands and set up event handlers for them. After some practice, the game with only three colors is pretty easy to solve so in this article we'll add new levels of difficulty. We can do this by simply adding more colors.
Adding a new menu option is done through the resource view. You can bring up the resource view from the View Menu in Visual Studio and then, Other Menus->Resource View or the keyboard shortcut of Ctrl+Shift+E. This will bring up a window similar to what you see below.
Open up the menu in the menu editor by double-clicking on the IDR_MAINFRAME option under Menu. The menu editor will allow you to add, remove and edit menu options. Take a look at all of the menu options that are already included. Most of these options aren't useful to our particular application so we'll delete a few. Click on the file menu and it will drop down the actual menu. Delete all of the options except for "New" and "Exit" as shown in the image below. Do this by clicking on the option and hitting the delete key.
Next we'll examine the Edit menu. In the very last article we'll discuss how to create an Undo/Redo stack for the game so let's add the redo option to the edit menu and delete all of the others except for undo. Once you've deleted the unwanted options, click on the "Type Here" area in the Edit menu and type in what you see in the graphic below.
I'll explain what each part of the string "&Redo\tCtrl+Y" means. First of all the ampersand signals to the menu what the hot-key is for that menu option. The hot-key is the character immediately following the ampersand, R in this case. This will allow you to press Alt+E to open the Edit menu and then press R to select the Redo option. The \t should look familiar; it is the escape sequence for the tab character. The tab character in a menu puts a tab between the name of the menu option and the accelerator key. Look at the Undo option to see the tab in action. The accelerator key is a set of key strokes that will accomplish the same task as clicking on the menu option. We'll use the Windows standard Ctrl+Y for redo. We'll add the event handler and supporting code in a later article.
Adding a new menu is as simple as clicking on the "Type Here" at the top in the menu bar. Type in "&Level" to add the Level menu. After hitting Enter you can move the menu to the left of the Help menu by just clicking on the menu and dragging it to the left. Now let's add in the menu options. We want to allow the user to select different levels from three to seven colors. Add in the following five menu options seen below. The first one contains the text "&3 Colors" and so on.
Let's go ahead and add the menu for the next article too. So add the "&Setup" menu and move it to the left of the Help menu. Then add the "Block &Size..." and "Block &Count..." options. Notice the three periods after the option name, that is a standard for menu systems to indicate that there will be another window to popup. We'll work on that in the next article.
Now we'll focus on the Level menu and the options that we added. Once we've added the menu options we can compile and run the game and see that the menus have been added but they still don't do anything.
When you click on a menu option the program fires an event indicating which menu option was selected. We just need to catch that event with an event handler. It is very similar to the event handlers that we discussed and used in the previous article. Really the only difference is where we find the events for the menu options. We start from the properties window of the CSameGameView and click on the "Events" button (lightning bolt). Once you click on that you'll see the following.
You can see in the Menu Commands section a list of all of the menu items that we entered. The entries are in the format ID_<MENU>_<OPTION> where <MENU> is the menu name and <OPTION> is the text from the menu option. Expand the plus next to the ID_LEVEL_3COLORS option and you'll see two options under it, COMMAND and UPDATE_COMMAND_UI. The COMMAND option is the event handler for the actual event of selecting the menu option. The UPDATE_COMMAND_UI is a handy little event that allows us to change the state of the menu option. By state I mean enable/disable or check/uncheck the menu option. In our menu, we are going to put a check by the level that we've selected.
Click the down arrow in the dropdown for COMMAND and select "Add" (to get the down arrow in the first place, click anywhere on the row). We want to do this for UPDATE_COMMAND_UI also. Go ahead and do it for all of the color options (you may have to go back to the properties dialog after adding each one). Once you've done this we are ready to start adding code.
We'll start with the game board, go to the document and finish with the view. In the header file SameGameBoard.h add a new data member to keep track of the number of colors below the m_nRemaining variable.
/* Number of colors */ int m_nColors;
We also need to add two functions to get and set the number of colors. Add them to the public section right below the DeleteBlocks function.
/* Functions to get and set the number of colors */ int GetNumColors(void) { return m_nColors; } void SetNumColors(int nColors) { m_nColors = (nColors %gt;= 3 && nColors <= 7) ? nColors : m_nColors; }
The SetNumColors function restricts the value set to a number between three and seven, just like our menu options. Since we are adding more colors we need to update the m_arrColors array to make it larger (changes bolded).
/* List of colors, 0 is background and 1-7 are piece colors */ static COLORREF m_arrColors[8];
Now in the source file for the game board we need to update a few of the functions and the color array. Of course we need to update the constructor to initialize the number of colors to three (changes bolded).
COLORREF CSameGameBoard::m_arrColors[8]; CSameGameBoard::CSameGameBoard(void) : m_arrBoard(NULL), m_nColumns(15), m_nRows(15), m_nHeight(35), m_nWidth(35), m_nRemaining(0), m_nColors(3) { 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); m_arrColors[4] = RGB( 0,255, 0); m_arrColors[5] = RGB( 0,255,255); m_arrColors[6] = RGB(255, 0,128); m_arrColors[7] = RGB( 0, 64, 0); // Create and setup the board SetupBoard(); }
The only other change we need to make is in the SetupBoard. Previously, we hard-coded the number of colors to 3 in the setup. Now, we need to change the number three to m_nColors where we mod the random number like below (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() % m_nColors) + 1; // Set the number of spaces remaining m_nRemaining = m_nRows * m_nColumns; }
Now we will move on to the document. We need to add functions so that the view can change the number of colors. The GetNumColors function is a simple function that we can complete in the header file. Add the following lines of code in the public section of the document in SameGameDoc.h.
int GetNumColors() { return m_board.GetNumColors(); } void SetNumColors(int nColors);
In the source file we'll add the implementation of the SetNumColors function. Here we can't just call the SetNumColors function on the game board by itself. We need to then reset the game board to add in the new colors.
void CSameGameDoc::SetNumColors(int nColors) { // Set the number of colors first m_board.SetNumColors(nColors); // Then reset the game board m_board.SetupBoard(); }
That is all that we need in the document. Of course, since we haven't changed the view, we can't yet use these functions. The view is the last thing that needs to be updated, though. By adding the event handlers you'll get the following functions inserted into the header file.
/* Functions to change the difficulty level */ afx_msg void OnLevel3colors(); afx_msg void OnLevel4colors(); afx_msg void OnLevel5colors(); afx_msg void OnLevel6colors(); afx_msg void OnLevel7colors(); /* Functions to update the menu options */ afx_msg void OnUpdateLevel3colors(CCmdUI *pCmdUI); afx_msg void OnUpdateLevel4colors(CCmdUI *pCmdUI); afx_msg void OnUpdateLevel5colors(CCmdUI *pCmdUI); afx_msg void OnUpdateLevel6colors(CCmdUI *pCmdUI); afx_msg void OnUpdateLevel7colors(CCmdUI *pCmdUI);
You'll notice a couple of things in these prototypes that you've never seen before. The afx_msg designation specifies that the function is an event handler. The OnUpdate functions use a pointer to a CCmdUI object. We'll talk about this but it is the way that we enable/disable, check/uncheck a menu option. In the source file you'll see some other code that has been added (changes bolded).
BEGIN_MESSAGE_MAP(CSameGameView, CView) ON_WM_LBUTTONDOWN() ON_WM_ERASEBKGND() ON_COMMAND(ID_LEVEL_3COLORS, &CSameGameView::OnLevel3colors) ON_COMMAND(ID_LEVEL_4COLORS, &CSameGameView::OnLevel4colors) ON_COMMAND(ID_LEVEL_5COLORS, &CSameGameView::OnLevel5colors) ON_COMMAND(ID_LEVEL_6COLORS, &CSameGameView::OnLevel6colors) ON_COMMAND(ID_LEVEL_7COLORS, &CSameGameView::OnLevel7colors) ON_UPDATE_COMMAND_UI(ID_LEVEL_3COLORS, &CSameGameView::OnUpdateLevel3colors) ON_UPDATE_COMMAND_UI(ID_LEVEL_4COLORS, &CSameGameView::OnUpdateLevel4colors) ON_UPDATE_COMMAND_UI(ID_LEVEL_5COLORS, &CSameGameView::OnUpdateLevel5colors) ON_UPDATE_COMMAND_UI(ID_LEVEL_6COLORS, &CSameGameView::OnUpdateLevel6colors) ON_UPDATE_COMMAND_UI(ID_LEVEL_7COLORS, &CSameGameView::OnUpdateLevel7colors) END_MESSAGE_MAP()
The message map is a list of C++ macros that associate an event with an event handler. Take some time to look it over but since it is automatically generated you won't need to edit it. All of the OnLevel*colors functions are going to be all very similar to the following function.
void CSameGameView::OnLevel3colors() { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Set the number of colors pDoc-%gt;SetNumColors(3); // Force the view to redraw Invalidate(); UpdateWindow(); }
In all of the view functions we need to get a pointer to the document. Then we set the number of colors to the number in the name of the function, i.e. OnLevel3colors calls SetNumColors(3) and so on. Finally we need to force the view to redraw. Repeat this for all of the event handlers for the menu options. With all of these functions done, compile it and test it out. You'll see the number of colors change from three to four and so on. (You might also consider creating a helper function that does all of the work and takes the number of colors as an argument.) Then you can make code like this:
In the header SameGameView.h, you'd add this helper:
void setColorCount(int numColors);
In SameGameView.cpp, you'd add this code:
void CSameGameView::setColorCount(int numColors) { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Set the number of colors pDoc-%gt;SetNumColors(numColors); // Force the view to redraw Invalidate(); UpdateWindow(); } void CSameGameView::OnLevel3colors() { setColorCount(3); }
Now back to the final set of event handlers. The ON_UPDATE_COMMAND_UI event handlers are called when the menu is pulled down by the user, one for each menu option. We'll use the SetCheck function from the CCmdUI object to set and unset the check box next to the level. We start with our boiler-plate code to get a pointer to the document and then set the check based on the number of colors in the board.
void CSameGameView::OnUpdateLevel3colors(CCmdUI *pCmdUI) { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Set the check if this is the right level pCmdUI-%gt;SetCheck(pDoc-%gt;GetNumColors() == 3); }
Again, these functions are all the same except for the number that we are comparing the result of GetNumColors against (you can implement this by writing a helper function here as well, if you like). All five of these functions are called when the Level menu is displayed to set or unset the check mark. Now your game should look something like this.
In this article we've made our game a little more interesting by adding four new levels of difficulty. We learned about the menu editor and how to setup menus. We also setup the event handlers for both ON_COMMAND and ON_COMMAND_UPDATE_UI types of events for menus. In our next article we'll add new options to set the number and size of the blocks in the game board.
Our version of the SameGame is really taking shape. We have a game that can be played from start to finish in five different levels of difficulty. In this article we'll be adding more options to customize the game play. We'll add the ability for the user to adjust the size of the blocks and the number of blocks on the game board. In order to get these values from the user we'll create a dialog box to prompt them for input.
The first step is to update the game board and document classes to be able to adjust these options. Starting with the game board we'll make some very minor adjustments to the header file only. We already have "getter" functions in the game board class to get the values of the width and height of the blocks in pixels and the number of rows and columns on the board. Now we'll add "setter" functions to set these values. Update the accessor function area in the game board class in SameGameBoard.h to look like the following (changes bolded).
/* Accessor functions to get/set board size information */ int GetWidth(void) const { return m_nWidth; } void SetWidth(int nWidth) { m_nWidth = (nWidth >= 3) ? nWidth : 3; } int GetHeight(void) const { return m_nHeight; } void SetHeight(int nHeight) { m_nHeight = (nHeight >= 3) ? nHeight : 3; } int GetColumns(void) const { return m_nColumns; } void SetColumns(int nColumns) { m_nColumns = (nColumns >= 5) ? nColumns : 5; } int GetRows(void) const { return m_nRows; } void SetRows(int nRows) { m_nRows = (nRows >= 5) ? nRows : 5; }
I've added the set functions right after the get functions for easier reading and comprehension of the class. The functions are fairly simple. We use a simple test on each of the values to make sure that they don't go below a specified value, three for the pixel height/width and five for the row/column values. Numbers smaller than these will mess up the aesthetics of our game board.
Next we update the document in a similar fashion adding "setter" functions to go along with our "getters". These functions are added to the SameGameDoc.h header file just like the game board (changes bolded).
int GetWidth(void) { return m_board.GetWidth(); } void SetWidth(int nWidth) { m_board.SetWidth(nWidth); } int GetHeight(void) { return m_board.GetHeight(); } void SetHeight(int nHeight) { m_board.SetHeight(nHeight); } int GetColumns(void) { return m_board.GetColumns(); } void SetColumns(int nColumns) { m_board.SetColumns(nColumns); } int GetRows(void) { return m_board.GetRows(); } void SetRows(int nRows) { m_board.SetRows(nRows); }
Now all we have to do is prompt the user for new sizes, resize the game board, resize the window and repaint...all this will go in the event handlers for the menu options. All of these things are very trivial except for prompting the user for new sizes. We'll create a new dialog to do this. Creating a new dialog begins with displaying the resource editor window in Visual Studio. Open up the Resource View under the "View" menu "Resource View" or the accelerator key Ctrl+Shift+E. You'll recognize this view from when we edited the menu system for our game. This time instead of opening the Menu option, open the Dialog option. You'll see that there is one dialog already there with the ID of IDD_ABOUTBOX. If you've noticed under the About menu there is a dialog that comes up with "About" information. This dialog is automatically generated by the MFC Application Wizard when we first set up our project. Adding a new dialog is simple, just right-click on the Dialog option in the Resource View and select "Insert Dialog" and a new, mostly blank, dialog will appear with the ID of IDD_DIALOG1 like the image below.
Once you double-click on the IDD_DIALOG1 option, the dialog editor will appear in the main window in Visual Studio. I said earlier that the dialog was a "mostly blank" dialog because MFC generates a dialog with both an Ok button and a Cancel button. Yours should look something like this when we first begin.
We'll use this dialog to prompt the user for both board size, rows and columns, and block size, width and height, so we'll have to make it generic. This dialog won't do much for us until we add some new controls. We'll add labels, edit boxes, and another button to the dialog to make it functional. We first need to bring up the dialog editor Toolbox through the View menu then "Toolbox" or Ctrl+Alt+X. Let's take a look at the Toolbox.
This is a list of what are called Common Controls. These are Windows controls that are common to most Windows applications. Most, I'm sure, look very familiar to you. There is a button, check box, etc. You can research these controls if they don't look familiar to you. We'll be using the "Static Text" control to indicate what input the user is supposed to input to each of the "Edit Controls". We'll also add a button to allow the user to restore the default values. To add a control, simply click and drag the desired control from the Toolbox right on to the dialog in the Dialog Editor. Let's start with the button, click and drag one just below the Ok and Cancel buttons, like so.
To change the text on the button face, just click on the button so it is selected like the image above and start to type "Defaults". Now we need a couple of "Edit Controls" added. Click and drag a couple on the dialog. In order to line them up you can click on the rulers above and to the left in the Dialog Editor to create guides that snap to the controls. I've added a few to line up my edit controls with the buttons.
Finally let's add a couple of "Static Text" controls to describe to the user what to type into the Edit Controls. I'm going to add two new guides that will line the static text up with the middle of the edit controls and extend the size of the static text. These text controls will have their text inserted programmatically so that it can change depending on the type of data we want from the user.
This is how our dialog will look for both of the menu options that we'll be working with in this article, "Block Size..." and "Block Count..." All we have to do is change the title of the dialog, the static text and the values in the edit controls based on what we want to prompt the user for. In order to do this we need to make a few changes to the IDs of the controls that we've added. As they sit we are unable to interact with the Static Text controls; they need new IDs. Pull up the properties window by pressing Alt+Enter or from the menu View->Other Windows->Properties Window. When you click on a control, the properties for that control will come up. I've selected the ID option that needs to be changed.
The ID IDC_STATIC is a reserved ID that is for all generic static text controls; let's change it to something like IDC_STATIC_TEXT_1. While we're at it, let's change the IDs for all of the controls and the dialog. To do this, leave the properties window up and click on a different control. This will fill in the properties for that particular control. Then just change the IDs. We'll need the top static text to be IDC_STATIC_TEXT_1 and the other IDC_STATIC_TEXT_2. Then rename the edit controls to IDC_EDIT_VALUE_1 for the top edit control and IDC_EDIT_VALUE_2 for the other. We do this so that we can dynamically change the text depending on the data we want the user to enter. The button we added for "Defaults" we'll rename to IDC_BUTTON_DEFAULTS. Finally change the ID of the dialog from IDD_DIALOG1 to IDD_DIALOG_OPTIONS. These changes will help when creating the dialog and displaying it to the user.
Once the dialog is set up in the dialog editor, we need to produce code for it. Simple enough, just right-click on the dialog itself, not any of the controls, and select "Add Class..." You'll be presented with the MFC Class Wizard. This gives us a class that represents this dialog that we've created that we can use in our code. Add in the class name COptionsDialog and everything will fill itself out. Clicking finish will generate the OptionsDialog.h and OptionsDialog.cpp files.
Before we look at the code that has been created for us let's make some variables in this class that represent the controls that we added. Go back to the dialog editor and right-click on each of the controls, one by one, and select "Add Variable..." to bring up the Add Member Variable Wizard. This will add a variable to the class that we just created and associate it with the control that we right-clicked on. Since I first clicked on the first static text control the wizard will look like the image below. Fill in the Variable Name with m_ctrlStaticText1 like I did and click Finish. This will add all of the necessary code to the dialog class.
For now we can keep the defaults. We want a control variable of type CStatic so that we can change the text in the static text control whenever we want. We'll select a different option when we get to the edit controls. Repeat this with the second static text control giving it the variable name m_ctrlStaticText2.
Now right-click on the first edit control and select "Add Variable..." This time we want a "Control Variable" but a Value control variable so drop down the "Category" and select value. This will change the "Variable Type" options from types of MFC controls to types of values; the default is likely to be CString, but select "int". Type in the name of the variable m_nValue1. Here we are setting an integer as the storage place for the value in the edit control and when the user clicks Ok, that value will be stored in the variable that we've created without us writing any code to do so. See the screenshot below to see what I did.
Repeat this process with the second edit control and give it the name of m_nValue2.
Now we need to add an event-handler to the class for the Defaults button. Just right-click on the button and select "Add Event Handler..." This will bring up the Event Handler Wizard that will allow you to create all kinds of event handlers for all of your classes. If you right-clicked on the Defaults button the wizard will default to the BN_CLICKED, which stands for button clicked, event for the defaults button. Just click "Add and Edit" and you'll be whisked away to the options dialog code where a button event handler will be waiting for you.
The last thing that we are going to need is an override for the OnInitDialog function. In a previous article we talked about overrides so I'm going to skip the explanation. From the OptionsDialog header open up the properties window and select the Overrides button. This will bring up a list of overrides, we are looking for the OnInitDialog. Click the drop-down and Add OnInitDialog. You'll see something like the image below.
At this point there will be a lot of MFC generated code for you to look through, most of which is beyond the scope of this article. We'll cover some of it. First let's take a look at, and add to, the header file OptionsDialog.h. Here is the code (changes bolded).
#pragma once #include "afxwin.h" // COptionsDialog dialog class COptionsDialog : public CDialog { DECLARE_DYNAMIC(COptionsDialog) public: // Standard Constructor COptionsDialog(bool bRowColumn, CWnd* pParent = NULL); virtual ~COptionsDialog(); // Dialog Data enum { IDD = IDD_DIALOG_OPTIONS }; protected: // DDX/DDV support virtual void DoDataExchange(CDataExchange* pDX); DECLARE_MESSAGE_MAP() public: CStatic m_ctrlStaticText1; CStatic m_ctrlStaticText2; int m_nValue1; int m_nValue2; afx_msg void OnBnClickedButtonDefaults(); virtual BOOL OnInitDialog(); private: /* Is this dialog for row/column (true) or width/height (false)? */ bool m_bRowColumnDialog; };
We added another variable to the argument list for the constructor so that the dialog can be built for both the row/column and width/height information. If we pass in "true" then the dialog will prompt the user for number of rows and columns in the game board. When it is "false" the dialog will ask for width and height of the blocks in the game board. We implement all of this functionality in the following (changes bolded).
// OptionsDialog.cpp : implementation file #include "stdafx.h" #include "SameGame.h" #include "OptionsDialog.h" // COptionsDialog dialog IMPLEMENT_DYNAMIC(COptionsDialog, CDialog) COptionsDialog::COptionsDialog(bool bRowColumn, CWnd* pParent) : CDialog(COptionsDialog::IDD, pParent) , m_nValue1(0) , m_nValue2(0) , m_bRowColumnDialog(bRowColumn) { } COptionsDialog::~COptionsDialog() { } void COptionsDialog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Control(pDX, IDC_STATIC_TEXT_1, m_ctrlStaticText1); DDX_Control(pDX, IDC_STATIC_TEXT_2, m_ctrlStaticText2); DDX_Text(pDX, IDC_EDIT_VALUE_1, m_nValue1); DDX_Text(pDX, IDC_EDIT_VALUE_2, m_nValue2); } BEGIN_MESSAGE_MAP(COptionsDialog, CDialog) ON_BN_CLICKED(IDC_BUTTON_DEFAULTS, &COptionsDialog::OnBnClickedButtonDefaults) END_MESSAGE_MAP() // COptionsDialog message handlers void COptionsDialog::OnBnClickedButtonDefaults() { // Do things differently for the different dialogs if(m_bRowColumnDialog) m_nValue1 = m_nValue2 = 15; // 15x15 board else m_nValue1 = m_nValue2 = 35; // 35x35 blocks // Have the controls updated to the new values UpdateData(false); } BOOL COptionsDialog::OnInitDialog() { CDialog::OnInitDialog(); // Setup the dialog based on the dialog type if(m_bRowColumnDialog) { // First update the title of the dialog SetWindowText(_T("Update Block Count")); // Next change the static text labels m_ctrlStaticText1.SetWindowText(_T("Rows")); m_ctrlStaticText2.SetWindowText(_T("Columns")); } else { // First update the title of the dialog SetWindowText(_T("Update Block Size")); // Next change the static text labels m_ctrlStaticText1.SetWindowText(_T("Block Width")); m_ctrlStaticText2.SetWindowText(_T("Block Height")); } return TRUE; }
The first thing we do is update the constructor to take the Boolean value that tells us what type of dialog we are creating. This is simple. The next change we've made is to reset the values to the defaults, for rows/columns the values are both 15 and for the width/height the values are both 35. Then to update the controls with the new values we must call the UpdateData function passing in false as the argument. The argument is a Boolean flag that indicates the direction of update or what is being updated, the control (false) or the variable (true). Passing in true would reset the changes that you just made by going to the control, reading the contents, and saving it to the variable. We want to update the control with the new value of the variable so we pass in false.
Lastly we put the code into the OnInitDialog function. This is a function that is called right before the dialog is first shown to the user. This is where we can set up the dialog. So for a row/column dialog we set the title of the dialog to "Update Block Count" with the SetWindowText function and the _T() macro which we talked about in the second article. Then we update the text in the static text controls by using the function of the same name in that control object. We set them to "Rows" and "Columns". If it is a width/height dialog we change the title and labels to reflect that. That is all that needs to be done in the dialog, it should all function properly now.
Our final step is to set up a couple of event handlers in the view for the two menu options that we are working with. We do this through the Properties window from the header file for the view (SameGameView.h). Click on the events button, the one that looks like a lightning bolt, expand the ID_SETUP_BLOCKCOUNT option and add the COMMAND event handler. Do this with the ID_SETUP_BLOCKSIZE option also.
We won't need to make any changes to the view's header file other than the changes that adding those two handlers did automatically. Below are the only changes that were made automatically.
afx_msg void OnSetupBlockcount(); afx_msg void OnSetupBlocksize();
The real changes happen in the implementation or source file of the view, SameGameView.cpp. In order to use the options dialog that we just created we must include the header file in the view's source file. Here are the first few lines of that source file (changes bolded).
#include "stdafx.h" #include "SameGame.h" #include "SameGameDoc.h" #include "SameGameView.h" #include "OptionsDialog.h" #ifdef _DEBUG #define new DEBUG_NEW #endif
Just a simple include of the OptionsDialog.h file. Next we fill in the two event handlers that we just added. Both of these functions are essentially the same except for in the five boldedlocations in each function that we see below.
void CSameGameView::OnSetupBlockcount() { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Create the options dialog COptionsDialog dlg(true, this); // Set the row and column values dlg.m_nValue1 = pDoc->GetRows(); dlg.m_nValue2 = pDoc->GetColumns(); // Display the dialog if(dlg.DoModal() == IDOK) { // First delete the board pDoc->DeleteBoard(); // Get the user selected values pDoc->SetRows(dlg.m_nValue1); pDoc->SetColumns(dlg.m_nValue2); // Update the board pDoc->SetupBoard(); // Resize the view ResizeWindow(); } } void CSameGameView::OnSetupBlocksize() { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Create the options dialog COptionsDialog dlg(false, this); // Set the width and height values dlg.m_nValue1 = pDoc->GetWidth(); dlg.m_nValue2 = pDoc->GetHeight(); // Display the dialog if(dlg.DoModal() == IDOK) { // First delete the board pDoc->DeleteBoard(); // Get the user selected values pDoc->SetWidth(dlg.m_nValue1); pDoc->SetHeight(dlg.m_nValue2); // Update the board pDoc->SetupBoard(); // Resize the view ResizeWindow(); } }
Just like every other event handler that we've created, we first get a pointer to the document. Then we create the dialog by instantiating an instance of that class. Here is the first difference between the two functions. In the first function, OnSetupBlockcount, we pass in the true value, and in the second function, we pass in false. We've already discussed what this does but here is where we do it. The next two lines set the public integer values m_nValue1 and m_nValue2 to be the rows and columns for the first function and the width and height for the second. By setting these values before calling DoModal on the dialog we ensure that the values start in their corresponding edit controls. The next line is where we actually pop-up and display the dialog with the DoModal function. This shows the dialog and doesn't return control to the application until the user clicks Ok, Cancel or the X button to close. Then the function returns a value based on how the user closed the dialog. Here we are testing for Ok so we compare the return value against IDOK. If the user clicked Ok then we continue making changes. If not we ignore everything that happened and continue on.
If the user clicked Ok we first have to delete the old game board and free the memory. Once that is done we can use our "setter" functions on the document to store those values that the user selected. This is the final set of differences; we use different setters depending on the function because we are working with different values in each function. Once they are set in the document and the game board we need to create a new game board by calling SetupBoard. This will create a new game board with the new selected options. Finally we resize the window to match the new number of rows/columns or the new block size. Your game should now be able to look like these.
Our game development is nearing complete. With only one article left, we've come a very long way. In this article we created a new options dialog to be able to prompt the user for size information. We then updated the game board accordingly. These options allow the user to have many different experiences. What level can you completely clear at with a game board of five rows by five columns? There are lots of combinations of options that can change the difficulty of the game and make you change your strategy. That is what makes a game fun, begin able to replay it with different options in different ways.
This is it. We are almost done with our version of the SameGame. We've discussed quite a few topics ranging from event driven programming to GDI graphics programming. A lot of the topics we've discussed transcend the gap between game programming and application programming in general. Building MFC applications is one of such topics, not many games are written in MFC but tons of applications are (the list is very long). The topic of this final article is one of these topics. We'll discuss how to put together an undo/redo stack for our game. Undo/redo is an essential feature for most applications. If you've played our game for any period of time I'm sure you've said to yourself, "Oops! Where's the undo?!" Well here it is.
We call this feature the "undo/redo stack" because of the abstract data type (ADT) stack. A stack is a very well known data type in Computer Science theory. We'll give it a quick explanation here but for further information concerning stacks see this article. The stack is a collection of objects that is similar to a stack of plates in your kitchen; the only way you can get to the bottom plates is to first move the top ones. To add plates you just put them on top of the stack. In other words it is a "Last In, First Out" (LIFO) type of collection. This is a handy way to store your last moves. When you make a move in the game the previous state is put on the top of the undo stack so that it can be restored in the reverse order that it was made. The way we are going to do this is to keep a copy of the old game board object on the undo stack before we delete pieces from the new one. When we undo a move, the current board is put onto the redo stack and the top board from the undo stack is now the current board. The redo operation is just the opposite, put the current board on the undo stack and take the top board off of the redo stack and make that the current board.
We are going to need to make a few changes to our game board in order to make this work. We need to create a copy constructor to do a deep copy. Just add the copy constructor function prototype right between the default constructor and the destructor in SameGameBoard.h (changes bolded).
/* Default Constructor */ CSameGameBoard(void); /* Copy Constructor */ CSameGameBoard(const CSameGameBoard& board); /* Destructor */ ~CSameGameBoard(void);
We use a deep copy constructor because we have a pointer to some dynamically allocated memory. This means we can't just copy the pointer but dynamically allocate more memory and then copy the contents into that memory space (if we did just copy the pointer, then the first time a game board was freed, all of the pointers would be freed too—right out from under other instances of the class). We add a copy constructor in the source file for the game board (SameGameBoard.cpp) by adding the function implementation.
CSameGameBoard::CSameGameBoard(const CSameGameBoard& board) { // Copy all of the regular data members m_nColumns = board.m_nColumns; m_nRows = board.m_nRows; m_nHeight = board.m_nHeight; m_nWidth = board.m_nWidth; m_nRemaining = board.m_nRemaining; m_nColors = board.m_nColors; // Copy ove the colors for the board for ( int i = 0; i < 8; i++ ) m_arrColors[i] = board.m_arrColors[i]; m_arrBoard = NULL; // Create a new game board of the same size CreateBoard(); // Copy the contents of the game board for(int row = 0; row < m_nRows; row++) for(int col = 0; col < m_nColumns; col++) m_arrBoard[row][col] = board.m_arrBoard[row][col]; }
The content of the copy constructor is very simple. First we copy all of the integral type data members, i.e. all of the integers, and the colors, and then set the board pointer to NULL. This is followed by a call to the CreateBoard function. This function creates a new game board 2-D array of the same size as the original because we set up the rows and columns before the function call. Then we finish up with a couple of for loops that iterate all of the board spaces and then copy the color value into the new board. That is all that is required by the game board.
Most of the work for this feature is going to be up to the document to handle. The document is going to contain both of the stacks and create the undo/redo trail. The Standard Template Library contains a stack class that is very easy to use, you just give it a type (we'll use a pointer to a SameGameBoard) and it provides you with a few simple functions. The Push function adds a new item onto the stack while the Pop function removes the most recently pushed item. The Top function returns the item on the top of the stack and the Empty function tells whether or not the stack is empty. Here is the full source code for the document (SameGameDoc.h) with all of the changes (changes bolded).
#pragma once #include "SameGameBoard.h" #include <stack> class CSameGameDoc : public CDocument { protected: // create from serialization only CSameGameDoc(); virtual ~CSameGameDoc(); DECLARE_DYNCREATE(CSameGameDoc) // Operations public: /* Functions for accessing the game board */ COLORREF GetBoardSpace(int row, int col) { return m_board->GetBoardSpace(row, col); } void SetupBoard(void) { m_board->SetupBoard(); } int GetWidth(void) { return m_board->GetWidth(); } void SetWidth(int nWidth) { m_board->SetWidth(nWidth); } int GetHeight(void) { return m_board->GetHeight(); } void SetHeight(int nHeight) { m_board->SetHeight(nHeight); } int GetColumns(void) { return m_board->GetColumns(); } void SetColumns(int nColumns) { m_board->SetColumns(nColumns); } int GetRows(void) { return m_board->GetRows(); } void SetRows(int nRows) { m_board->SetRows(nRows); } void DeleteBoard(void) { m_board->DeleteBoard(); } bool IsGameOver() { return m_board->IsGameOver(); } /* Notice we removed the implementation of this function */ int DeleteBlocks(int row, int col); int GetRemainingCount() { return m_board->GetRemainingCount(); } int GetNumColors() { return m_board->GetNumColors(); } void SetNumColors(int nColors); /* Undo/redo functions */ void UndoLast(); bool CanUndo(); void RedoLast(); bool CanRedo(); // Overrides public: virtual BOOL OnNewDocument(); protected: /* Functions to clear undo/redo stacks */ void ClearUndo(); void ClearRedo(); /* Instance of the game board--notice that we made it a pointer */ CSameGameBoard* m_board; /* Undo stack */ std::stack<CSameGameBoard*> m_undo; /* Redo stack */ std::stack<CSameGameBoard*> m_redo; // Generated message map functions protected: DECLARE_MESSAGE_MAP() };
First of all we need to include the stack header so that we can use the stack class. Since we are going to change the m_board variable to a pointer we'll have to change from using the dot operator to the arrow or pointer operator through every function in the document. Next on the list of changes is the fact that we are moving the implementation of the DeleteBlocks function to the source file. This function has become more involved than just a single line so we are going to move it.
We then add six new functions, four are public functions and two are protected. The public functions are divided into two groups, one set of functions, UndoLast and RedoLast, actually do the undo and redo, while the second set, CanUndo and CanRedo, are simple tests we'll use for enabling and disabling the menu options when they are not available. The protected functions are simple helper functions to clear out and deallocate the associated memory from both of the stacks. Finally we add the two declarations of the undo/redo stacks.
With the change to a pointer to the game board, there are a few things that need to be added to existing functions before we add the new functions. Below is the source code for the new document in SameGameDoc.cpp (changes bolded).
#include "stdafx.h" #include "SameGame.h" #include "SameGameDoc.h" #ifdef _DEBUG #define new DEBUG_NEW #endif // CSameGameDoc IMPLEMENT_DYNCREATE(CSameGameDoc, CDocument) BEGIN_MESSAGE_MAP(CSameGameDoc, CDocument) END_MESSAGE_MAP() // CSameGameDoc construction/destruction CSameGameDoc::CSameGameDoc() { // There should always be a game board m_board = new CSameGameBoard(); } CSameGameDoc::~CSameGameDoc() { // Delete the current game board delete m_board; // Delete everything from the undo stack ClearUndo(); // Delete everything from the redo stack ClearRedo(); } BOOL CSameGameDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // Set (or reset) the game board m_board->SetupBoard(); // Clear the undo/redo stacks ClearUndo(); ClearRedo(); return TRUE; } void CSameGameDoc::SetNumColors(int nColors) { // Set the number of colors first m_board->SetNumColors(nColors); // Then reset the game board m_board->SetupBoard(); } int CSameGameDoc::DeleteBlocks(int row, int col) { // Save the current board in the undo stack m_undo.push(new CSameGameBoard(*m_board)); // Empty out the redo stack ClearRedo(); // Then delete the blocks int blocks = m_board->DeleteBlocks(row, col); // Clear the undo stack at the end of a game if(m_board->IsGameOver()) ClearUndo(); // Return the number of blocks return blocks; } void CSameGameDoc::UndoLast() { // First make sure that there is a move to undo if(m_undo.empty()) return; // Take the current board and put it on the redo m_redo.push(m_board); // Take the top undo and make it the current m_board = m_undo.top(); m_undo.pop(); } bool CSameGameDoc::CanUndo() { // Can undo if the undo stack isn't empty return !m_undo.empty(); } void CSameGameDoc::RedoLast() { // First make sure that there is a move to redo if(m_redo.empty()) return; // Take the current board and put it on the undo m_undo.push(m_board); // Take the top redo and make it the current m_board = m_redo.top(); m_redo.pop(); } bool CSameGameDoc::CanRedo() { // Can redo if the redo stack isn't empty return !m_redo.empty(); } void CSameGameDoc::ClearUndo() { // Delete everything from the undo stack while(!m_undo.empty()) { delete m_undo.top(); m_undo.pop(); } } void CSameGameDoc::ClearRedo() { // Delete everything from the redo stack while(!m_redo.empty()) { delete m_redo.top(); m_redo.pop(); } }
In the constructor we need to create a new game board. We make the assumption that there will always be a valid game board pointed to by the m_board pointer so this must be created in the constructor and then deleted in the destructor. Once it is deleted in the destructor we also have to delete all of the other game boards that have been kept around by calling the Clear functions to clear the undo and redo stacks.
Next we update the OnNewDocument function to clear the undo and redo stacks so that a new game starts out with a fresh set of stacks. The last update to this file before we get to new functions is the movement of the DeleteBlocks function from the header to the source file. The function used to be very simple, just a call to the DeleteBlocks function on the game board, but now it needs to do more. Before we delete any blocks and change the layout of the game board we need to save a copy of the current game board on the undo stack. The way we do this is by using the copy constructor that we just recently implemented. Once we make a move we need to clear the redo stack because anything that was on it is no longer valid. Once these two stacks have been updated we are then ready to proceed with the actual deleting of the blocks. Once the game is over we want to clear the undo stack because the game is over and the state is final. Clearing the stack gives the game a true finality and doesn't allow the player to go back and play the board differently once the game is over. Finally we return the number of blocks that were deleted.
The UndoLast and RedoLast are very similar to each other, they just reverse the process. First we have to make sure that there is a move to undo or redo, we could just use CanUndo or CanRedo here but I prefer to just access the empty() function on the private stack, mostly out of preference but it is a little bit more efficient also. So if there is a move to undo/redo then we take the current game board and push it onto the opposite stack, the redo stack if we are undoing and the undo stack if we are redoing. Then we set the current pointer to the top game board on the undo or redo stack and pop it off. That process accomplishes an undo or redo. The CanUndo and CanRedo functions are simple functions that give us the answer to the question, "can I undo/redo something? Or is there something on the stack?" We check this by checking if the stack is not empty.
The last two functions that we've added to the document class are used to clear and recover the memory used by the different stacks. We just loop through all of the pointers in the stack, deleting the object and then popping the pointer off of the stack. This ensures that all of the memory is deallocated for us.
At this point we are ready to make the final changes to the view. These changes are just event handlers for the menu options of undo and redo. We first create the event handlers through the events (lightning bolt) button on the Properties View from the CSameGameView.h file. We want to add both ON_COMMAND and ON_UPDATE_COMMAND_UI handlers. The ON_UPDATE_COMMAND_UI handlers will allow us to disable the menu options when there aren't moves to undo/redo. Once you add all four of the event handlers the following code will be added to the header file, I added the comments.
/* Functions for undo/redo */ afx_msg void OnEditUndo(); afx_msg void OnEditRedo(); /* Functions to update the undo/redo menu options */ afx_msg void OnUpdateEditUndo(CCmdUI *pCmdUI); afx_msg void OnUpdateEditRedo(CCmdUI *pCmdUI);
These function prototypes are just like the menu event handlers we've seen for the past two articles so I won't go into further detail. Now let's take a look at the source file. In the message map you'll find four new lines that set up the event handlers, associating events, IDs and functions. Again we've seen this before.
ON_COMMAND(ID_EDIT_UNDO, &CSameGameView::OnEditUndo) ON_COMMAND(ID_EDIT_REDO, &CSameGameView::OnEditRedo) ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, &CSameGameView::OnUpdateEditUndo) ON_UPDATE_COMMAND_UI(ID_EDIT_REDO, &CSameGameView::OnUpdateEditRedo)
The implementation of the two ON_COMMAND event handlers are fairly simple and follow a pattern that we've seen before, get a pointer to the document, call the function on the document and finally cause the view to redraw. Do this for both undo and redo.
void CSameGameView::OnEditUndo() { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Call undo on the document pDoc->UndoLast(); // Force the view to redraw Invalidate(); UpdateWindow(); } void CSameGameView::OnEditRedo() { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Call redo on the document pDoc->RedoLast(); // Force the view to redraw Invalidate(); UpdateWindow(); }
The event handlers for the ON_UPDATE_COMMAND_UI events, again, first get a pointer to the document for access to the game board. The new thing (bolded) about these functions is the use of the Enable function. This function indicates whether to enable or disable the menu option based on the result of the CanUndo or CanRedo functions.
void CSameGameView::OnUpdateEditUndo(CCmdUI *pCmdUI) { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Enable option if it is available pCmdUI->Enable(pDoc->CanUndo()); } void CSameGameView::OnUpdateEditRedo(CCmdUI *pCmdUI) { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Enable option if it is available pCmdUI->Enable(pDoc->CanRedo()); }
That is it, our undo/redo stacks are now completely functioning. Go ahead and try it out. Make a valid move and check the undo menu option. It should now be enabled. Click on it and you'll see the original game board. Check the redo menu option and it will be enabled now. Click that and you'll be back to where you were before the undo. Here is what your game should look like now.
Try pressing Ctrl+Z after you've made a few moves and you'll see that undo works with the keyboard. Now try to press Ctrl+Y for redo. Did it work? No? Well we can fix that. Remember that on the menu option for redo we indicated to the user that Ctrl+Y would send the ON_COMMAND to ID_EDIT_REDO. I mentioned that is was called an accelerator.
To access the accelerators, open up the Resource View from the View menu (under Other Windows) or press the accelerator Ctrl+Shift+E in Visual Studio. Then open up the Accelerator option under SameGame.rc and double click on IDR_MAINFRAME to bring up the Accelerator Editor. In the image below I've added an accelerator for the redo command.
To add your own, click on the blank line following the last accelerator, in the ID column; this will bring up a drop-down menu that lets you select ID_EDIT_REDO, the ID of the menu option for the redo command; give it a key of Y and a modifier of Ctrl (Ctrl+Y). Now compile your game and run it. Just like that we've added a keystroke combination that now sends the ON_COMMAND to ID_EDIT_REDO. Pretty simple to do, isn't it?
That was quite the journey! We went from nothing to a fully functioning, interesting, challenging game. We've covered lots of topics that pertain to both game development and Windows application development in general. Making games is a ton of fun and I hope that you've had fun making this one. I hope that you've seen how easy it is to make a game and will be inspired to begin working on your own. There are many more options that we could have added to this game including keeping score and keeping track of the high scores, or saving the options to the registry so that next time you play it will remember that you are playing level 7 with 10x10 blocks on a 40x40 game board, or providing a "hint" feature that suggests a possible next move. Try adding some of those options. I've given you a great place to start now it is up to you to continue. I hope you've enjoyed learning from these articles as much as I've enjoyed writing them.