win32 opengl example - 05 (MFC)

发布时间:2014-10-22 14:23:36编辑 分享查询网我要评论
本篇文章主要介绍了"win32 opengl example - 05 (MFC)",主要涉及到win32 opengl example - 05 (MFC)方面的内容,对于win32 opengl example - 05 (MFC)感兴趣的同学可以参考一下。     TetroGL: An OpenGL Game Tutorial in C++ for Win32 platforms - Part 3 By Cedric Moonen,8 Nov 2008    5.00 (26 votes) 1 2 3 4 5 5.00/5 - 26 votes 2 removed μ 4.74, σa 1.90 [?]     Is your email address OK? You are signed up for our newsletters but your email address is either unconfirmed, or has not been reconfirmed in a long time. Please clickhere to have a confirmation email sent so we can confirm your email address and start sending you newsletters again. Alternatively, you canupdate your subscriptions. Download source - 2.49 MBDownload binaries - 2.11 MB Foreword This series of articles focuses on a 2D game development withC++ and OpenGL for Windows platform. The target is to provide agame that is similar to a classic block puzzlegame by the end of the series. We will not only focus onOpenGL but also talk about the designs that are commonly used ingame programming with a full object oriented approach. You should already be familiar with theC++ 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: Part 1 : covers the window creation and the setting-up ofOpenGL. Part 2 : covers resources handling and displaying simple animations.Part 3: groups everything together and talk about the game logic. Contents IntroductionDrawing TextHandling the States of yourGame Game Example The Menu StateThe Play StateThe High-Scores State ConclusionLinksAcknowledgement Introduction 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 usingOpenGL and how to manage the different states of agame (main menu, play state, high-scores, ...). Drawing Text 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 yourgame. For example, the player name used in the high-scores, the current player score, etc. You can draw text withOpenGL by making use of display lists. A display list is a list ofOpenGL 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 ofOpenGL calls (e.g. an object made of a lot of textures) that you would like to draw often. Instead of each time repeating the sameOpenGL 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 theOpenGL 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: TheOpenGL 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 theCGameFont class, which is used to draw the text: Collapse |Copy Code // 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 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 byXPos and YPos, with the specified RGB color (fRed,fGreen and fBlue). The DrawText function should not be called beforeCreateFont, 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). TheGetTextSize 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 them_uiListBase member. Let's now look at the implementation of CreateFont: Collapse |Copy Code 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 askOpenGL 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 them_uiListBase 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 callingCreateFont (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. 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 callwglUseFontBitmaps 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 callingDrawText: Collapse |Copy Code 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 callingglRasterPos2i 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 inOpenGL. 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 callingglListBase, this tells OpenGL thatm_uiListBase is the new base for the display lists. Suppose that when we generated our font, the first available display list was at Id 500. TheglListBase command specifies that 500 is the new base for the display list, so that if you callglCallLists, 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 callingglCallLists: 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 isGL_UNSIGNED_BYTE. 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 callingglPopAttrib) 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: Collapse |Copy Code CGameFont::~CGameFont() { if (m_uiListBase) glDeleteLists(m_uiListBase,255); DeleteObject(m_hFont); } If the font was properly initialized (m_uiListBase different than 0), we delete 255 lists starting at indexm_uiListBase, which were the lists that were generated for this font. We also delete theWin32 font. So, displaying text becomes quite easy when using this little class: Collapse |Copy Code // 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 yourGame In almost every game, you will encounter different 'states': Usually you have a main menu (which allows the user to start a newgame, 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: Collapse |Copy Code // 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 ofOnKeyDown, OnKeyUp, Update and Draw, you will see that they simply call the same function on them_pActiveState instance. When switching to another state, the state manager calls theLeaveState 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: Collapse |Copy Code // 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. TheChangeState function is there only as a helper function: It simply callChangeState 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 theCGameState class. Each of these classes are implemented as singletons. 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 activegame), high scores and exit the game. The header file is: Collapse |Copy Code // 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 them_iCurrentSelection index and the SelectionChosen function switches to another state or exit thegame 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 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 theCTetrad class, which looks like: Collapse |Copy Code // 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