Render-text.html (17286B)
1 <h1 id="content-title">Render text</h1> 2 <h1 id="content-url" style='display:none;'>In-Practice/2D-Game/Render-text</h1> 3 <p> 4 In this chapter we'll be adding the final enhancements to the game by adding a life system, a win condition, and feedback in the form of rendered text. This chapter heavily builds upon the earlier introduced <a href="https://learnopengl.com/In-Practice/Text-Rendering" target="_blank">Text Rendering</a> chapter so it is highly advised to first work your way through that chapter if you haven't already. 5 </p> 6 7 <p> 8 In Breakout all text rendering code is encapsulated within a class called <fun>TextRenderer</fun> that features the initialization of the FreeType library, render configuration, and the actual render code. You can find the code of the <fun>TextRenderer</fun> class here: 9 </p> 10 11 <ul> 12 <li><strong>TextRenderer</strong>: <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/text_renderer.h" target="_blank">header</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/text_renderer.cpp" target="_blank">code</a>.</li> 13 <li><strong>Text shaders</strong>: <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/shaders/text.vs" target="_blank">vertex</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/shaders/text.frag" target="_blank">fragment</a>.</li> 14 </ul> 15 16 <p> 17 The content of the text renderer's functions is almost exactly the same as the code from the text rendering chapter. However, the code for rendering glyphs onto the screen is slightly different: 18 </p> 19 20 <pre><code> 21 void TextRenderer::RenderText(std::string text, float x, float y, float scale, glm::vec3 color) 22 { 23 [...] 24 for (c = text.begin(); c != text.end(); c++) 25 { 26 float xpos = x + ch.Bearing.x * scale; 27 float ypos = y + (this->Characters['H'].Bearing.y - ch.Bearing.y) * scale; 28 29 float w = ch.Size.x * scale; 30 float h = ch.Size.y * scale; 31 // update VBO for each character 32 float vertices[6][4] = { 33 { xpos, ypos + h, 0.0f, 1.0f }, 34 { xpos + w, ypos, 1.0f, 0.0f }, 35 { xpos, ypos, 0.0f, 0.0f }, 36 37 { xpos, ypos + h, 0.0f, 1.0f }, 38 { xpos + w, ypos + h, 1.0f, 1.0f }, 39 { xpos + w, ypos, 1.0f, 0.0f } 40 }; 41 [...] 42 } 43 } 44 </code></pre> 45 46 <p> 47 The reason for it being slightly different is that we use a different orthographic projection matrix from the one we've used in the text rendering chapter. In the text rendering chapter all <code>y</code> values ranged from bottom to top, while in the Breakout game all <code>y</code> values range from top to bottom with a <code>y</code> coordinate of <code>0.0</code> corresponding to the top edge of the screen. This means we have to slightly modify how we calculate the vertical offset. 48 </p> 49 50 <p> 51 Since we now render downwards from <fun>RenderText</fun>'s <var>y</var> parameter, we calculate the vertical offset as the distance a glyph is pushed downwards from the top of the glyph space. Looking back at the glyph metrics image from FreeType, this is indicated by the red arrow: 52 </p> 53 54 <img src="/img/in-practice/breakout/glyph_offset.png" alt="Vertical offset of a FreeType glyph from the top of its glyph space for vertically inversed orthographic projection matrix in OpenGL"/> 55 56 <p> 57 To calculate this vertical offset we need to get the top of the glyph space (the length of the black vertical arrow from the origin). Unfortunately, FreeType has no such metric for us. What we do know is that that some glyphs always touch this top edge; characters like 'H', 'T' or 'X'. So what if we calculate the length of this red vector by subtracting <code>bearingY</code> from any of these <em>top-reaching</em> glyphs by <code>bearingY</code> of the glyph in question. This way, we push the glyph down based on how far its top point differs from the top edge. 58 </p> 59 60 <pre><code> 61 float ypos = y + (this->Characters['H'].Bearing.y - ch.Bearing.y) * scale; 62 </code></pre> 63 64 <p> 65 In addition to updating the <code>ypos</code> calculation, we also switched the order of the vertices a bit to make sure all the vertices are still front facing when multiplied with the current orthographic projection matrix (as discussed in the <a href="https://learnopengl.com/Advanced-OpenGL/Face-culling" target="_blank">face culling</a> chapter). 66 </p> 67 68 <p> 69 Adding the <fun>TextRenderer</fun> to the game is easy: 70 </p> 71 72 <pre><code> 73 TextRenderer *Text; 74 75 void Game::Init() 76 { 77 [...] 78 Text = new TextRenderer(this->Width, this->Height); 79 Text->Load("fonts/ocraext.TTF", 24); 80 } 81 </code></pre> 82 83 <p> 84 The text renderer is initialized with a font called OCR A Extended that you can download from <a href="http://fontzone.net/font-details/ocr-a-extended" target="_blank">here</a>. If the font is not to your liking, feel free to use a different font. 85 </p> 86 87 <p> 88 Now that we have a text renderer, let's finish the gameplay mechanics. 89 </p> 90 91 <h2>Player lives</h2> 92 <p> 93 Instead of immediately resetting the game as soon as the ball reaches the bottom edge, we'd like to give the player a few extra chances. We do this in the form of player lives, where the player begins with an initial number of lives (say <code>3</code>) and each time the ball touches the bottom edge, the player's life total is decreased by 1. Only when the player's life total becomes <code>0</code> we reset the game. This makes it easier for the player to finish a level while also building tension. 94 </p> 95 96 <p> 97 We keep count of the lives of a player by adding it to the game class (initialized within the constructor to a value of <code>3</code>): 98 </p> 99 100 <pre><code> 101 class Game 102 { 103 [...] 104 public: 105 unsigned int Lives; 106 } 107 </code></pre> 108 109 <p> 110 We then modify the game's <fun>Update</fun> function to, instead of resetting the game, decrease the player's life total, and only reset the game once the life total reaches <code>0</code>: 111 </p> 112 113 <pre><code> 114 void Game::Update(float dt) 115 { 116 [...] 117 if (Ball->Position.y >= this->Height) // did ball reach bottom edge? 118 { 119 --this->Lives; 120 // did the player lose all his lives? : Game over 121 if (this->Lives == 0) 122 { 123 this->ResetLevel(); 124 this->State = GAME_MENU; 125 } 126 this->ResetPlayer(); 127 } 128 } 129 </code></pre> 130 131 <p> 132 As soon as the player is game over (<var>lives</var> equals <code>0</code>), we reset the level and change the game state to <var>GAME_MENU</var> which we'll get to later. 133 </p> 134 135 <p> 136 Don't forget to reset the player's life total as soon as we reset the game/level: 137 </p> 138 139 <pre><code> 140 void Game::ResetLevel() 141 { 142 [...] 143 this->Lives = 3; 144 } 145 </code></pre> 146 147 <p> 148 The player now has a working life total, but has no way of seeing how many lives he currently has while playing the game. That's where the text renderer comes in: 149 </p> 150 151 <pre><code> 152 void Game::Render() 153 { 154 if (this->State == GAME_ACTIVE) 155 { 156 [...] 157 std::stringstream ss; ss << this->Lives; 158 Text->RenderText("Lives:" + ss.str(), 5.0f, 5.0f, 1.0f); 159 } 160 } 161 </code></pre> 162 163 <p> 164 Here we convert the number of lives to a string, and display it at the top-left of the screen. It'll now look a bit like this: 165 </p> 166 167 <img src="/img/in-practice/breakout/render_text_lives.png" class="clean" alt="Rendered text with FreeType in OpenGL displaying the life total of the player"/> 168 169 <p> 170 As soon as the ball touches the bottom edge, the player's life total is decreased which is instantly visible at the top-left of the screen. 171 </p> 172 173 <h2>Level selection</h2> 174 <p> 175 Whenever the user is in the game state <var>GAME_MENU</var>, we'd like to give the player the control to select the level he'd like to play in. With either the 'w' or 's' key the player should be able to scroll through any of the levels we loaded. Whenever the player feels like the chosen level is indeed the level he'd like to play in, he can press the enter key to switch from the game's <var>GAME_MENU</var> state to the <var>GAME_ACTIVE</var> state. 176 </p> 177 178 <p> 179 Allowing the player to choose a level is not too difficult. All we have to do is increase or decrease the game class's <var>Level</var> variable based on whether he pressed 'w' or 's' respectively: 180 </p> 181 182 <pre><code> 183 if (this->State == GAME_MENU) 184 { 185 if (this->Keys[GLFW_KEY_ENTER]) 186 this->State = GAME_ACTIVE; 187 if (this->Keys[GLFW_KEY_W]) 188 this->Level = (this->Level + 1) % 4; 189 if (this->Keys[GLFW_KEY_S]) 190 { 191 if (this->Level > 0) 192 --this->Level; 193 else 194 this->Level = 3; 195 } 196 } 197 </code></pre> 198 199 <p> 200 We use the modulus operator (<code>%</code>) to make sure the <var>Level</var> variable remains within the acceptable level range (between <code>0</code> and <code>3</code>). 201 </p> 202 203 <p> 204 We also want to define what we want to render when we're in the menu state. We'd like to give the player some instructions in the form of text and also display the selected level in the background. 205 </p> 206 207 <pre><code> 208 void Game::Render() 209 { 210 if (this->State == GAME_ACTIVE || this->State == GAME_MENU) 211 { 212 [...] // Game state's rendering code 213 } 214 if (this->State == GAME_MENU) 215 { 216 Text->RenderText("Press ENTER to start", 250.0f, Height / 2, 1.0f); 217 Text->RenderText("Press W or S to select level", 245.0f, Height / 2 + 20.0f, 0.75f); 218 } 219 } 220 </code></pre> 221 222 <p> 223 Here we render the game whenever we're in either the <var>GAME_ACTIVE</var> state or the <var>GAME_MENU</var> state, and whenever we're in the <var>GAME_MENU</var> state we also render two lines of text to inform the player to select a level and/or accept his choice. Note that for this to work when launching the game you do have to set the game's state as <var>GAME_MENU</var> by default. 224 </p> 225 226 <img src="/img/in-practice/breakout/render_text_select.png" class="clean" alt="Selecting levels with FreeType rendered text in OpenGL"/> 227 228 <p> 229 It looks great, but once you try to run the code you'll probably notice that as soon as you press either the 'w' or the 's' key, the game rapidly scrolls through the levels making it difficult to select the level you want to play in. This happens because the game records the key press over frames until we release the key. This causes the <fun>ProcessInput</fun> function to process the pressed key more than once. 230 </p> 231 232 <p> 233 We can solve this issue with a little trick commonly found within GUI systems. The trick is to, not only record the keys currently pressed, but also store the keys that have been processed once, until released again. We then check (before processing) whether the key has not yet been processed, and if so, process this key after which we store this key as being processed. Once we want to process the same key again without the key having been released, we do not process the key. This probably sounds somewhat confusing, but as soon as you see it in practice it (probably) starts to make sense. 234 </p> 235 236 <p> 237 First we have to create another array of bool values to indicate which keys have been processed. We define this within the game class: 238 </p> 239 240 <pre><code> 241 class Game 242 { 243 [...] 244 public: 245 bool KeysProcessed[1024]; 246 } 247 </code></pre> 248 249 <p> 250 We then set the relevant key(s) to <code>true</code> as soon as they're processed and make sure to only process the key if it wasn't processed before (until released): 251 </p> 252 253 <pre><code> 254 void Game::ProcessInput(float dt) 255 { 256 if (this->State == GAME_MENU) 257 { 258 if (this->Keys[GLFW_KEY_ENTER] && !this->KeysProcessed[GLFW_KEY_ENTER]) 259 { 260 this->State = GAME_ACTIVE; 261 this->KeysProcessed[GLFW_KEY_ENTER] = true; 262 } 263 if (this->Keys[GLFW_KEY_W] && !this->KeysProcessed[GLFW_KEY_W]) 264 { 265 this->Level = (this->Level + 1) % 4; 266 this->KeysProcessed[GLFW_KEY_W] = true; 267 } 268 if (this->Keys[GLFW_KEY_S] && !this->KeysProcessed[GLFW_KEY_S]) 269 { 270 if (this->Level > 0) 271 --this->Level; 272 else 273 this->Level = 3; 274 this->KeysProcessed[GLFW_KEY_S] = true; 275 } 276 } 277 [...] 278 } 279 </code></pre> 280 281 <p> 282 Now as soon as the key's value in the <var>KeysProcessed</var> array has not yet been set, we process the key and set its value to <code>true</code>. Next time we reach the <code>if</code> condition of the same key, it will have been processed so we'll pretend we never pressed the button until it's released again. 283 </p> 284 285 <p> 286 Within GLFW's key callback function we then need to reset the key's processed value as soon as it's released so we can process it again the next time it's pressed: 287 </p> 288 289 <pre><code> 290 void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) 291 { 292 [...] 293 if (key >= 0 && key < 1024) 294 { 295 if (action == GLFW_PRESS) 296 Breakout.Keys[key] = true; 297 else if (action == GLFW_RELEASE) 298 { 299 Breakout.Keys[key] = false; 300 Breakout.KeysProcessed[key] = false; 301 } 302 } 303 } 304 </code></pre> 305 306 <p> 307 Launching the game gives us a neat level select screen that now precisely selects a single level per key press, no matter how long we press he key. 308 </p> 309 310 <h2>Winning</h2> 311 <p> 312 Currently the player is able to select levels, play the game, and fail in doing so to lose. It is kind of unfortunate if the player finds out after destroying all the bricks he cannot in any way win the game. So let's fix that. 313 </p> 314 315 <p> 316 The player wins when all of the non-solid blocks have been destroyed. We already created a function to check for this condition in the <fun>GameLevel</fun> class: 317 </p> 318 319 <pre><code> 320 bool GameLevel::IsCompleted() 321 { 322 for (GameObject &tile : this->Bricks) 323 if (!tile.IsSolid && !tile.Destroyed) 324 return false; 325 return true; 326 } 327 </code></pre> 328 329 <p> 330 We check all bricks in the game level and if a single non-solid brick isn't yet destroyed we return <code>false</code>. All we have to do is check for this condition in the game's <fun>Update</fun> function and as soon as it returns <code>true</code> we change the game state to <var>GAME_WIN</var>: 331 </p> 332 333 <pre><code> 334 void Game::Update(float dt) 335 { 336 [...] 337 if (this->State == GAME_ACTIVE && this->Levels[this->Level].IsCompleted()) 338 { 339 this->ResetLevel(); 340 this->ResetPlayer(); 341 Effects->Chaos = true; 342 this->State = GAME_WIN; 343 } 344 } 345 </code></pre> 346 347 <p> 348 Whenever the level is completed while the game is active, we reset the game and display a small victory message in the <var>GAME_WIN</var> state. For fun we'll also enable the chaos effect while in the <var>GAME_WIN</var> screen. In the <fun>Render</fun> function we'll congratulate the player and ask him to either restart or quit the game: 349 </p> 350 351 <pre><code> 352 void Game::Render() 353 { 354 [...] 355 if (this->State == GAME_WIN) 356 { 357 Text->RenderText( 358 "You WON!!!", 320.0, Height / 2 - 20.0, 1.0, glm::vec3(0.0, 1.0, 0.0) 359 ); 360 Text->RenderText( 361 "Press ENTER to retry or ESC to quit", 130.0, Height / 2, 1.0, glm::vec3(1.0, 1.0, 0.0) 362 ); 363 } 364 } 365 </code></pre> 366 367 <p> 368 Then we of course have to actually catch the mentioned keys: 369 </p> 370 371 <pre><code> 372 void Game::ProcessInput(float dt) 373 { 374 [...] 375 if (this->State == GAME_WIN) 376 { 377 if (this->Keys[GLFW_KEY_ENTER]) 378 { 379 this->KeysProcessed[GLFW_KEY_ENTER] = true; 380 Effects->Chaos = false; 381 this->State = GAME_MENU; 382 } 383 } 384 } 385 </code></pre> 386 387 <p> 388 If you're then good enough to actually win the game, you'd get the following image: 389 </p> 390 391 <img src="/img/in-practice/breakout/render_text_win.png" class="clean" alt="Image of winning in OpenGL Breakout with FreeType rendered text"/> 392 393 <p> 394 And that is it! The final piece of the puzzle of the Breakout game we've been actively working on. Try it out, customize it to your liking, and show it to all your family and friends! 395 </p> 396 397 <p> 398 You can find the final version of the game's code below: 399 </p> 400 401 <ul> 402 <li><strong>Game</strong>: <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/game.h" target="_blank">header</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/game.cpp" target="_blank">code</a>.</li> 403 </ul> 404 405 <h2>Further reading</h2> 406 <ul> 407 <li><a href="https://www.websiteplanet.com/blog/best-free-fonts/" target="_blank">70+ Best Free Fonts for Designers</a>: summarized list of a large group of fonts to use in your project for personal or commercial use.</li> 408 </ul> 409 410 </div> 411 412 <div id="hover"> 413 HI 414 </div> 415 <!-- 728x90/320x50 sticky footer --> 416 <div id="waldo-tag-6196"></div> 417 418 <div id="disqus_thread"></div> 419 420 421 422 423 </div> <!-- container div --> 424 425 426 </div> <!-- super container div --> 427 </body> 428 </html>