LearnOpenGL

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

Powerups.html (17739B)


      1     <h1 id="content-title">Powerups</h1>
      2 <h1 id="content-url" style='display:none;'>In-Practice/2D-Game/Powerups</h1>
      3 <p>
      4   Breakout is close to finished, but it would be cool to add at least one more gameplay mechanic so it's not your average standard Breakout clone; what about powerups?
      5 </p>
      6 
      7 <p>
      8   The idea is that whenever a brick is destroyed, the brick has a small chance of spawning a powerup block. Such a block will slowly fall downwards and if it collides with the player paddle, an interesting effect occurs based on the type of powerup. For example, one powerup makes the paddle larger, and another powerup allows the ball to pass through objects. We also include several negative powerups that affect the player in a negative way.
      9 </p>
     10 
     11 <p>
     12   We can model a powerup as a <fun>GameObject</fun> with a few extra properties. That's why we define a class <fun>PowerUp</fun> that inherits from <fun>GameObject</fun>:
     13 </p>
     14 
     15 <pre><code>
     16 const glm::vec2 SIZE(60.0f, 20.0f);
     17 const glm::vec2 VELOCITY(0.0f, 150.0f);
     18 
     19 class PowerUp : public GameObject 
     20 {
     21 public:
     22     // powerup state
     23     std::string Type;
     24     float       Duration;	
     25     bool        Activated;
     26     // constructor
     27     PowerUp(std::string type, glm::vec3 color, float duration, 
     28             glm::vec2 position, Texture2D texture) 
     29         : GameObject(position, SIZE, texture, color, VELOCITY), 
     30           Type(type), Duration(duration), Activated() 
     31     { }
     32 };  
     33 </code></pre>
     34 
     35 <p>
     36   A <fun>PowerUp</fun> is just a <fun>GameObject</fun> with extra state, so we can simply define it in a single header file which you can find <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/power_up.h" target="_blank">here</a>.
     37 </p>
     38 
     39 <p>
     40   Each powerup defines its type as a string, a duration for how long it is active, and whether it is currently activated. Within Breakout we're going to feature a total of 4 positive powerups and 2 negative powerups:
     41 </p>
     42 
     43 <img src="/img/in-practice/breakout/powerups.png" class="clean" alt="PowerUps used in OpenGL Breakoout"/>
     44   
     45 <ul>
     46   <li><strong>Speed</strong>: increases the velocity of the ball by 20%. </li> 
     47   <li><strong>Sticky</strong>: when the ball collides with the paddle, the ball remains stuck to the paddle unless the spacebar is pressed again. This allows the player to better position the ball before releasing it. </li> 
     48   <li><strong>Pass-Through</strong>: collision resolution is disabled for non-solid blocks, allowing the ball to pass through multiple blocks.</li> 
     49   <li><strong>Pad-Size-Increase</strong>: increases the width of the paddle by 50 pixels.</li> 
     50   <li><strong>Confuse</strong>: activates the confuse postprocessing effect for a short period of time, confusing the user. </li> 
     51   <li><strong>Chaos</strong>: activates the chaos postprocessing effect for a short period of time, heavily disorienting the user.</li> 
     52 </ul>
     53   
     54 <p>
     55  You can find the textures here:
     56 </p>
     57   
     58 <ul>
     59   <li><strong>Textures</strong>: <a href="/img/in-practice/breakout/textures/powerup_speed.png" target="_blank">Speed</a>, <a href="/img/in-practice/breakout/textures/powerup_sticky.png" target="_blank">Sticky</a>, <a href="/img/in-practice/breakout/textures/powerup_passthrough.png" target="_blank">Pass-Through</a>, <a href="/img/in-practice/breakout/textures/powerup_increase.png" target="_blank">Pad-Size-Increase</a>, <a href="/img/in-practice/breakout/textures/powerup_confuse.png" target="_blank">Confuse</a>, <a href="/img/in-practice/breakout/textures/powerup_chaos.png" target="_blank">Chaos</a>.
     60 </ul>
     61   
     62 <p>
     63    Similar to the level block textures, each of the powerup textures is completely grayscale. This makes sure the color of the powerups remain balanced whenever we multiply them with a color vector. 
     64 </p>
     65   
     66 <p>
     67   Because powerups have state, a duration, and certain effects associated with them, we would like to keep track of all the powerups currently active in the game; we store them in a vector:
     68 </p>
     69   
     70 <pre><code>
     71 class Game {
     72     public:
     73         [...]
     74         std::vector&lt;PowerUp&gt;  PowerUps;
     75         [...]
     76         void SpawnPowerUps(GameObject &block);
     77         void UpdatePowerUps(float dt);
     78 };
     79 </code></pre>
     80   
     81 <p>
     82   We've also defined two functions for managing powerups. <fun>SpawnPowerUps</fun> spawns a powerups at the location of a given block and <fun>UpdatePowerUps</fun> manages all powerups currently active within the game.
     83 </p>
     84   
     85 <h3>Spawning PowerUps</h3>
     86 <p>
     87   Each time a block is destroyed we would like to, given a small chance, spawn a powerup. This functionality is found inside the game's <fun>SpawnPowerUps</fun> function:
     88 </p>
     89   
     90 <pre><code>
     91 bool ShouldSpawn(unsigned int chance)
     92 {
     93     unsigned int random = rand() % chance;
     94     return random == 0;
     95 }
     96 void Game::SpawnPowerUps(GameObject &block)
     97 {
     98     if (ShouldSpawn(75)) // 1 in 75 chance
     99         this-&gt;PowerUps.push_back(
    100              PowerUp("speed", glm::vec3(0.5f, 0.5f, 1.0f), 0.0f, block.Position, tex_speed
    101          ));
    102     if (ShouldSpawn(75))
    103         this-&gt;PowerUps.push_back(
    104             PowerUp("sticky", glm::vec3(1.0f, 0.5f, 1.0f), 20.0f, block.Position, tex_sticky 
    105         );
    106     if (ShouldSpawn(75))
    107         this-&gt;PowerUps.push_back(
    108             PowerUp("pass-through", glm::vec3(0.5f, 1.0f, 0.5f), 10.0f, block.Position, tex_pass
    109         ));
    110     if (ShouldSpawn(75))
    111         this-&gt;PowerUps.push_back(
    112             PowerUp("pad-size-increase", glm::vec3(1.0f, 0.6f, 0.4), 0.0f, block.Position, tex_size    
    113         ));
    114     if (ShouldSpawn(15)) // negative powerups should spawn more often
    115         this-&gt;PowerUps.push_back(
    116             PowerUp("confuse", glm::vec3(1.0f, 0.3f, 0.3f), 15.0f, block.Position, tex_confuse
    117         ));
    118     if (ShouldSpawn(15))
    119         this-&gt;PowerUps.push_back(
    120             PowerUp("chaos", glm::vec3(0.9f, 0.25f, 0.25f), 15.0f, block.Position, tex_chaos
    121         ));
    122 }  
    123 </code></pre>
    124   
    125 <p>
    126   The <fun>SpawnPowerUps</fun> function creates a new <fun>PowerUp</fun> object based on a given chance (1 in 75 for normal powerups and 1 in 15 for negative powerups) and sets their properties. Each powerup is given a specific color to make them more recognizable for the user and a duration in seconds based on its type; here a duration of <code>0.0f</code> means its duration is infinite. Additionally, each powerup is given the position of the destroyed block and one of the textures from the beginning of this chapter.
    127 </p>
    128   
    129 <h3>Activating PowerUps</h3>
    130 <p>
    131   We then have to update the game's <fun>DoCollisions</fun> function to not only check for brick and paddle collisions, but also collisions between the paddle and each non-destroyed PowerUp. Note that we call <fun>SpawnPowerUps</fun> directly after a block is destroyed.
    132 </p>
    133   
    134 <pre><code>
    135 void Game::DoCollisions()
    136 {
    137     for (GameObject &box : this-&gt;Levels[this-&gt;Level].Bricks)
    138     {
    139         if (!box.Destroyed)
    140         {
    141             Collision collision = CheckCollision(*Ball, box);
    142             if (std::get&lt;0&gt;(collision)) // if collision is true
    143             {
    144                 // destroy block if not solid
    145                 if (!box.IsSolid)
    146                 {
    147                     box.Destroyed = true;
    148                     this-&gt;SpawnPowerUps(box);
    149                 }
    150                 [...]
    151             }
    152         }
    153     }        
    154     [...] 
    155     for (PowerUp &powerUp : this-&gt;PowerUps)
    156     {
    157         if (!powerUp.Destroyed)
    158         {
    159             if (powerUp.Position.y &gt;= this-&gt;Height)
    160                 powerUp.Destroyed = true;
    161             if (CheckCollision(*Player, powerUp))
    162             {	// collided with player, now activate powerup
    163                 ActivatePowerUp(powerUp);
    164                 powerUp.Destroyed = true;
    165                 powerUp.Activated = true;
    166             }
    167         }
    168     }  
    169 }
    170 </code></pre>
    171 
    172 <p>
    173   For all powerups not yet destroyed, we check if the powerup either reached the bottom edge of the screen or collided with the paddle. In both cases the powerup is destroyed, but when collided with the paddle, it is also activated.
    174 </p>
    175   
    176 <p>
    177   Activating a powerup is accomplished by settings its <var>Activated</var> property to <code>true</code> and enabling the powerup's effect by giving it to the <fun>ActivatePowerUp</fun> function:
    178 </p>
    179   
    180 <pre><code>
    181 void ActivatePowerUp(PowerUp &powerUp)
    182 {
    183     if (powerUp.Type == "speed")
    184     {
    185         Ball-&gt;Velocity *= 1.2;
    186     }
    187     else if (powerUp.Type == "sticky")
    188     {
    189         Ball-&gt;Sticky = true;
    190         Player-&gt;Color = glm::vec3(1.0f, 0.5f, 1.0f);
    191     }
    192     else if (powerUp.Type == "pass-through")
    193     {
    194         Ball-&gt;PassThrough = true;
    195         Ball-&gt;Color = glm::vec3(1.0f, 0.5f, 0.5f);
    196     }
    197     else if (powerUp.Type == "pad-size-increase")
    198     {
    199         Player-&gt;Size.x += 50;
    200     }
    201     else if (powerUp.Type == "confuse")
    202     {
    203         if (!Effects-&gt;Chaos)
    204             Effects-&gt;Confuse = true; // only activate if chaos wasn't already active
    205     }
    206     else if (powerUp.Type == "chaos")
    207     {
    208         if (!Effects-&gt;Confuse)
    209             Effects-&gt;Chaos = true;
    210     }
    211 } 
    212 </code></pre>
    213   
    214 <p>
    215   The purpose of <fun>ActivatePowerUp</fun> is exactly as it sounds: it activates the effect of a powerup as we've described at the start of this chapter. We check the type of the powerup and change the game state accordingly. For the <code>"sticky"</code> and <code>"pass-through"</code> effect, we also change the color of the paddle and the ball respectively to give the user some feedback as to which effect is currently active.
    216 </p>
    217   
    218 <p>
    219    Because the sticky and pass-through effects somewhat change the game logic we store their effect as a property of the ball object; this way we can change the game logic based on whatever effect on the ball is currently active. The only thing we've changed in the <fun>BallObject</fun> header is the addition of these two properties, but for completeness' sake its updated code is listed below:
    220 </p>
    221   
    222 <ul>
    223   <li><strong>BallObject</strong>: <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/ball_object.h" target="_blank">header</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/ball_object.cpp" target="_blank">code</a>.</li>
    224 </ul>
    225   
    226 <p>
    227   We can then easily implement the sticky effect by slightly updating the <fun>DoCollisions</fun> function at the collision code between the ball and the paddle:
    228 </p>
    229   
    230 <pre><code>
    231 if (!Ball-&gt;Stuck && std::get&lt;0&gt;(result))
    232 {
    233     [...]
    234     Ball-&gt;Stuck = Ball-&gt;Sticky;
    235 }
    236 </code></pre>
    237   
    238 <p>
    239   Here we set the ball's <var>Stuck</var> property equal to the ball's <var>Sticky</var> property. If the sticky effect is activated, the ball will end up stuck to the player paddle whenever it collides; the user then has to press the spacebar again to release the ball.
    240 </p>
    241   
    242 <p>
    243   A similar small change is made for the pass-through effect within the same <fun>DoCollisions</fun> function. When the ball's <var>PassThrough</var> property is set to <code>true</code> we do not perform any collision resolution on the non-solid bricks.
    244 </p>
    245   
    246 <pre><code>
    247 Direction dir = std::get&lt;1&gt;(collision);
    248 glm::vec2 diff_vector = std::get&lt;2&gt;(collision);
    249 if (!(Ball-&gt;PassThrough && !box.IsSolid)) 
    250 {
    251     if (dir == LEFT || dir == RIGHT) // horizontal collision
    252     {
    253         [...]
    254     }
    255     else 
    256     {
    257         [...]
    258     }
    259 }  
    260 </code></pre>
    261   
    262 <p>
    263   The other effects are activated by simply modifying the game's state like the ball's velocity, the paddle's size, or an effect of the <fun>PostProcesser</fun> object.
    264 </p>
    265   
    266 <h3>Updating PowerUps</h3>
    267 <p>
    268   Now all that is left to do is make sure that powerups are able to move once they've spawned and that they're deactivated as soon as their duration runs out; otherwise powerups will stay active forever.
    269 </p>
    270   
    271 <p>
    272   Within the game's <fun>UpdatePowerUps</fun> function we move the powerups based on their velocity and decrease the active powerups their duration. Whenever a powerup's duration is decreased to <code>0.0f</code>, its effect is deactivated and the relevant variables are reset to their original state:
    273 </p>
    274   
    275 <pre><code>
    276 void Game::UpdatePowerUps(float dt)
    277 {
    278     for (PowerUp &powerUp : this-&gt;PowerUps)
    279     {
    280         powerUp.Position += powerUp.Velocity * dt;
    281         if (powerUp.Activated)
    282         {
    283             powerUp.Duration -= dt;
    284 
    285             if (powerUp.Duration &lt;= 0.0f)
    286             {
    287                 // remove powerup from list (will later be removed)
    288                 powerUp.Activated = false;
    289                 // deactivate effects
    290                 if (powerUp.Type == "sticky")
    291                 {
    292                     if (!isOtherPowerUpActive(this-&gt;PowerUps, "sticky"))
    293                     {	// only reset if no other PowerUp of type sticky is active
    294                         Ball-&gt;Sticky = false;
    295                         Player-&gt;Color = glm::vec3(1.0f);
    296                     }
    297                 }
    298                 else if (powerUp.Type == "pass-through")
    299                 {
    300                     if (!isOtherPowerUpActive(this-&gt;PowerUps, "pass-through"))
    301                     {	// only reset if no other PowerUp of type pass-through is active
    302                         Ball-&gt;PassThrough = false;
    303                         Ball-&gt;Color = glm::vec3(1.0f);
    304                     }
    305                 }
    306                 else if (powerUp.Type == "confuse")
    307                 {
    308                     if (!isOtherPowerUpActive(this-&gt;PowerUps, "confuse"))
    309                     {	// only reset if no other PowerUp of type confuse is active
    310                         Effects-&gt;Confuse = false;
    311                     }
    312                 }
    313                 else if (powerUp.Type == "chaos")
    314                 {
    315                     if (!isOtherPowerUpActive(this-&gt;PowerUps, "chaos"))
    316                     {	// only reset if no other PowerUp of type chaos is active
    317                         Effects-&gt;Chaos = false;
    318                     }
    319                 }                
    320             }
    321         }
    322     }
    323     this-&gt;PowerUps.erase(std::remove_if(this-&gt;PowerUps.begin(), this-&gt;PowerUps.end(),
    324         [](const PowerUp &powerUp) { return powerUp.Destroyed && !powerUp.Activated; }
    325     ), this-&gt;PowerUps.end());
    326 }  
    327 </code></pre>
    328   
    329 <p>
    330   You can see that for each effect we disable it by resetting the relevant items to their original state. We also set the powerup's <var>Activated</var> property to <code>false</code>. At the end of <fun>UpdatePowerUps</fun> we then loop through the <var>PowerUps</var> vector and erase each powerup if they are destroyed <strong>and</strong> deactivated. We use the <fun>remove_if</fun> function from the <fun>algorithm</fun> header to erase these items given a lambda predicate.
    331 </p>
    332   
    333 <note>
    334   The <fun>remove_if</fun> function moves all elements for which the lambda predicate is true to the end of the container object and returns an iterator to the start of this <em>removed elements</em> range. The container's <fun>erase</fun> function then takes this iterator and the vector's end iterator to remove all the elements between these two iterators.
    335 </note>
    336   
    337 <p>
    338   It may happen that while one of the powerup effects is active, another powerup of the same type collides with the player paddle. In that case we have more than 1 powerup of that type currently active within the game's <var>PowerUps</var> vector. Whenever one of these powerups gets deactivated, we don't want to disable its effects yet since another powerup of the same type may still be active. For this reason we use the <fun>IsOtherPowerUpActive</fun> function to check if there is still another powerup active of the same type. Only if this function returns <code>false</code> we deactivate the powerup. This way, the powerup's duration of a given type is extended to the duration of its last activated powerup:
    339 </p>
    340   
    341 <pre><code>
    342 bool IsOtherPowerUpActive(std::vector&lt;PowerUp&gt; &powerUps, std::string type)
    343 {
    344     for (const PowerUp &powerUp : powerUps)
    345     {
    346         if (powerUp.Activated)
    347             if (powerUp.Type == type)
    348                 return true;
    349     }
    350     return false;
    351 }  
    352 </code></pre>
    353 
    354 <p>
    355   The function checks for all activated powerups if there is still a powerup active of the same type and if so, returns <code>true</code>.
    356 </p>
    357   
    358 <p>
    359   The last thing left to do is render the powerups:
    360 </p>
    361   
    362 <pre><code>
    363 void Game::Render()
    364 {
    365     if (this->State == GAME_ACTIVE)
    366     {
    367         [...]
    368         for (PowerUp &powerUp : this-&gt;PowerUps)
    369             if (!powerUp.Destroyed)
    370                 powerUp.Draw(*Renderer);
    371         [...]
    372     }
    373 }    
    374 </code></pre>
    375   
    376 <p>
    377   Combine all this functionality and we have a working powerup system that not only makes the game more fun, but also a lot more challenging. It'll look a bit like this:
    378 </p>
    379 
    380 <div class="video paused" onclick="ClickVideo(this)">
    381   <video width="600" height="450" loop>
    382     <source src="/video/in-practice/breakout/powerups.mp4" type="video/mp4" />
    383     <img src="/img/in-practice/breakout/powerups_video.png" class="clean"/>
    384   </video>
    385 </div>
    386   
    387 <p>
    388   You can find the updated game code here (there we also reset all powerup effects whenever the level is reset):
    389 </p>
    390   
    391 <ul>
    392   <li><strong>Game</strong>: <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/progress/8.game.h" target="_blank">header</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/progress/8.game.cpp" target="_blank">code</a>.</li>
    393 </ul>  
    394        
    395 
    396     </div>
    397     
    398     <div id="hover">
    399         HI
    400     </div>
    401    <!-- 728x90/320x50 sticky footer -->
    402 <div id="waldo-tag-6196"></div>
    403 
    404    <div id="disqus_thread"></div>
    405 
    406     
    407 
    408 
    409 </div> <!-- container div -->
    410 
    411 
    412 </div> <!-- super container div -->
    413 </body>
    414 </html>