|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
ForewordThis series of articles focuses on a 2D game development with C++ and OpenGL for Windows platform. The target is to provide a game that is similar to a classic block puzzle game by the end of the series. We will not only focus on OpenGL but also talk about the designs that are commonly used in game programming with a full object oriented approach. You should already be familiar with the C++ language in order to get the maximum out of this series. There is a message board at the bottom of the article that you can use if you have questions, remarks or suggestions. The series is divided into three articles:
Contents
IntroductionThis is the last article in the series, we already saw how to create the main window and display images and animations on it. We will now see how we can use that on a concrete example. Before we do so, we will first look at how we can draw text using OpenGL and how to manage the different states of a game (main menu, play state, high-scores, ...). Drawing TextFor a lot of games, displaying images loaded from files is not enough: Sometimes you would like to display some text which is not known when you design your game. For example, the player name used in the high-scores, the current player score, etc. You can draw text with OpenGL by making use of display lists. A display list is a list of OpenGL commands that are stored together and which can be executed later as often as you need. Suppose that you have a very complex object for which you have a lot of OpenGL calls (e.g. an object made of a lot of textures) that you would like to draw often. Instead of each time repeating the same OpenGL commands, you can 'execute' the commands once and store them in a display list. You can then call your display list later whenever you want to display the objects instead of calling all the OpenGL functions. When the list is invoked, all the commands in the list are executed in the order in which they were issued. The major advantage of using display lists is optimization: The OpenGL commands are already evaluated and might be stored in a format that is more suitable for your graphic card. For example, a rotation transformation requires quite a lot of calculations because a rotation matrix has to be generated from this command. If you use a display list, the final rotation matrix will be saved, which avoids redoing the complex calculation each time. So, for what will those display list be useful to draw text? Well, when you create your font (with a specific typeface, height and weight), all the characters that need to be reused later can be drawn in a display list (one list for each character). You can then easily display text later by calling the different display lists of the characters you want to draw. Let's look at the header of the // Utility class used to draw text on the screen using a
// specific font.
class CGameFont
{
public:
// Default constructor
CGameFont();
// Default destructor
~CGameFont();
// Create the font with a specific height and weight.
void CreateFont(const std::string& strTypeface ,
int iFontHeight,
int iFontWeight);
// Draw text on the screen at the specified location with
// the specified colour.
void DrawText(const std::string& strText, int XPos,
int YPos, GLfloat fRed, GLfloat fGreen,
GLfloat fBlue);
// Returns the size of the text. The top and right fields
// of the returned rectangle are set to 0.
TRectanglei GetTextSize(const std::string& strText);
static void SetDeviceContext(HDC hDevContext)
{ m_hDeviceContext = hDevContext; }
private:
// The device context used to create the font.
static HDC m_hDeviceContext;
// The index of the base of the lists.
GLuint m_uiListBase;
// The win32 font
HFONT m_hFont;
};
The Let's now look at the implementation of void CGameFont::CreateFont(const std::string& strTypeface,
int iFontHeight,
int iFontWeight)
{
if (!m_hDeviceContext)
{
string strError = "Impossible to create the font: ";
strError += strTypeface;
throw CException(strError);
return;
}
// Ask openGL to generate a contiguous set of 255 display lists.
m_uiListBase = glGenLists(255);
if (m_uiListBase == 0)
{
string strError = "Impossible to create the font: ";
strError += strTypeface;
throw CException(strError);
return;
}
// Create the Windows font
m_hFont = ::CreateFont(-iFontHeight,
0,
0,
0,
iFontWeight,
FALSE,
FALSE,
FALSE,
ANSI_CHARSET,
OUT_TT_PRECIS,
CLIP_DEFAULT_PRECIS,
ANTIALIASED_QUALITY,
FF_DONTCARE|DEFAULT_PITCH,
strTypeface.c_str());
if (m_hFont == NULL)
{
m_uiListBase = 0;
string strError = "Impossible to create the font: ";
strError += strTypeface;
throw CException(strError);
return;
}
// Select the newly create font into the device context (and save the previous
// one).
HFONT hOldFont = (HFONT)SelectObject(m_hDeviceContext, m_hFont);
// Generate the font display list (for the 255 characters) starting
// at display list m_uiListBase.
wglUseFontBitmaps(m_hDeviceContext, 0, 255, m_uiListBase);
// Set the original font back in the device context
SelectObject(m_hDeviceContext, hOldFont);
}
The first thing we do there is to verify if a device context has been supplied. If that's not the case, we can't create a font so we throw an exception. We then ask OpenGL to generate a continuous set of 255 diplay lists that will be used for 255 characters. The function returns the Id of the first display list, that is saved in the Once the font has been created, we are able to draw text on the screen by calling void CGameFont::DrawText(const std::string& strText,
int XPos, int YPos,
GLfloat fRed,
GLfloat fGreen,
GLfloat fBlue)
{
if (m_uiListBase == 0)
{
throw CException("Impossible to diplay the text.");
return;
}
// Disable 2D texturing
glDisable(GL_TEXTURE_2D);
// Specify the current color
glColor3f(fRed, fGreen, fBlue);
// Specify the position of the text
glRasterPos2i(XPos, YPos);
// Push the list base value
glPushAttrib (GL_LIST_BIT);
// Set a new list base value.
glListBase(m_uiListBase);
// Call the lists to draw the text.
glCallLists((GLsizei)strText.size(), GL_UNSIGNED_BYTE,
(GLubyte *)strText.c_str());
glPopAttrib ();
// Reenable 2D texturing
glEnable(GL_TEXTURE_2D);
}
The first thing we do here is to verify if we have a valid list base (which is generated when the font is created). If that's not the case, we throw an exception. After that, we disable 2D texturing because it interferes with the text and the text color will be affected by the last texture that was applied. We then specify the text color by setting the current color, and then the position of the text by calling The display lists should also be destroyed once you don't need them anymore. This is done in the class destructor: CGameFont::~CGameFont()
{
if (m_uiListBase)
glDeleteLists(m_uiListBase,255);
DeleteObject(m_hFont);
}
If the font was properly initialized ( So, displaying text becomes quite easy when using this little class: // Done once, at the start of the program.
CGameFont::SetDeviceContext(hDC);
...
...
CGameFont newFont;
newFont.CreateFont("Arial", 30, FW_BOLD);
newFont.DrawText("Test",300,150,1.0,1.0,1.0);
Handling the States of your GameIn almost every game, you will encounter different 'states': Usually you have a main menu (which allows the user to start a new game, set some options, view the highscores), the main play state, the highscore state, etc. It quickly becomes a mess in your code if you have to manage everything in the same class: The update and draw functions becomes a gigantic switch in which you have to take care of all the possible states, all the variables are mixed together which makes the code hard to maintain, and so on. Luckily, there's an easy design pattern that elegantly solves the problem: The state pattern. The principle is quite simple: Each state of your game has its own separate class which inherits from a common 'state' class. So, in our example we have a class for the menu, a class for the play state, a class for the highscores, and so on. A state manager class keeps track of the current state of the game and redirect every call to this active state (draw, update, key down, ...). When you have to switch to another state, you simply inform the state manager of the new state. You can find quite a lot of nice articles about this pattern on the net, so I won't enter into much details here. Take a look at the first link in the references if you want to go more in details about this design pattern. In the sources, you will find a // Manages the different states of the game.
class CStateManager
{
public:
// Default constructor
CStateManager();
// Default destructor
~CStateManager();
// Switches to another active state.
void ChangeState(CGameState* pNewState)
{
if (m_pActiveState)
m_pActiveState->LeaveState();
m_pActiveState = pNewState;
m_pActiveState->EnterState();
}
// Returns the current active state.
CGameState* GetActiveState() { return m_pActiveState; }
// 'Events' function, they are simply redirected to
// the active state.
void OnKeyDown(WPARAM wKey);
void OnKeyUp(WPARAM wKey);
void Update(DWORD dwCurrentTime);
void Draw();
private:
// Active State of the game (intro, play, ...)
CGameState* m_pActiveState;
};
This class manages the current state of the game and redirects all 'events' call to it: If you look at the implementation of The // Base class for the different states
// of the game.
class CGameState
{
public:
// Constructor
CGameState(CStateManager* pManager);
// Destructor
virtual ~CGameState();
// The different 'events' functions. Child classes can
// implement the ones in which they are interested in.
virtual void OnKeyDown(WPARAM ) { }
virtual void OnKeyUp(WPARAM ) { }
virtual void OnChar(WPARAM ) { }
virtual void Update(DWORD ) { }
virtual void Draw() { }
// Functions called when the state is entered or exited
// (transition from/to another state).
virtual void EnterState() { }
virtual void ExitState() { }
protected:
// Helper function to switch to a new active state.
void ChangeState(CGameState* pNewState);
// The state manager.
CStateManager* m_pStateManager;
};
The different classes that will manage the states of the game inherit from this class. These child classes can then implement the 'event' functions in which they are interested. The Game ExampleWe have now covered everything that we need in order to create our game. This section explains important parts of the code but doesn't go into all the details here: There's a bit too much code to explain every single line of code in this article. However, the source code is fairly well commented so do not hesitate to take a deeper look. The game is divided in three states: the menu state, the play state and the high-score state. As explained earlier, each of these states are handled in their own classes, which inherit from the There is a little addition in this game that is not available in its typical predecessors: A combo multiplier. Each time one (or several) line(s) is (are) completed, the player has a certain time to complete another line to multiply the score of the new completed line (or lines). The multiplier increases each time a new line has been completed before the combo time runs out. If the time runs out, the current muliplier is decreased by one and a new timer starts. Of course, the higher the multiplier is, the faster the time decrases. The Menu StateThis state displays the main menu with the following options: new game, resume game (if there is currently an active game), high scores and exit the game. The header file is: // Specialization of the CGameState class for
// the menu state. This displays a menu in which
// the player can start a new game, continue an
// existing game, see the high-scores or exit the game.
class CMenuState : public CGameState
{
public:
~CMenuState();
void OnKeyDown(WPARAM wKey);
void Draw();
void EnterState();
static CMenuState* GetInstance(CStateManager* pManager);
protected:
CMenuState(CStateManager* pManager);
private:
// The player went up or down in
// the menu
void SelectionUp();
void SelectionDown();
// The player validated the current selection
void SelectionChosen();
CGameFont* m_pFont;
// Index of the current selected menu item
int m_iCurrentSelection;
// A pointer to the current active game (if any).
CPlayState* m_pCurrentGame;
// The background and title images
TImagePtr m_pBackgroundImg;
TImagePtr m_pTitleImg;
// The images of the menu items (normal and
// selected).
TImagePtr m_pItemBckgndNormal;
TImagePtr m_pItemBckgndSelected;
// The text controls of the different entries.
CTextControl* m_pNewGameText;
CTextControl* m_pResumeGameText;
CTextControl* m_pScoresText;
CTextControl* m_pExitText;
};
The The Play StateThis state is the most complicated one, because it is there that all the game logic is handled. The play state delegates most of the logic to the // Base class for all shapes (tetrad)
class CTetrad
{
public:
// Construct a new tetrad. The image of the block used to draw
// the tetrad is loaded depending of the tetrad color.
CTetrad(CBlocksMatrix* pParent, EBlockColor blockColor)
: m_pParentMatrix(pParent), m_iXPos(4), m_iYPos(0),
m_Orientation(Rotation0), m_pBlockImg(NULL), m_BlockColor(blockColor)
{
switch (blockColor)
{
case bcCyan:
m_pBlockImg = CImage::CreateImage("Block.PNG",
TRectanglei(0,BLOCK_HEIGHT,0,BLOCK_WIDTH));
break;
case bcBlue:
m_pBlockImg = CImage::CreateImage("Block.PNG",
TRectanglei(0,BLOCK_HEIGHT,BLOCK_WIDTH,2*BLOCK_WIDTH));
break;
case bcOrange:
m_pBlockImg = CImage::CreateImage("Block.PNG",
TRectanglei(0,BLOCK_HEIGHT,2*BLOCK_WIDTH,3*BLOCK_WIDTH));
break;
case bcYellow:
m_pBlockImg = CImage::CreateImage("Block.PNG",
TRectanglei(0,BLOCK_HEIGHT,3*BLOCK_WIDTH,4*BLOCK_WIDTH));
break;
case bcGreen:
m_pBlockImg = CImage::CreateImage("Block.PNG",
TRectanglei(0,BLOCK_HEIGHT,4*BLOCK_WIDTH,5*BLOCK_WIDTH));
break;
case bcPurple:
m_pBlockImg = CImage::CreateImage("Block.PNG",
TRectanglei(BLOCK_HEIGHT,2*BLOCK_HEIGHT,0,BLOCK_WIDTH));
break;
case bcRed:
m_pBlockImg = CImage::CreateImage("Block.PNG",
TRectanglei(BLOCK_HEIGHT,2*BLOCK_HEIGHT,BLOCK_WIDTH,2*BLOCK_WIDTH));
break;
}
}
virtual ~CTetrad() { }
// Tries to rotate the tetrad. If it can't be rotated,
// the function returns false.
virtual bool Rotate() = 0;
// Tries to move the tetrad to the left. If it can't be
// moved, the function returns false.
virtual bool MoveLeft() = 0;
// Tries to move the tetrad to the right. If it can't be
// moved, the function returns false.
virtual bool MoveRight() = 0;
// Tries to move the tetrad down. If it can't be
// moved, the function returns false.
virtual bool MoveDown() = 0;
// Ask the tetrad to fill the cells in the matrix.
// This function is called when the tetrad is positioned.
virtual void FillMatrix() = 0;
// Checks if the tetrad is at a valid position (do not
// overlap with a filled cell in the matrix). This is
// called when the tetrad is created to check for game over.
virtual bool IsValid() = 0;
// Draw the tetrad at its position in the matrix.
virtual void Draw() = 0;
// Draw the tetrad somewhere on the screen (used to
// display the next shape). The tetrad is centered
// in the rectangle.
virtual void DrawOnScreen(const TRectanglei& rect) = 0;
protected:
// The play area in which the tetrad is used
CBlocksMatrix* m_pParentMatrix;
// The position in the play area (in
// blocks).
int m_iXPos;
int m_iYPos;
enum EOrientation
{
Rotation0,
Rotation90,
Rotation180,
Rotation270,
};
// Orientation of the tetrad
EOrientation m_Orientation;
// The block image use to draw the tetrad.
TImagePtr m_pBlockImg;
// The block color.
EBlockColor m_BlockColor;
};
The child classes implement those virtual methods. They interract with the bool CTetrad_Z::Rotate()
{
bool bSuccess = false;
switch (m_Orientation)
{
case Rotation0:
case Rotation180:
if (m_pParentMatrix->IsCellFree(m_iXPos,m_iYPos-1) &&
m_pParentMatrix->IsCellFree(m_iXPos-1,m_iYPos+1) )
{
m_Orientation = Rotation90;
bSuccess = true;
}
break;
case Rotation90:
case Rotation270:
if (m_pParentMatrix->IsCellFree(m_iXPos,m_iYPos+1) &&
m_pParentMatrix->IsCellFree(m_iXPos+1,m_iYPos+1))
{
m_Orientation = Rotation0;
bSuccess = true;
}
break;
}
return bSuccess;
}
Depending of the current rotation of the tetrad, it will check if the cells that will be occupied after the rotation are free or not. If they are free, the void CTetrad_Z::Draw()
{
int screenX=0, screenY=0;
switch (m_Orientation)
{
case Rotation0:
case Rotation180:
m_pParentMatrix->GetScreenPosFromCell(m_iXPos-1,m_iYPos,screenX,screenY);
m_pBlockImg->BlitImage(screenX,screenY);
m_pParentMatrix->GetScreenPosFromCell(m_iXPos ,m_iYPos,screenX,screenY);
m_pBlockImg->BlitImage(screenX,screenY);
m_pParentMatrix->GetScreenPosFromCell(m_iXPos ,m_iYPos+1,screenX,screenY);
m_pBlockImg->BlitImage(screenX,screenY);
m_pParentMatrix->GetScreenPosFromCell(m_iXPos+1,m_iYPos+1,screenX,screenY);
m_pBlockImg->BlitImage(screenX,screenY);
break;
case Rotation90:
case Rotation270:
m_pParentMatrix->GetScreenPosFromCell(m_iXPos ,m_iYPos-1,screenX,screenY);
m_pBlockImg->BlitImage(screenX,screenY);
m_pParentMatrix->GetScreenPosFromCell(m_iXPos ,m_iYPos ,screenX,screenY);
m_pBlockImg->BlitImage(screenX,screenY);
m_pParentMatrix->GetScreenPosFromCell(m_iXPos-1,m_iYPos ,screenX,screenY);
m_pBlockImg->BlitImage(screenX,screenY);
m_pParentMatrix->GetScreenPosFromCell(m_iXPos-1,m_iYPos+1,screenX,screenY);
m_pBlockImg->BlitImage(screenX,screenY);
break;
}
}
The screen position of a cell can be retrieved from the The // Class managing the playing area (where the shapes are
// falling). It handles all the logic related to lines.
class CBlocksMatrix
{
public:
// Constructor and destructor
CBlocksMatrix(CMatrixEventsListener* pListener, int xPos, int yPos);
~CBlocksMatrix();
// Draw and update the matrix
void Draw();
void Update(DWORD dwCurrentTime);
// Reset the matrix to its initial state
void Reset();
// Move the current shape
void ShapeLeft();
void ShapeRight();
void ShapeDown();
void ShapeRotate();
// Check if the specified cell is free or not.
bool IsCellFree(int XPos, int YPos);
// Fill the specified cell with a specific block color
void FillCell(int XPos, int YPos, EBlockColor BlockColor);
// Transform a cell coordinates into screen coordinates.
void GetScreenPosFromCell(int cellXPos, int cellYPos,
int& screenXPos, int& screenYPos);
// Returns the next shape
CTetrad* GetNextShape() const { return m_pNextShape; }
// Sets/Gets the time between two update of the current
// shape (determines the speed at which it falls).
void SetTetradUpdate(int iNewUpdate) { m_iTetradUpdate = iNewUpdate; }
int GetTetradUpdate() const { return m_iTetradUpdate; }
private:
// Check if there are lines completed in the
// matrix. This returns true if at least one
// line is complete
bool CheckMatrix();
// Check if the specified line is currently being
// removed
bool IsLineRemoved(int iRow);
// Remove the lines that are complete from the
// matrix and adjust the remaining blocks.
void RemoveLines();
// Tries to create a new shape. If this is not
// possible (e.g. matrix full), m_bGameOver is
// set to true.
void NewShape();
// The screen coordinates of the top-left
// corner.
int m_iXPos;
int m_iYPos;
// The matrix of blocks which are already filled
int m_pBlocksMatrix[MATRIX_WIDTH][MATRIX_HEIGHT];
// The images of the 7 different blocks
TImagePtr m_pBlockImg[7];
// The tetrad factory
CTetradFactory m_TetradFactory;
// Current shape that the player manipulates
CTetrad* m_pTetrad;
// Next shape
CTetrad* m_pNextShape;
// The last move down of the current shape
DWORD m_dwLastShapeDown;
// Flag indicating that one or more
// lines are being removed (blinking)
bool m_bRemovingLine;
// The number of times the line being removed
// has already blinked.
int m_iLineBlinkCount;
// Specify if the the line being removed is currently
// visible or not (for blinking)
bool m_bLineBlinkOn;
// Vector containing the line numbers of the
// lines being removed.
std::vector<int> m_vecLinesRemoved;
// The event listener
CMatrixEventsListener* m_pListener;
// Time (in msec) before the tetrad is moved down
// one step.
int m_iTetradUpdate;
// Flag indicating a game over.
bool m_bGameOver;
};
The void CBlocksMatrix::ShapeDown()
{
if (m_pTetrad && !m_pTetrad->MoveDown())
{
// If the current shape can't move down,
// we ask it to fill the matrix.
m_pTetrad->FillMatrix();
// Then delete the current shape
delete m_pTetrad;
m_pTetrad = NULL;
// We then check if no lines have been completed
// and create the next shape. The m_bGameOver flag
// can be set in this NewShape function.
if (!CheckMatrix())
NewShape();
}
// Set the last update (down) of the shape to
// the current time.
m_dwLastShapeDown = GetCurrentTime();
}
If the shape cannot be moved down (the The void CBlocksMatrix::Update(DWORD dwCurrentTime)
{
if (!m_bGameOver)
{
// Check if the current shape should be moved down
if (dwCurrentTime > m_dwLastShapeDown+m_iTetradUpdate)
ShapeDown();
}
}
The Finally, the void CBlocksMatrix::Draw()
{
int iBlockX=0, iBlockY=0;
// If some lines are currently being removed,
// We shouldn't draw them all.
if (m_bRemovingLine)
{
for (int j=0; j<MATRIX_HEIGHT;j++)
{
// Don't draw the line if it is being removed and blinking off
if (IsLineRemoved(j) && !m_bLineBlinkOn)
continue;
// Else draw the line
for (int i=0; i<MATRIX_WIDTH;i++)
{
if (m_pBlocksMatrix[i][j])
{
int color = m_pBlocksMatrix[i][j]-1;
GetScreenPosFromCell(i, j, iBlockX, iBlockY);
m_pBlockImg[color]->BlitImage(iBlockX, iBlockY);
}
}
}
// Switch the blinking
if (m_bLineBlinkOn)
m_bLineBlinkOn = false;
else
m_bLineBlinkOn = true;
m_iLineBlinkCount++;
// If blink count equals 10, we stop blinking and remove
// the lines.
if (m_iLineBlinkCount == 10)
{
RemoveLines();
m_bRemovingLine = false;
m_bLineBlinkOn = false;
m_iLineBlinkCount = 0;
NewShape();
}
}
else
{
// Draw filled blocks
for (int j=0; j<MATRIX_HEIGHT;j)
{
for (int i=0; i<MATRIX_WIDTH;i++)
{
if (m_pBlocksMatrix[i][j])
{
int color = m_pBlocksMatrix[i][j]-1;
GetScreenPosFromCell(i, j, iBlockX, iBlockY);
m_pBlockImg[color]->BlitImage(iBlockX, iBlockY);
}
}
}
// Finally, draw the current shape
if (!m_bGameOver)
m_pTetrad->Draw();
}
}
The first part of the function (if As you probably saw, there is also a class CPlayState : public CGameState,
public CMatrixEventsListener
{
public:
~CPlayState();
// Implementation of specific events
void OnKeyDown(WPARAM wKey);
void Update(DWORD dwCurrentTime);
void Draw();
// Implementation of the CMatrixEventsListener class
void OnStartRemoveLines();
void OnLinesRemoved(int iLinesCount);
void OnMatrixFull();
void Reset();
bool IsGameOver() { return m_bGameOver; }
// Returns the single instance
static CPlayState* GetInstance(CStateManager* pManager);
protected:
CPlayState(CStateManager* pManager);
private:
// The blocks matrix class
CBlocksMatrix* m_pMatrix;
// The font used to draw text
CGameFont* m_pFont;
// The control in charge of the decreasing
// time for the combo score.
CComboControl* m_pComboControl;
// The text controls to display the current
// information.
CTextControl* m_pScoreControl;
CTextControl* m_pLevelControl;
CTextControl* m_pLinesControl;
// The current number of lines completed
int m_iTotalLines;
// The current level
int m_iCurrentLevel;
// The current score
int m_iCurrentScore;
bool m_bGameOver;
// The background image
TImagePtr m_pBackgroundImg;
};
The main role of this class is to coordinate the different elements: The game matrix and the combo control and to manage the score and current lines completed. The implementation of the class is fairly trivial, so I won't describe it here. The When the game is over, a semi-transparent black rectangle will be displayed over the full screen with some text on it. This is done with the help of blending: support for blending has been added in the // Specifies the blending function
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Enable blending
glEnable(GL_BLEND);
The if (m_bGameOver)
{
// In game over, we draw a semi-transparent black screen on top
// of the background. This is possible because blending has
// been enabled.
glColor4f(0.0,0.0,0.0,0.5);
// Disable 2D texturing because we want to draw a non
// textured rectangle over the screen.
glDisable(GL_TEXTURE_2D);
glBegin(GL_QUADS);
glVertex3i(0,0,0);
glVertex3i(0,600,0);
glVertex3i(800,600,0);
glVertex3i(800,0,0);
glEnd();
glEnable(GL_TEXTURE_2D);
m_pFont->DrawText("GAME OVER",340,200);
m_pFont->DrawText("Press Enter to continue",285,300);
}
This means that we first select a black color with an alpha channel of 0.5 and then we draw our rectangle over the complete screen. We have to disable texturing when drawing our rectangle, because we want a non textured black rectangle. The High-Scores StateThis state is responsible to display the high-scores which are saved from previous games. The information is saved in a file for persistency ("HighScores.txt"). The file is not protected, so anybody can edit this file and change the high-scores. This is of course not very nice but describing ways to protect this data is outside the scope of the article. As usual, I'll first show the class declaration before going into some details: // Specialization of the CGameState class for
// the high scores state. This displays the high
// scores (player name+score). When a new high
// score is available after a game, it lets the
// player enters his name.
class CHighScoreState : public CGameState
{
public:
~CHighScoreState();
// Sets a new score: if this score should be
// part of the high scores, the user will need
// to enter his name.
void SetNewHighScore(ULONG ulNewHighScore)
{ m_ulNewHighScore = ulNewHighScore; }
// Implementation of specific events
void OnKeyDown(WPARAM wKey);
void OnChar(WPARAM wChar);
void Draw();
void EnterState();
static CHighScoreState* GetInstance(CStateManager* pManager);
protected:
CHighScoreState(CStateManager* pManager);
private:
// Saves the current high scores
void SaveScores();
// Adds a new score in the high-score table and
// insert it at the correct location.
void AddNewScore(const std::string& strName, ULONG ulScore);
// High-score data: score and player name.
struct HighScoreData
{
std::string strPlayer;
ULONG ulScore;
// We have to sort in decreasing order, so the <
// operator returns the opposite.
bool operator< (const HighScoreData& other)
{
if (this->ulScore > other.ulScore)
return true;
return false;
}
};
// The new high-score, if any.
ULONG m_ulNewHighScore;
// Mode in which the user has to enter his name.
bool m_bEnterName;
// Char array containing the name currently being entered.
char m_pCurrentName[26];
// The index of the next char to be entered.
int m_iNameIndex;
CGameFont* m_pFont;
typedef std::vector<HighScoreData> THighScoreTable;
// The high-score table.
THighScoreTable m_vecHighScores;
// The background and title images.
TImagePtr m_pBackgroundImg;
TImagePtr m_pTitleImg;
// The image of the entries background
TImagePtr m_pEntriesBckgndImg;
// The 'Enter name' image and the background.
TImagePtr m_pEnterNameImg;
TImagePtr m_pEnterNameBackImg;
};
The class overrides the void CHighScoreState::EnterState()
{
// Clear the high-score table
m_vecHighScores.clear();
ifstream inputFile("HighScores.txt");
if (inputFile.fail())
{
if (m_ulNewHighScore)
m_bEnterName = true;
return;
}
// Read all entries from the file
while (!inputFile.eof())
{
HighScoreData newScore;
inputFile >> newScore.strPlayer >> newScore.ulScore;
m_vecHighScores.push_back(newScore);
}
// Sort the table
sort(m_vecHighScores.begin(), m_vecHighScores.end());
// Check if we have a new high-score that should be
// added in the table. If yes, m_bEnterName is set
// to true.
ULONG lastScore = 0;
if (m_vecHighScores.size())
lastScore = m_vecHighScores[m_vecHighScores.size()-1].ulScore;
if (m_ulNewHighScore && m_ulNewHighScore>lastScore)
m_bEnterName = true;
}
When reading the file, we sort the high-scores using the void CHighScoreState::AddNewScore(const std::string& strName, ULONG ulScore)
{
// Create a new high-score and push it into the table
HighScoreData newData;
newData.strPlayer = strName;
newData.ulScore = ulScore;
m_vecHighScores.push_back(newData);
// Sort the table
sort(m_vecHighScores.begin(), m_vecHighScores.end());
// If too much elements, remove the last one.
while (m_vecHighScores.size() > 10)
m_vecHighScores.pop_back();
SaveScores();
}
The void CHighScoreState::SaveScores()
{
// Create the file
ofstream outputFile("HighScores.txt");
if (outputFile.fail())
return;
// Write all the entries in the file.
THighScoreTable::iterator iter = m_vecHighScores.begin();
for (iter; iter != m_vecHighScores.end(); iter++)
{
outputFile << iter->strPlayer << " " << iter->ulScore;
}
}
In normal mode (when the name is not entered), the user can exit the high-score state and return to the main menu by pressing enter or escape. ConclusionThis was the last article in the series in which we saw how to draw text on the screen and how to manage the different states of a game. Everything we saw during these three tutorials was then used for a concrete example on a classic block game. Of course, this example is fairly simple because there is no sound, no advanced user interface and no network access. In a more advanced game, you'll probably want to do something like that. Take a look at the references where I have put some links to libraries that supports that. I hope you enjoyed the series. Don't hesitate to give your impressions through the message board at the bottom of the article or by rating it. Thanks. Links[1] State pattern: a good article about the state pattern design pattern.[2] SFML library: Simple and Fast Multimedia Library. A free multimedia C++ API that provides you low and high level access to graphics, input, audio, etc. [3] FMOD library: a music and sound effect library which is free for non-commercial distributions. [4] RakNet: a cross-platform C++ game networking engine. [5] CEGUI: a free library providing windowing and widgets for graphics APIs. [6] dafont: a website providing some nice free fonts (the "01 digitall" font used for the game is downloaded from there). AcknowledgementI would like to thanks Daniel Metien and Andrew Vos for their very nice work on the graphics. The game wouldn't be very enjoyable without their work :). Thanks also to Jeff (aka El Corazon) for his patience and advices related to OpenGL.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||