Game Programming

 A Simple Game from Start to Finish, Part 1 

Foreword to Same Game

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.

Starting the Same Game Project

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.

Part 2 : Creating a Playable Game

By the end of this article we will have a "playable" version of the SameGame. I have playable in quotes because we will have the game in a state the will allow the player to click to remove blocks and end the game when there are no more valid moves left. The game won't be very feature-rich but will be playable. In the remaining articles we'll add more features to increase the difficulty and allow the game to be customized a little.
As for this article we'll be looking into event driven programming and how to get our game to respond to mouse clicks. Once we can respond to clicks we'll discuss the algorithm we'll use to remove the blocks and finally, how to tell when the game is over.

Event Driven Programming

Event driven programming, if you've never done it before, is a complete paradigm change in programming. If this isn't your first encounter with event driven programming then go ahead and skip to the next section.
Up till now you've probably only written procedural programs in C++. The difference between the two types of programming paradigms is that the flow of control in event driven programming is determined by events not a predetermined set of steps. It is a reactive program. The user does something like click on a button and the program reacts to that event by executing some code. The main loop in an event driven program simply waits for an event to happen then calls the appropriate event handler and goes back to wait for another event. An event handler is a piece of code that is called each time a specific event happens.

Mouse Clicks

The MFC library is inherently event driven and therefore makes it pretty easy for us to create event handlers and respond to any event that we want. To set up event handling in MFC, Visual Studio lists all of the messages that are available to respond to. In this case messages are synonymous with events. All of the Windows messages are constants that start with WM_ followed by the message name. To respond to mouse clicks in the client area of the view there are messages for the left, right and middle mouse buttons. The event that we will use is the WM_LBUTTONDOWN. This message is sent by the MFC framework every time the user clicks the left mouse button down. All we need to do is set up an event handler to listen for this message to be sent and then respond. To add an event handler open up the Properties Window from the CSameGameView header file. Do this by pressing Alt+Enter or from the menu View->Other Windows->Properties Window. Below is what you'll see in the properties window. (If it isn't, make sure your cursor is placed within the class declaration inside the SameGameView.h file.)
In the screenshot my cursor is hovering over the "Messages" section, click on it. Look for the WM_LBUTTONDOWN option, click on it, click the dropdown as shown in the screenshot below and select "<Add> OnLButtonDown".
This will add the OnLButtonDown event handler to your view with some default code in it to call the CView implementation of the function. Here we'll add the following code to the function body (changes in bold) Note that this code won't yet compile, but we'll get to that shortly. That's OK to do—the code won't compile, but it lets us figure out what needs to be done to make this function work, without worrying yet about how to write the other functions we will rely on.
Please do wait to compile the resulting code until you've finished the article, since the changes will cascade; as we go through how to implement each of the functions we need, we'll discover we need more functions. But eventually we'll get through all of them.
void CSameGameView::OnLButtonDown(UINT nFlags, CPoint point)
{
  //  First get a pointer to the document
  CSameGameDoc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);
  if(!pDoc)
    return;
  //  Get the row and column of the block that was clicked on
  int row = point.y / pDoc->GetHeight();
  int col = point.x / pDoc->GetWidth();
  //  Delete the blocks from the document
  int count = pDoc->DeleteBlocks(row, col);
  //  Check if there were any blocks deleted
  if(count > 0)
  {
    //  Force the view to redraw
    Invalidate();
    UpdateWindow();
    //  Check if the game is over
    if(pDoc->IsGameOver())
    {
      //  Get the count remaining
      int remaining = pDoc->GetRemainingCount();
      CString message;
      message.Format(_T("No more moves left\nBlocks remaining: %d"),
        remaining);
      //  Display the results to the user
      MessageBox(message, _T("Game Over"), MB_OK | MB_ICONINFORMATION);
    }
  }
  //  Default OnLButtonDown
  CView::OnLButtonDown(nFlags, point);
}
The two arguments to the function are an integer of bit-flags which can be ignored and a CPoint object. The CPoint object contains the (x, y) coordinate of where the mouse was clicked within your view. We'll use this to figure out which block they clicked. The first few lines of code are familiar to us by now; we are just getting a valid pointer to the document. To find the row and column of the block that was clicked we use some simple integer math and divide the x coordinate by the width of a block and the y by the height.
//  Get the row and column of the block that was clicked on
  int row = point.y / pDoc->GetHeight();
  int col = point.x / pDoc->GetWidth();
