LearnOpenGL

Translation in progress of learnopengl.com.
git clone https://git.mtkn.jp/LearnOpenGL
Log | Files | Refs

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&lt;GameObject&gt; 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&lt;std::vector&lt;unsigned int&gt;&gt; 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-&gt;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&lt;std::vector&lt;unsigned int&gt;&gt; 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&lt;unsigned int&gt; row;
     96             while (sstream &gt;&gt; tileCode) // read each word separated by spaces
     97                 row.push_back(tileCode);
     98             tileData.push_back(row);
     99         }
    100         if (tileData.size() &gt; 0)
    101             this-&gt;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&lt;std::vector&lt;unsigned int&gt;&gt; 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&lt;float&gt;(width);
    118     float unit_height   = lvlHeight / height;
    119     // initialize level tiles based on tileData		
    120     for (unsigned int y = 0; y &lt; height; ++y)
    121     {
    122         for (unsigned int x = 0; x &lt; 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-&gt;Bricks.push_back(obj);
    135             }
    136             else if (tileData[y][x] &gt; 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-&gt;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&lt;GameLevel&gt; 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-&gt;Width, this-&gt;Height / 2);
    226     GameLevel two; two.Load("levels/two.lvl", this-&gt;Width, this-&gt;Height / 2);
    227     GameLevel three; three.Load("levels/three.lvl", this-&gt;Width, this-&gt;Height / 2);
    228     GameLevel four; four.Load("levels/four.lvl", this-&gt;Width, this-&gt;Height / 2);
    229     this-&gt;Levels.push_back(one);
    230     this-&gt;Levels.push_back(two);
    231     this-&gt;Levels.push_back(three);
    232     this-&gt;Levels.push_back(four);
    233     this-&gt;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-&gt;State == GAME_ACTIVE)
    245     {
    246         // draw background
    247         Renderer-&gt;DrawSprite(ResourceManager::GetTexture("background"), 
    248             glm::vec2(0.0f, 0.0f), glm::vec2(this-&gt;Width, this-&gt;Height), 0.0f
    249         );
    250         // draw level
    251         this-&gt;Levels[this-&gt;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-&gt;Width / 2.0f - PLAYER_SIZE.x / 2.0f, 
    288         this-&gt;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-&gt;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-&gt;State == GAME_ACTIVE)
    314     {
    315         float velocity = PLAYER_VELOCITY * dt;
    316         // move playerboard
    317         if (this-&gt;Keys[GLFW_KEY_A])
    318         {
    319             if (Player-&gt;Position.x &gt;= 0.0f)
    320                 Player-&gt;Position.x -= velocity;
    321         }
    322         if (this-&gt;Keys[GLFW_KEY_D])
    323         {
    324             if (Player-&gt;Position.x &lt;= this-&gt;Width - Player-&gt;Size.x)
    325                 Player-&gt;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>