Levels.html (15889B)
1 <h1 id="content-title">Levels</h1> 2 <h1 id="content-url" style='display:none;'>In-Practice/2D-Game/Levels</h1> 3 <p> 4 Breakout is unfortunately not just about a single happy green face, but contains complete levels with a lot of playfully colored bricks. We want these levels to be configurable such that they can support any number of rows and/or columns, we want the levels to have solid bricks (that cannot be destroyed), we want the levels to support multiple brick colors, and we want them to be stored externally in (text) files. 5 </p> 6 7 <p> 8 In this chapter we'll briefly walk through the code of a game level object that is used to manage a large amount of bricks. We first have to define what an actual <def>brick</def> is though. 9 </p> 10 11 <p> 12 We create a component called a <def>game object</def> that acts as the base representation of an object inside the game. Such a game object holds state data like its position, size, and velocity. It holds a color, a rotation component, whether it is solid and/or destroyed, and it also stores a <fun>Texture2D</fun> variable as its sprite. 13 </p> 14 15 <p> 16 Each object in the game is represented as a <fun>GameObject</fun> or a derivative of this class. You can find the code of the <fun>GameObject</fun> class below: 17 </p> 18 19 <ul> 20 <li><strong>GameObject</strong>: <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/game_object.h" target="_blank">header</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/game_object.cpp" target="_blank">code</a> </li> 21 </ul> 22 23 <p> 24 A level in Breakout consists entirely of bricks so we can represent a level by exactly that: a collection of bricks. Because a brick requires the same state as a game object, we're going to represent each brick of the level as a <fun>GameObject</fun>. The declaration of the <fun>GameLevel</fun> class then looks as follows: 25 </p> 26 27 <pre><code> 28 class GameLevel 29 { 30 public: 31 // level state 32 std::vector<GameObject> Bricks; 33 // constructor 34 GameLevel() { } 35 // loads level from file 36 void Load(const char *file, unsigned int levelWidth, unsigned int levelHeight); 37 // render level 38 void Draw(SpriteRenderer &renderer); 39 // check if the level is completed (all non-solid tiles are destroyed) 40 bool IsCompleted(); 41 private: 42 // initialize level from tile data 43 void init(std::vector<std::vector<unsigned int>> tileData, 44 unsigned int levelWidth, unsigned int levelHeight); 45 }; 46 </code></pre> 47 48 <p> 49 Since a level is loaded from an external (text) file, we need to propose some kind of level structure. Here is an example of what a game level may look like in a text file: 50 </p> 51 52 <pre><code> 53 1 1 1 1 1 1 54 2 2 0 0 2 2 55 3 3 4 4 3 3 56 </code></pre> 57 58 <p> 59 A level is stored in a matrix-like structure where each number represents a type of brick, each one separated by a space. Within the level code we can then assign what each number represents. We have chosen the following representation: 60 </p> 61 62 <ul> 63 <li>A number of 0: no brick, an empty space within the level.</li> 64 <li>A number of 1: a solid brick, a brick that cannot be destroyed.</li> 65 <li>A number higher than 1: a destroyable brick; each subsequent number only differs in color.</li> 66 </ul> 67 68 <p> 69 The example level listed above would, after being processed by <fun>GameLevel</fun>, look like this: 70 </p> 71 72 <img src="/img/in-practice/breakout/levels-example.png" class="clean" alt="Example of a level using the Breakout GameLevel class"/> 73 74 <p> 75 The <fun>GameLevel</fun> class uses two functions to generate a level from file. It first loads all the numbers in a two-dimensional vector within its <fun>Load</fun> function that then processes these numbers (to create all game objects) in its <fun>init</fun> function. 76 </p> 77 78 79 <pre><code> 80 void GameLevel::Load(const char *file, unsigned int levelWidth, unsigned int levelHeight) 81 { 82 // clear old data 83 this->Bricks.clear(); 84 // load from file 85 unsigned int tileCode; 86 GameLevel level; 87 std::string line; 88 std::ifstream fstream(file); 89 std::vector<std::vector<unsigned int>> tileData; 90 if (fstream) 91 { 92 while (std::getline(fstream, line)) // read each line from level file 93 { 94 std::istringstream sstream(line); 95 std::vector<unsigned int> row; 96 while (sstream >> tileCode) // read each word separated by spaces 97 row.push_back(tileCode); 98 tileData.push_back(row); 99 } 100 if (tileData.size() > 0) 101 this->init(tileData, levelWidth, levelHeight); 102 } 103 } 104 </code></pre> 105 106 <p> 107 The loaded <var>tileData</var> is then passed to the game level's <fun>init</fun> function: 108 </p> 109 110 <pre><code> 111 void GameLevel::init(std::vector<std::vector<unsigned int>> tileData, 112 unsigned int lvlWidth, unsigned int lvlHeight) 113 { 114 // calculate dimensions 115 unsigned int height = tileData.size(); 116 unsigned int width = tileData[0].size(); 117 float unit_width = lvlWidth / static_cast<float>(width); 118 float unit_height = lvlHeight / height; 119 // initialize level tiles based on tileData 120 for (unsigned int y = 0; y < height; ++y) 121 { 122 for (unsigned int x = 0; x < width; ++x) 123 { 124 // check block type from level data (2D level array) 125 if (tileData[y][x] == 1) // solid 126 { 127 glm::vec2 pos(unit_width * x, unit_height * y); 128 glm::vec2 size(unit_width, unit_height); 129 GameObject obj(pos, size, 130 ResourceManager::GetTexture("block_solid"), 131 glm::vec3(0.8f, 0.8f, 0.7f) 132 ); 133 obj.IsSolid = true; 134 this->Bricks.push_back(obj); 135 } 136 else if (tileData[y][x] > 1) 137 { 138 glm::vec3 color = glm::vec3(1.0f); // original: white 139 if (tileData[y][x] == 2) 140 color = glm::vec3(0.2f, 0.6f, 1.0f); 141 else if (tileData[y][x] == 3) 142 color = glm::vec3(0.0f, 0.7f, 0.0f); 143 else if (tileData[y][x] == 4) 144 color = glm::vec3(0.8f, 0.8f, 0.4f); 145 else if (tileData[y][x] == 5) 146 color = glm::vec3(1.0f, 0.5f, 0.0f); 147 148 glm::vec2 pos(unit_width * x, unit_height * y); 149 glm::vec2 size(unit_width, unit_height); 150 this->Bricks.push_back( 151 GameObject(pos, size, ResourceManager::GetTexture("block"), color) 152 ); 153 } 154 } 155 } 156 } 157 </code></pre> 158 159 <p> 160 The <fun>init</fun> function iterates through each of the loaded numbers and adds a <fun>GameObject</fun> to the level's <var>Bricks</var> vector based on the processed number. The size of each brick is automatically calculated (<var>unit_width</var> and <var>unit_height</var>) based on the total number of bricks so that each brick perfectly fits within the screen bounds. 161 </p> 162 163 <p> 164 Here we load the game objects with two new textures, a <a href="/img/in-practice/breakout/textures/block.png" target="_blank">block</a> texture and a <a href="/img/in-practice/breakout/textures/block_solid.png" target="_blank">solid block</a> texture. 165 </p> 166 167 <img src="/img/in-practice/breakout/block-textures.png" alt="Image of two types of block textures"/> 168 169 <p> 170 A nice little trick here is that these textures are completely in gray-scale. The effect is that we can neatly manipulate their colors within the game-code by multiplying their grayscale colors with a defined color vector; exactly as we did within the <fun>SpriteRenderer</fun>. This way, customizing the appearance of their colors doesn't look too weird or unbalanced. 171 </p> 172 173 <p> 174 The <fun>GameLevel</fun> class also houses a few other functions, like rendering all non-destroyed bricks, or validating if all non-solid bricks are destroyed. You can find the source code of the <fun>GameLevel</fun> class below: 175 </p> 176 177 <ul> 178 <li><strong>GameLevel</strong>: <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/game_level.h" target="_blank">header</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/game_level.cpp" target="_blank">code</a> </li> 179 </ul> 180 181 <p> 182 The game level class gives us a lot of flexibility since any amount of rows and columns are supported and a user could easily create his/her own levels by modifying the level files. 183 </p> 184 185 <h2>Within the game</h2> 186 <p> 187 We would like to support multiple levels in the Breakout game so we'll have to extend the game class a little by adding a vector that holds variables of type <fun>GameLevel</fun>. We'll also store the currently active level while we're at it: 188 </p> 189 190 <pre><code> 191 class Game 192 { 193 [...] 194 std::vector<GameLevel> Levels; 195 unsigned int Level; 196 [...] 197 }; 198 </code></pre> 199 200 <p> 201 This series' version of the Breakout game features a total of 4 levels: 202 </p> 203 204 <ul> 205 <li><a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/levels/one.lvl" target="_blank">Standard</a></li> 206 <li><a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/levels/two.lvl" target="_blank">A few small gaps</a></li> 207 <li><a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/levels/three.lvl" target="_blank">Space invader</a></li> 208 <li><a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/levels/four.lvl" target="_blank">Bounce galore</a></li> 209 </ul> 210 211 <p> 212 Each of the textures and levels are then initialized within the game class's <fun>Init</fun> function: 213 </p> 214 215 <pre><code> 216 void Game::Init() 217 { 218 [...] 219 // load textures 220 ResourceManager::LoadTexture("textures/background.jpg", false, "background"); 221 ResourceManager::LoadTexture("textures/awesomeface.png", true, "face"); 222 ResourceManager::LoadTexture("textures/block.png", false, "block"); 223 ResourceManager::LoadTexture("textures/block_solid.png", false, "block_solid"); 224 // load levels 225 GameLevel one; one.Load("levels/one.lvl", this->Width, this->Height / 2); 226 GameLevel two; two.Load("levels/two.lvl", this->Width, this->Height / 2); 227 GameLevel three; three.Load("levels/three.lvl", this->Width, this->Height / 2); 228 GameLevel four; four.Load("levels/four.lvl", this->Width, this->Height / 2); 229 this->Levels.push_back(one); 230 this->Levels.push_back(two); 231 this->Levels.push_back(three); 232 this->Levels.push_back(four); 233 this->Level = 0; 234 } 235 </code></pre> 236 237 <p> 238 Now all that is left to do, is actually render the level. We accomplish this by calling the currently active level's <fun>Draw</fun> function that in turn calls each <fun>GameObject</fun>'s <fun>Draw</fun> function using the given sprite renderer. Next to the level, we'll also render the scene with a nice <a href="/img/in-practice/breakout/textures/background.jpg" target="_blank">background image</a> (courtesy of Tenha): 239 </p> 240 241 <pre><code> 242 void Game::Render() 243 { 244 if(this->State == GAME_ACTIVE) 245 { 246 // draw background 247 Renderer->DrawSprite(ResourceManager::GetTexture("background"), 248 glm::vec2(0.0f, 0.0f), glm::vec2(this->Width, this->Height), 0.0f 249 ); 250 // draw level 251 this->Levels[this->Level].Draw(*Renderer); 252 } 253 } 254 </code></pre> 255 256 <p> 257 The result is then a nicely rendered level that really starts to make the game feel more alive: 258 </p> 259 260 <img src="/img/in-practice/breakout/levels.png" class="clean" alt="Level in OpenGL breakout"/> 261 262 <h3>The player paddle</h3> 263 <p> 264 While we're at it, we may just as well introduce a paddle at the bottom of the scene that is controlled by the player. The paddle only allows for horizontal movement and whenever it touches any of the scene's edges, its movement should halt. For the player paddle we're going to use the <a href="/img/in-practice/breakout/textures/paddle.png" target="_blank">following</a> texture: 265 </p> 266 267 <img src="/img/in-practice/breakout/textures/paddle.png" class="clean" style="width:256px;height:auto;" alt="Texture image if a paddle in OpenGL breakout"/> 268 269 <p> 270 A paddle object will have a position, a size, and a sprite texture, so it makes sense to define the paddle as a <fun>GameObject</fun> as well: 271 </p> 272 273 <pre><code> 274 // Initial size of the player paddle 275 const glm::vec2 PLAYER_SIZE(100.0f, 20.0f); 276 // Initial velocity of the player paddle 277 const float PLAYER_VELOCITY(500.0f); 278 279 GameObject *Player; 280 281 void Game::Init() 282 { 283 [...] 284 ResourceManager::LoadTexture("textures/paddle.png", true, "paddle"); 285 [...] 286 glm::vec2 playerPos = glm::vec2( 287 this->Width / 2.0f - PLAYER_SIZE.x / 2.0f, 288 this->Height - PLAYER_SIZE.y 289 ); 290 Player = new GameObject(playerPos, PLAYER_SIZE, ResourceManager::GetTexture("paddle")); 291 } 292 </code></pre> 293 294 <p> 295 Here we defined several constant values that define the paddle's size and speed. Within the Game's <fun>Init</fun> function we calculate the starting position of the paddle within the scene. We make sure the player paddle's center is aligned with the horizontal center of the scene. 296 </p> 297 298 <p> 299 With the player paddle initialized, we also need to add a statement to the Game's <fun>Render</fun> function: 300 </p> 301 302 <pre><code> 303 Player->Draw(*Renderer); 304 </code></pre> 305 306 <p> 307 If you'd start the game now, you would not only see the level, but also a fancy player paddle aligned to the bottom edge of the scene. As of now, it doesn't really do anything so we're going to delve into the Game's <fun>ProcessInput</fun> function to horizontally move the paddle whenever the user presses the <var>A</var> or <var>D</var> key: 308 </p> 309 310 <pre><code> 311 void Game::ProcessInput(float dt) 312 { 313 if (this->State == GAME_ACTIVE) 314 { 315 float velocity = PLAYER_VELOCITY * dt; 316 // move playerboard 317 if (this->Keys[GLFW_KEY_A]) 318 { 319 if (Player->Position.x >= 0.0f) 320 Player->Position.x -= velocity; 321 } 322 if (this->Keys[GLFW_KEY_D]) 323 { 324 if (Player->Position.x <= this->Width - Player->Size.x) 325 Player->Position.x += velocity; 326 } 327 } 328 } 329 </code></pre> 330 331 <p> 332 Here we move the player paddle either in the left or right direction based on which key the user pressed (note how we multiply the velocity with the <def>deltatime</def> variable). If the paddle's <code>x</code> value would be less than <code>0</code> it would've moved outside the left edge, so we only move the paddle to the left if the paddle's <code>x</code> value is higher than the left edge's <code>x</code> position (<code>0.0</code>). We do the same for when the paddle breaches the right edge, but we have to compare the right edge's position with the right edge of the paddle (subtract the paddle's width from the right edge's <code>x</code> position). 333 </p> 334 335 <p> 336 Now running the game gives us a player paddle that we can move all across the bottom edge: 337 </p> 338 339 <img src="/img/in-practice/breakout/levels-player.png" class="clean" alt="Image of OpenGL breakout now with player paddle"/> 340 341 <p> 342 You can find the updated code of the Game class here: 343 </p> 344 345 <ul> 346 <li><strong>Game</strong>: <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/progress/4.game.h" target="_blank">header</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/progress/4.game.cpp" target="_blank">code</a> </li> 347 </ul> 348 349 </div> 350 351 <div id="hover"> 352 HI 353 </div> 354 <!-- 728x90/320x50 sticky footer --> 355 <div id="waldo-tag-6196"></div> 356 357 <div id="disqus_thread"></div> 358 359 360 361 362 </div> <!-- container div --> 363 364 365 </div> <!-- super container div --> 366 </body> 367 </html>