TetroGL:适用于Win32平台的C ++中的OpenGL游戏教程-第3部分(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/tetrogl-an-opengl-game-tutorial-in-cplusplus-for-w-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 28 分钟阅读 - 13656 个词 阅读量 0TetroGL:适用于Win32平台的C ++中的OpenGL游戏教程-第3部分(译文)
原文地址:https://www.codeproject.com/Articles/30775/TetroGL-An-OpenGL-Game-Tutorial-in-Cplusplus-for-W
原文作者:Cedric Moonen
译文由本站 robot-v1.0 翻译
前言
Learn how to draw text and handle the states of your game.
了解如何绘制文本和处理游戏状态.
前言(Foreword)
本系列文章重点介绍使用C ++和OpenGL for Windows平台进行2D游戏开发.目标是在系列结束时提供类似于经典积木益智游戏的游戏.我们将不仅关注OpenGL,还将讨论使用完整的面向对象方法在游戏编程中常用的设计.为了充分利用本系列,您应该已经熟悉C ++语言.如果您有任何问题,评论或建议,可以在文章底部使用留言板.(This 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:)
- 第1部分(Part 1) :介绍了窗口的创建和OpenGL的设置.(: covers the window creation and the setting-up of OpenGL.)
- 第2部分(Part 2) :涵盖资源处理和显示简单动画的内容.(: covers resources handling and displaying simple animations.)
- 第3部分:将所有内容组合在一起,并讨论游戏逻辑.(Part 3: groups everything together and talk about the game logic.)
内容(Contents)
介绍(Introduction)
这是该系列的最后一篇文章,我们已经了解了如何创建主窗口并在其上显示图像和动画.现在,我们将看到如何在具体示例中使用它.在开始之前,我们将首先研究如何使用OpenGL绘制文本以及如何管理游戏的不同状态(主菜单,游戏状态,高分等).(This 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 Text)
对于很多游戏,仅显示从文件加载的图像是不够的:有时您希望显示一些在设计游戏时未知的文本.例如,高分数中使用的播放器名称,当前播放器得分等.您可以通过使用显示列表使用OpenGL绘制文本.显示列表是一起存储的OpenGL命令的列表,以后可以根据需要多次执行.假设您有一个非常复杂的对象,并且需要经常对其进行许多OpenGL调用(例如,由许多纹理组成的对象).您无需一次重复执行相同的OpenGL命令,而可以一次"执行"命令并将其存储在显示列表中.然后,无论何时要显示对象,都可以调用显示列表,而不用调用所有OpenGL函数.调用列表时,列表中的所有命令均按其发出的顺序执行.使用显示列表的主要优点是优化:OpenGL命令已经过评估,并且可能以更适合您的图形卡的格式存储.例如,旋转变换需要大量的计算,因为必须从此命令生成旋转矩阵.如果使用显示列表,将保存最终的旋转矩阵,这避免了每次重做复杂的计算.(For 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) CGameFont
类,用于绘制文本:(class, which is used to draw the text:)
// 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) CreateFont
函数用于为指定的字体(例如" Arial"," Times New Roman"等)创建具有特定高度和粗细(粗细指定字体粗细)的字体.成功创建字体后,您可以调用(function is used to create the font for the specified typeface (e.g. “Arial”, “Times New Roman”,…), with a specific height and weight (the weight specifies the thickness of the font). Once the font has been created successfully, you can call) DrawText
绘制包含在其中的文本(to draw the text contained in) strText
在屏幕上指定的位置(on the screen at the position specified by) XPos
和(and) YPos
,具有指定的RGB颜色((, with the specified RGB color () fRed
,(,) fGreen
和(and) fBlue
).的(). The) DrawText
该函数不应在之前调用(function should not be called before) CreateFont
,否则将引发异常.为了能够创建字体,应提供设备上下文.可以通过静态函数完成一次((, otherwise an exception will be thrown. In order to be able to create the font, a device context should be supplied. This can be done once through a static function () SetDeviceContext
):您可以在程序开始时通过调用来一次调用该函数(): You can call that function once at the start of your program by calling) CGameFont::SetDeviceContext(hDC)
.的(. The) GetTextSize
函数是一种实用程序函数,可用于检索文本的大小,然后再将其绘制在碎石上以便正确放置.该类还包含显示列表基础的索引:已经可以创建几个显示列表,并且这些列表由ID标识.当您为字体生成多个连续的显示列表时,第一个的ID将保存在(function is a utility function that can be used to retrieve the size of the text prior to draw it on the scree in order to place it correctly. The class also contains the index of the base of the display lists: several display lists can already be created, and those lists are identified by an Id. When you generate several consecutive display lists for your font, the Id of the first one is saved in the) m_uiListBase
会员.(member.)
现在让我们看一下(Let’s now look at the implementation of) CreateFont
:(:)
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);
}
我们要做的第一件事是验证是否已提供设备上下文.如果不是这种情况,则无法创建字体,因此会引发异常.然后,我们要求OpenGL生成连续的255个diplay列表集,这些列表将用于255个字符.该函数返回第一个显示列表的ID,该ID保存在(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) m_uiListBase
会员.如果函数返回,则表示OpenGL无法分配255个显示列表,因此我们抛出异常.然后,我们通过调用来创建字体(member. If the function returns, it means that OpenGL couldn’t allocate 255 display lists, so we throw an exception. We then create the font by calling) CreateFont
(这是Windows功能).我不会列出该函数的所有参数,但是我们感兴趣的是字体高度(第一个),字体粗细(第五个)和字体(最后一个).如果您有兴趣,可以看看MSDN文档((which is a Windows function). I won’t list all the parameters of the function but the ones in which we are interested is the font height (the first one), the font weight (the fifth one) and the typeface (the last one). If you are interested, you can have a look at the MSDN documentation) 这里(here) .请注意,我们将字体高度提供为负数.这样做是为了使Windows尝试使用字符高度而不是单元格高度来查找匹配的字体.如果成功创建了字体,则将其返回,否则返回NULL(在这种情况下,我们将引发异常).然后,我们将该字体选择为设备上下文中的活动字体,并存储旧字体,以便在完成后将其重新设置.然后我们打电话(. Note that we supply the font height as a negative number. This is done so that Windows will try to find a matching font using the character height instead of the cell height. If the font creation was successfull, it is returned, otherwise NULL is returned (in which case we throw an exception). We then select this font as the active one in the device context and store the old font in order to set it back when we are done. We then call) wglUseFontBitmaps
它将根据设备上下文中选定的字体为每个字符生成显示列表.第二个参数是我们要为其生成显示列表的第一个字符的索引,第三个是我们要为其生成显示列表的字符数(从该字符开始).对于我们的情况,我们想为所有255个字符生成一个显示列表.如果查看ASCII表,您会发现并非所有字符都可用:第一个可用字符从32(空格字符)开始,最后一个是127(删除字符).因此,我们可以将显示列表减少到96个列表,而不是255个,但是这里为了简化起见,这里没有这样做.生成所有显示列表后,我们在设备上下文中再次选择旧字体.(that will generate the display lists for each of the characters based on the selected font in the device context. The second argument is the index of the first character for which we want to generate a display list and the third one is the number of characters (starting from this one) that we would like to generate a display list for. In our case, we would like to generate a display list for all 255 characters. If you look at an ASCII table, you can see that not all characters are usable: The first usable character start at 32 (the space character) and the last one is 127 (the delete character). So, we could have reduced our display lists to 96 lists instead of 255, but this has not be done here to keep things simple. Once all the display lists have been generated, we select the old font again in the device context.)
创建字体后,我们可以通过调用在屏幕上绘制文本(Once the font has been created, we are able to draw text on the screen by calling) DrawText
:(:)
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);
}
我们在这里要做的第一件事是验证我们是否有一个有效的列表库(在创建字体时生成).如果不是这种情况,我们将引发异常.此后,我们禁用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) glRasterPos2i
设置当前栅格位置(用于绘制像素和位图的位置).然后,我们按下"列表位",以将当前列表库保存在OpenGL中.这样做是为了避免干扰已保存列表库的其他显示列表.然后,我们通过调用(which sets the current raster position (the position which is used to draw pixels and bitmaps). We then push the ‘list bit’ in order to save the current list base in OpenGL. This is done so that you won’t interfere with other display lists that migh have saved the list base. We then set this list base value by calling) glListBase
,这告诉OpenGL(, this tells OpenGL that) m_uiListBase
是显示列表的新基础.假设我们生成字体时,第一个可用的显示列表为ID 500.(is the new base for the display lists. Suppose that when we generated our font, the first available display list was at Id 500. The) glListBase
命令指定500为显示列表的新基数,因此,如果您调用(command specifies that 500 is the new base for the display list, so that if you call) glCallLists
,则我们将提供500美元的ID作为补偿(, an offset of 500 will be added to the Id we supply to) glCallLists
.您会在下一行代码中看到我们为什么这样做.最后,我们通过调用来绘制文本(. You’ll see at the next line of code why we do so. Finally, we draw the text by calling) glCallLists
:第一个参数是要执行的显示列表的数量,我们需要为每个要绘制的字母执行一个显示列表(因此,显示列表的数量就是字符串中的字符数).第二个参数是在第三个参数中传递的值的类型.我们传递单字节字符,所以类型是(: The first argument is the number of display lists to be executed, we need to execute one for each letter we want to draw (thus the number of display lists is the number of characters in the string). The second argument is the type of the values which are passed in the third argument. We pass single byte characters, so the type is) GL_UNSIGNED_BYTE
.第三个参数是我们要调用的列表的ID.假设第一个字符是" A",它对应于ASCII码65,则我们将使用ID 565(由于上一个示例的偏移量)来调用列表,该ID对应于ID的列表ID.字母" A".并且我们对字符串中的每个字符都执行相同的操作.每次调用显示列表都会修改当前栅格位置,并将其移至绘制字符的右侧.这就是为什么角色不会彼此堆积的原因.然后,我们将列表库重置为其先前值(通过调用(. The third argument is the Id’s of the list we want to call. Suppose that the first character is an ‘A’, which correspond to an ASCII code of 65, we will then call the list with Id 565 (because of the offset of the previous example), which correspond to the Id of the list for the letter ‘A’. And we do the same for each of the character in the string. Each call to the display list will modify the current raster position and move it to the right of the character that was drawn. That’s why the characters do not pile up on each other. We then reset the list base to its previous value (by calling) glPopAttrib
),然后重新启用2D纹理.() and we re-enable 2D texturing.)
一旦不再需要显示列表,也应该销毁它们.这是在类析构函数中完成的:(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 () m_uiListBase
不同于0),我们从索引处删除255个列表(different than 0), we delete 255 lists starting at index) m_uiListBase
,这是为此字体生成的列表.我们还将删除Win32字体.(, which were the lists that were generated for this font. We also delete the Win32 font.)
因此,使用这个小类时,显示文本变得非常容易:(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 Game)
在几乎每个游戏中,您都会遇到不同的"状态":通常,您有一个主菜单(允许用户启动新游戏,设置一些选项,查看高分),主游戏状态,高分状态等.如果您必须管理同一类中的所有内容,那么它很快就会变成一团糟:更新和绘制功能变成了一个巨大的开关,您必须在其中照顾所有可能的状态,所有变量混合在一起,这使得代码难以维护,等等.幸运的是,有一个简单的设计模式可以很好地解决问题:状态模式.原理很简单:游戏的每个状态都有自己的单独类,该类继承自通用的"状态"类.因此,在我们的示例中,我们有一个菜单类,一个播放状态类,一个高分类等等.状态管理器类跟踪游戏的当前状态,并将每个调用重定向到此活动状态(绘制,更新,按键按下等).当您必须切换到另一个状态时,您只需将新状态通知状态管理器即可.您可以在网上找到很多关于此模式的不错的文章,因此在这里我不会详细介绍.如果要详细了解此设计模式,请查看参考文献中的第一个链接.(In 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) CStateManager
类如下所示:(class which looks like:)
// 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) OnKeyDown
,(,) OnKeyUp
,(,) Update
和(and) Draw
,您会看到他们只是在(, you will see that they simply call the same function on the) m_pActiveState
实例.切换到其他状态时,状态管理器会调用(instance. When switching to another state, the state manager calls the) LeaveState
当前状态和(of the current state and the) EnterState
新状态.状态可以实现这些功能以进行特殊的初始化或在状态变为活动或不活动时清除.(of the new state. States can implement those functions to do special initialization or clean up when the state becomes active or inactive.)
的(The) CGameState
也很容易:(is very easy too:)
// 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) ChangeState
函数仅作为辅助函数存在:它只需调用(function is there only as a helper function: It simply call) ChangeState
的(of the) CStateManager
.(.)
游戏范例(Game Example)
现在,我们已经涵盖了创建游戏所需的一切.本节说明了代码的重要部分,但这里没有涉及所有细节:本文中的每一行代码都太多了,无法解释.但是,源代码的注释相当好,因此请不要犹豫,更深入地了解一下.(We 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) CGameState
类.这些类中的每一个都实现为单例.(class. Each of these classes are implemented as singletons.)
此游戏中有一些附加功能,在其典型的前代产品中不可用:组合乘法器.每次完成一条(或几条)线,玩家就有一定的时间完成另一条线,以乘以新的已完成线的得分.每次在组合时间用完之前完成新行时,乘数都会增加.如果时间用完,则将当前的倍数降低1,然后启动新的计时器.当然,乘数越高,时间越快.(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 State)
此状态显示具有以下选项的主菜单:新游戏,继续游戏(如果当前有活动游戏),高分并退出游戏.头文件是:(This 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;
};
SelectionUp
,(,) SelectionDown
要么(or) SelectionChosen
当按下向上,向下或输入键时,将调用功能.向上和向下选择功能只需更改(functions are called when the up, down or enter key is pressed. The selection up and down functions simply change the) m_iCurrentSelection
索引和(index and the) SelectionChosen
功能会根据所选菜单项切换到另一状态或退出游戏.(function switches to another state or exit the game depending of the selected menu item.)
的(The) CTextControl
是一个简单的实用程序类,它以一定的对齐方式(左,中或右)在矩形区域中显示文本.(is a simple utility class that displays text in a rectangle region with a certain alignment (left, center or right).)
游戏状态(The Play State)
这种状态是最复杂的状态,因为在那里处理了所有游戏逻辑.游戏状态将大部分逻辑委托给了(This 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) CBlocksMatrix
班级,负责比赛区的管理.有7种不同的形状(也称为四面体),根据它们的形状用字母命名:I,O,Z,S,T,L和J.这些四面体中的每一个都有一个特定的类来处理它.这样做是因为没有通用的方法来处理所有不同的四元组.例如,旋转并不总是以相同的方式进行:四边形(直线)只有两个不同的位置(垂直和水平),您不能简单地围绕一个点旋转所有像元.因此,由于这个原因,每个四元组必须分别处理.他们都继承了(class, which is responsible of the management of the playing area. There are 7 different shapes (also called tetrads), which are named by a letter depending on their shape: I, O, Z, S, T, L and J. Each of these tetrads has a specific class to handle it. It is done so because there is no generic way to handle all the different tetrads. For instance, rotations are not always done in the same way: The I tetrad (the line) has only two different position (vertical and horizontal), you don’t simply rotate all the cells around a point. So, for this reason, each tetrad has to be handled separately. They all inherits from the) CTetrad
类,如下所示:(class, which looks like:)
// 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) CBlocksMatrix
类以检查某些单元格是否空闲.这是Z四面体的旋转函数的示例:(class to check if some cells are free or not. Here is an example of the rotation function for the Z tetrad:)
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) m_Orientation
成员已更新,函数返回true.所有四足动物的其他移动或旋转功能都相似,因此我不会在此处放置所有四足动物的代码.的(member is updated and the function returns true. The other move or rotate functions of all the tetrads are similar, so I won’t put here the code for all of them. The) Draw
功能也不是很困难:(function is not very difficult neither:)
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) CBlocksMatrix
类(请记住(class (remember that the) m_iXPos
和(and) m_iYPos
成员是矩阵中的位置,而不是屏幕位置).(members are the position in the matrix and are not screen positions).)
的(The) CBlocksMatrix
负责处理与检查填充线并将其删除有关的所有逻辑.让我们先看一下类头,稍后再看一些函数的实现:(is responsible to handle all the logic related to checking filled lines and removing them. Let’s first look at the class header, we will look at the implementation of some functions later:)
// 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) ShapeLeft
,(,) ShapeRight
,(,) ShapeRotate
和(and) ShapeDown
函数简单地重定向到当前的tetrad((functions are simply redirected to the current tetrad () m_pTetrad
).的(). The) ShapeDown
该函数的作用还更多,因为如果四分体不能向下移动,则需要进行一些特殊检查:(function does a bit more, because if a tetrad cannot move down, some special checks need to be done:)
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) MoveDown
函数返回false),我们首先要求它填充其所在矩阵的单元格,然后将其删除.然后,我们检查矩阵的至少一行是否完整:(function returns false), we first ask it to fill the cells of the matrix where it is located, then delete it. We then check if at least one line of the matrix is complete: The) CheckMatrix
如果至少完成了一行,则函数返回true,如果是,它将推送填充在行中的行号.(function returns true if at least one line is completed and if that is the case, it will push the line numbers of the ones that are filled in the) m_vecLinesRemoved
向量和集(vector and set) m_bRemovingLine
真实.如果没有完成任何线条,我们尝试通过调用来创建新形状(to true. If no lines were completed, we try to create a new shape by calling) NewShape
.如果无法创建形状(因为矩阵已满),则(. If the shape cannot be created (because the matrix is full), the) m_bGameOver
标志将设置为true.(flag will be set to true.)
的(The) Update
函数仅检查当前形状是否应向下移动:(function only checks if the current shape should be moved down:)
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) m_iTetradUpdate
变量指定当前形状两次下移之间的最长时间.随级别降低(级别越高,形状下降的速度越快).不要忘记(variable specifies the maximum time between two moves down of the current shape. This decreases with the level (the higher the level, the faster the shape will go down). Don’t forget that the) m_dwLastShapeDown
变量设置为中的当前时间(variable is set to the current time in the) ShapeDown
功能(因此,如果手动移动形状,则也会进行设置).(function (so if the shape is moved manually, this is also set).)
最后,(Finally, the) Draw
函数负责在屏幕上绘制当前状态:(function takes care of drawing the current state on the screen:)
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) m_bRemovingLine
是true)仅在我们删除行时执行(完成的行将在删除前闪烁).请记住,为了显示"动画",应以某种方式保存状态以显示下一帧.这就是为什么我们必须记住这些行当前是否可见的原因((is true) is only executed when we are removing lines (the lines which are complete will blink before being removed). Remember that in order to display ‘animations’, the state should be saved in some way for the next frame to be displayed. That’s the reason why we have to remember if the lines are currently visible or not () m_bLineBlinkOn
)及其闪烁的次数(() and the number of times they have already blinked () m_iLineBlinkCount
).的(). The) IsLineRemoved
如果传递的参数行被删除,则函数返回true.闪烁结束后,(function returns true if the line passed in argument is being removed. When the blinking is finished, the) RemoveLines
函数被调用,该函数将从矩阵中删除行并清除所有内容(已删除行上方的块将向下移动).该函数的第二部分在其余时间(当未删除任何行时)执行.它仅绘制所有填充的块和当前形状.(function is called which will remove the lines from the matrix and clean everything (blocks above a removed line will be moved down). The second part of the function gets executed the rest of the time (when no lines are being removed). It simply draws all the filled blocks and the current shape.)
如您所见,还有一个(As you probably saw, there is also a) CMatrixEventsListener
类.实际上,这只是一个接口,应该由另一个类实现,以便在发生在块矩阵中的某些事件时得到通知(开始删除行,删除行,结束游戏).的(class. In fact, this is just an interface that should be implemented by another class in order to be notified about some events that occurs in the blocks matrix (starting to remove lines, lines removed, game over). The) CPlayState
类实现此接口(并将其地址传递给(class implements this interface (and its address is passed to the) CBlocksMatrix
在构建时).使用此技术是为了减少这些类之间的耦合:(when constructing it). This technique is used in order to reduce coupling between those classes: The) CBlocksMatrix
class变得独立于正在使用它的类,并且应该通知事件.的(class becomes independant of the class which is using it and which should be notified about the events. The) CPlayState
看起来像:(looks like:)
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) CComboControl
类处理组合控件:完成一行后,此控件显示递减的时间条.如果新线在时间结束之前完成,则将乘数应用于为完成线获得的点.乘数越高,最快的时间将减少.(class handles the combo control: This control shows a decreasing time bar when a line has been completed. If a new line is completed before time is over, a multiplier is applied to the points gained for the line(s) completed. The higher the multiplier, the fastest time will decrease.)
游戏结束后,半透明的黑色矩形将显示在全屏上,并带有一些文本.这是在混合的帮助下完成的:混合支持已添加到(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) CMainWindow::InitGL
功能:(function:)
// Specifies the blending function
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Enable blending
glEnable(GL_BLEND);
的(The) glEnable(GL_BLEND)
只需启用混合功能,但还必须指定一个混合功能.混合功能告诉OpenGL如何将输入像素与帧缓冲区中存在的像素混合.指定此操作是通过(simply enables the blending, but you also have to specify a blending function. The blending function tells OpenGL how to blend the incoming pixels with the pixels present in the frame buffer. Specifying this is done through the) glBlendFunc
:第一个参数指定要应用于源像素(传入像素)的RGB分量的因子,第二个参数指定要应用于目标像素(已在帧缓冲区中的像素)的因子.最终的像素将是结果值的加法(对于每个分量).我们用来制作半透明黑屏的代码是:(: The first argument specifies the factor to apply to the RGB components of the source pixels (the incoming pixels) and the second argument specifies the factor to apply to the destination pixels (the pixels which are already in the frame buffer). The final pixel will be the addition of the resulting values (for each components). The code we use to make the semi-transparent black screen is:)
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);
}
这意味着我们首先选择一个alpha通道为0.5的黑色,然后在整个屏幕上绘制矩形.绘制矩形时,我们必须禁用纹理化,因为我们想要一个非纹理黑色矩形.(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 State)
此状态负责显示以前游戏中保存的高分.该信息保存在文件中以保持持久性(" HighScores.txt").该文件不受保护,因此任何人都可以编辑此文件并更改高分.这当然不是很好,但是描述保护此数据的方法不在本文讨论范围之内.和往常一样,在介绍一些细节之前,我将首先显示类声明:(This 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) EnterState
函数,用于从文件中读取高分,并检查是否应在表中添加新的高分:(function, it is used to read the high-scores from the file and check if a new high-score should be added in the table:)
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) std::sort
功能.为此,我们应提供(function. For that purpose, we should provide an) operator<
对于我们的结构.的(for our structure. The) std::sort
函数按升序对元素进行排序,这就是我们的运算符返回与除外元素相反的结果的原因(因此,所有元素都按相反的顺序排序).当用户插入字符时,(function sorts the elements in ascending order, that’s the reason our operator returns the opposite of what is excepted (so that all elements are ordered in the opposite order). When characters are inserted by the user, the) OnChar
函数被调用.如果(function is called. If the) m_bEnterName
标志为true,字符将添加到(flag is true, characters will be added to the) m_pCurrentName
数组,直到用户按Enter.在这种情况下,(array, until the user presses enter. In that case, the) AddNewScore
函数称为:(function is called:)
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) SaveScores
最后调用,将新的高分保存在文件中:(is called at the end to save the new high scores in the file:)
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;
}
}
在普通模式下(不输入名称时),用户可以按Enter或Esc键退出高分状态并返回主菜单.(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.)
结论(Conclusion)
这是该系列的最后一篇文章,我们看到了如何在屏幕上绘制文本以及如何管理游戏的不同状态.然后,我们将在这三个教程中看到的所有内容用作经典积木游戏的具体示例.(This 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)
状态模式(State pattern) SFML库(SFML library) FMOD库(FMOD library) 拉克网(RakNet) 电子图形用户界面(CEGUI) 大方丹(dafont)
致谢(Acknowledgement)
我要感谢Daniel Metien和Andrew Vos在图形方面的出色工作.没有他们的工作,游戏将不会很有趣.也感谢Jeff(又名El Corazon)的耐心和与OpenGL有关的建议.(I 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.)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C++ Windows Visual-Studio OpenGL Win32 Design Dev 新闻 翻译