Since we are using integer division the result is the exact row and column the user clicked on.
Once we have the row and column we will call a function, DeleteBlocks (we'll add it next) on the document to delete the adjacent blocks. This function will return the number of blocks that it deleted. If none are deleted then the function essentially ends. If there were blocks deleted then we need to force the view to redraw itself now that we've changed the game board. The function call Invalidate() signals to the view that the whole client area needs to be redrawn and UpdateWindow() does that redraw.
  int count = pDoc->DeleteBlocks(row, col);
  //  Check if there were any blocks deleted
  if(count > 0)
  {
    //  Force the view to redraw
    Invalidate();
    UpdateWindow();
    // ...
  }
}
Now that the board has been updated and redrawn we test if the game is over. In the section entitled "Finishing Condition" we'll go over exactly how we can tell if the game is over. For now we'll just add a call to it.
if(pDoc->IsGameOver())
{
  //  Get the count remaining
  int remaining = pDoc->GetRemainingCount();
  CString message;
  message.Format(_T("No more moves left\nBlocks remaining: %d"),
    remaining);
  //  Display the results to the user
  MessageBox(message, _T("Game Over"), MB_OK | MB_ICONINFORMATION);
}
If the game is over we get the number of blocks remaining on the board and report that to the user. We create a CString object which is MFC's string class and call its built-in format method. The format method behaves just like sprintf(). Here we use the MFC _T() macro to allow for different kinds of strings (i.e. ASCII or wide character formats). We finally call the MessageBox() function that displays a small dialog with the title "Game Over" and the message that we created using the format method. The dialog has an OK button (MB_OK) and an information icon (MB_ICONINFORMATION).
Now that this event handler is in place we need to implement the three functions on the document that we called, IsGameOver, DeleteBlocks and GetRemainingCount. These functions are just simple wrappers for the same functions on the game board. So they can just be added to the header file for the document just after the DeleteBoard function, like the following.
  bool IsGameOver()       { return m_board.IsGameOver(); }
  int DeleteBlocks(int row, int col)
  { return m_board.DeleteBlocks(row, col); }
  int GetRemainingCount()
  { return m_board.GetRemainingCount(); }
Once we have added these wrapper functions to the document it is time to modify the game board to take care of these operations. In the header file for the game board add the following public methods (again put them right below the DeleteBoard function).
  /*  Is the game over? */
  bool IsGameOver(void) const;
  /*  Get the number of blocks remaining */
  int GetRemainingCount(void) const { return m_nRemaining; }
  /*  Function to delete all adjacent blocks */
  int DeleteBlocks(int row, int col);
Two of the functions are fairly complex and will require quite a bit of code but the GetRemainingCount function simply returns the count of remaining blocks. We'll store that count a member variable called m_nRemaining. We need to add this to the game board in the private member section of the class.
  /*  Number of blocks remaining */
  int m_nRemaining;
Since we are adding another data member to our class we need to initialize it in the constructor like so (changesbolded).
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!

Part 3 :A Simple Game from Start to Finish

The View: Drawing Your Game

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:

Conclusion

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.

 Part 4 : Adjusting Board Size and Block Count


Introduction

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.

Adjusting the Size and Block Count

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 bolded locations 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.

Conclusion

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.

 Part 5 : Adding an Undo/Redo Stack and Keyboard Accelerators

Introduction

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.

Undo/Redo Stack

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 (changesbolded).
#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.

Accelerators

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?

Final Thoughts

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.

Aucun commentaire:

Enregistrer un commentaire