How to Make a Space Invaders Clone with SFLM - Part 5

As with all the other installments of this five part series - I'd like to remind you that the source code is available on Github. Go ahead and check out a copy and follow along. Because this last part involves quite a lot of code - over 1,400 lines I think - you'll be better off downloading a copy of the source and following along. Because it's so long, I'm only going to touch on the most important aspects. Hopefully I've laid out more of the details in one of my side tutorials. Grab a caffeinated beverage, because this is going to be a long one!

For your reference, and maybe sanity, here's a list of tutorials that might fill some gaps in understanding the complete source of level.cpp.
*****UPDATE******
I recently updated/debugged a few things I didn't notice until it was pointed out to me. If you just feel like giving it a play you can download the game and assets and check it out ;)

Past Tutorials:

First of all, I'd like to preface this last installment with a word about refactoring your code. There are many things that could be improved with this, the final push, in making this game. Personally, I was more concerned with getting this done than with making it ultra object-oriented. But if you've been following along thus far and partaking in the side tutorials, you might see where the opportunities for refactoring lie.

Another thing I'd like to say is that I "borrowed" two things here - the "StringHelpers" class template for converting numeric data to strings and the "addFrames" method which comes from the THOR animation tutorial. The "StringHelpers" class came from the source code for the SFML Book. That said, let's take a look at the level class definition:


#ifndef LEVEL_HPP
#define LEVEL_HPP

#include <iostream>
#include <random>
#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#include <Thor/Animation.hpp>
#include <Thor/Math/Distributions.hpp>
#include <THOR/Shapes.hpp>
#include <starfield.hpp>
#include <enemyWave.hpp>
#include <player.hpp>
#include <killer.hpp>
#include <ammo.hpp>
#include <particle.hpp>
#include <opstruct.hpp>
#include <button.hpp>
#include <interpolate.hpp>
#include <StringHelpers.hpp>

namespace nasic
{
    void addFrames(thor::FrameAnimation& animation, int w, int h, int y, int xFirst, int xLast, float duration = 1.f);

    class level
    {
        public:
            level(sf::RenderWindow& window);
            ~level();

            void show(sf::RenderWindow& window);

            const sf::Uint32 levelState(){return m_levelstate;};

            enum levelstate
            {
                uninitialized,
                playing,
                paused,
                lost,
                won,
                exit
            };

        private:
            void showIntro(sf::RenderWindow& window, sf::Time dt);
            bool introIsDone(){return m_introStatus;};
            void setIntroStatus(bool b){m_introStatus = b;};
            void showBossIntro(sf::RenderWindow& window, sf::Time dt);
            void showBonus(sf::RenderWindow& window);

        private:
            nasic::player hero;
            sf::Sprite m_life;
            sf::Uint32 m_numLives;
            std::vector<sf::Sprite> m_lives;
            std::vector<sf::Sprite>::iterator m_livesItr;
            sf::Uint32 m_score;
            sf::Uint32 m_timeBonusFactor;
            sf::Uint32 m_survivalBonus;
            sf::Uint32 m_accuracyBonus;
            sf::Uint32 m_hits;
            sf::Uint32 m_misses;
            sf::Text m_scoreLabel;
            sf::Text m_timeLabel;
            static sf::Uint32 m_levelstate;
            bool m_introStatus;

            nasic::enemyWave m_enemyWave;
            int m_wave;

            nasic::killer m_killer;

            float m_winsizeX;
            float m_winsizeY;
            float m_scaleX;
            float m_scaleY;

            nasic::opstruct m_options;
            int m_musicVolume;
            int m_effectsVolume;
            int m_difficulty;

            sf::Font m_myfont;
            sf::Font m_hudFont;
            gui::button m_introMessage;
            gui::button m_bossMessage;
            sf::Text m_timeBonusMessage;
            sf::Text m_survivalBonusMessage;
            sf::Text m_accuracyBonusMessage;
            sf::Text m_pauseOverlay;

            sf::Sprite m_explosionSpr;

            sf::Sound m_explosion;
            sf::SoundBuffer m_hitBuff;
            sf::SoundBuffer m_explodBuff;
            thor::Animator<sf::Sprite, std::string> m_expAnim;
            thor::FrameAnimation m_expFrames;

            sf::SoundBuffer m_shotBuff;
            sf::Sound m_shot;

            sf::SoundBuffer m_bgSndBuffer;
            sf::Sound m_bgSnd;

            sf::SoundBuffer m_pauseBuffer;
            sf::Sound m_pauseSnd;

            sf::Music m_music;
    };
}

#endif // LEVEL_HPP

Go ahead and grab the source (link provided above) if you haven't already and open up level.cpp. A quick look at the constructor in the "level.cpp" file will tell you there is not a whole lot going on in there. Most of it consists of initializing assets, and code to grab the player's volume, sound effects, and difficulty settings using a the serialize class "opstruct". Then there are some things that are initialized for messages throughout the game and bookkeeping variables for bonuses and scoring:


//initialize score and bonus data
    m_score = 0;
    m_hits = 0;
    m_misses = 0;
    m_timeBonusFactor = 0;
    m_survivalBonus = 0;
    m_accuracyBonus = 0;
    m_timeBonusMessage.setFont(m_hudFont);
    m_timeBonusMessage.setCharacterSize(18);
    m_survivalBonusMessage.setFont(m_hudFont);
    m_survivalBonusMessage.setCharacterSize(18);
    m_accuracyBonusMessage.setFont(m_hudFont);
    m_accuracyBonusMessage.setCharacterSize(18);
    m_timeBonusMessage.setPosition(window.getSize().x*.9f, window.getSize().y/4.f);
    m_survivalBonusMessage.setPosition(window.getSize().x*.9f, window.getSize().y/6.f);
    m_accuracyBonusMessage.setPosition(window.getSize().x*.9f, window.getSize().y/12.f);

That said, there are 4 very important data members in the level class - nasic::player hero, nasic::enemy* m_enemy, nasic::killer  m_killer, and nasic::ammo* m_ammoPtr. There are also corresponding containers for the enemies (std::list<nasic::enemy> m_enemies), the enemy ammo (std::list<nasic::ammo> m_enemyAmmo), the boss's special missile container (std::list<nasic::ammo> m_missileAmmo), and the hero's ammo (std::list<nasic::ammo> m_playerAmmo). The other data members include iterators for the enemy and ammunition containers, as well as gui::button instances which were repurposed here to display messages to the player (not very semantic, but it works), an animator (thor::Animator) with a thor::FrameAnimation instance to go with it for animating the enemy sprites, music/sounds/sound buffers, and some sf::Text instances for outputting vital statistics (health, lives, time, etc.) to the HUD (heads up display). There are of course, more members than that - but that's most of them.

This definition does not contain all the data that will be required to build the level, but no worries, the rest is hard-coded in the implementation. Like I mentioned earlier, refactoring this would be ideal - but there's always a balance to strike when it comes to "getting things done" and making things more modular.

Moving forward, there is a level::show(sf::RenderWindow& window) which just takes a handle to the main window from our game object (see part 1 of the series - link provided above). This function will contain all the core game logic for the level. We will instantiate the enemies using the enemyWave class, control the flow of enemy waves via the m_wave variable (there will be 3 waves of enemies...), as well as setting up all the side functionality such as a heads up display (HUD), flash messaging for scoring and bonuses that the player earns along the way, and some instructions at the start of each wave. In addition to this, the m_wave variable will indicate when it is time to move to the boss fight after 3 waves of enemies. The first 300 lines or so are mostly setting up assets and variables for the HUD, scoring, and messaging throughout the level. There are a few that have obvious uses - bool doBossFight is simply a switch to tell the when the enemy waves are finished and it's time to move to the boss fight. Here is a snippet that might leave you hanging:


    ...
    //some stuff to uniformly distribute the particles
    std::mt19937 engine;
    std::uniform_int_distribution<int> distr(m_scaleX*10, (int)m_killer.getAABB().x/4);
    auto randomizer = std::bind(distr, engine);
    thor::Distribution<int> thorDistr(randomizer);
    ...

It's probably not obvious at all what this is used for until much later in the flow of things. However, this ensures that the particles emitted from Killer - the boss in this level - in a uniformly distributed manner somewhere in the range of 10 pixels and 1/4 of the width of Killer (also given in pixels...) from the origin of the particles (where they are emitted from).

The next possible head-scratcher is this lovely snippet:


for(int i=1; i<m_numLives; i++)
    {
        if(i==1)
        {
            m_life.setPosition(m_life.getGlobalBounds().width + m_scaleX*lives.getGlobalBounds().width/2.f, lives.getPosition().y);
        }
        else
        {
            m_life.setPosition(i*m_life.getGlobalBounds().width + m_scaleX*lives.getGlobalBounds().width/2.f, lives.getPosition().y);
        }

        m_lives.push_back(m_life);
    }

The sprite, m_life, is used as the graphical representation in the HUD (the image of the player's ship) of the number of lives the player has left (m_numLives - which is dictated by the difficulty settings that were loaded in the constructor). It works by simply pushing sprites into a container to be operated on later - i.e. when the player loses a life, we remove a ship from the vector.

What follows this, is more initialization of resources (sounds like this game could really use a resource manager class, huh?). Finally, you'll notice the recognizable entry into the game loop:


...
while(running)
    {
        while(window.pollEvent(e))
        {
            if(e.type == sf::Event::Closed)
            {
                m_levelstate = levelstate::exit;
                return;
            }

            if(e.type == sf::Event::KeyPressed && (introFrames.asSeconds() > 10.f && bossIntroFrames.asSeconds() > 10.f))
            {
                switch(e.key.code)
                {
                case sf::Keyboard::Escape:
                {
                    m_levelstate = levelstate::exit;
                    return;
                }
                break;

                case sf::Keyboard::Delete://super secret enemy clearing mechanism ;)
                {
                    m_enemyWave.debugClearEnemies();
                }
                break;

                case sf::Keyboard::B:
                {
                    m_wave = 4;
                }
                break;

                case sf::Keyboard::K://super secret player killswitch
                {
                    if(m_numLives > 0)
                    {
                        m_numLives -=1;

                        if(m_lives.size() > 0)
                            m_lives.pop_back();

                        std::cout<<"Number of lives: "<<m_numLives<<std::endl;
                    }
                    else
                    {
                        std::cout<<"Number of lives: "<<m_numLives<<std::endl;
                        m_levelstate = levelstate::lost;
                        return;
                    }
                }
                break;

                case sf::Keyboard::F://super secret win button...
                {
                    m_levelstate = levelstate::won;
                    return;
                }
                break;

                case sf::Keyboard::L://super secret lose button...
                {
                    m_levelstate = levelstate::lost;
                    return;
                }
                break;

                case sf::Keyboard::Left:
                {
                    moveleft = true;
                    moveright = false;
                }
                break;

                case sf::Keyboard::Right:
                {
                    moveright = true;
                    moveleft = false;
                }
                break;

                case sf::Keyboard::Up:
                {
                    moveup = true;
                    movedown = false;
                }
                break;

                case sf::Keyboard::Down:
                {
                    movedown = true;
                    moveup = false;
                }
                break;

                case sf::Keyboard::P:
                {
                    pauseSwitch *= -1;//toggle the pause overlay
                    m_pauseSnd.play();
                }
                break;

                case sf::Keyboard::Space:
                {
                    if(ammoClock.getElapsedTime().asSeconds() > .01f)
                    {
                        ammoClock.restart();
                        shooting = true;
                    }
                }
                break;

                default:
                    break;
                }
            }
...

You might notice there are a couple things going on that seem a bit strange. There are buttons with notes that say "super secret". Well, to tell you the truth, they're only there for my debugging pleasure, so I can skip around through the waves of enemies and trigger states in the level. This is very handy when debugging your games, but it is probably unwise to leave things like this in your game permanently. At least ratchet up the difficulty somewhat with a timed key sequence if you want to leave it in. I'm sure you're all aware of things like the Konami Code, take some pains to ensure your debug code is not easily accessible - preferably not there at all if you can. 

The next major section of code deals with handling pause functionality:


...
            /////////////////////////////////
            //pause screen overlay updates
            /////////////////////////////////

            if(pauseSwitch)
            {
                //updates and time for pause message
                if(messageFrames.asSeconds() > 2.f)
                {
                    messageFrames = sf::Time::Zero;
                    colorSwitch *= -1;
                }

                else
                    messageFrames += TimePerFrame;

                float r = interpolate::sineEaseIn(messageFrames.asSeconds(),0.f,255.f,2.f);
                float g = interpolate::sineEaseIn(messageFrames.asSeconds(),0.f,255.f,2.f);
                float b = interpolate::sineEaseIn(messageFrames.asSeconds(),0.f,255.f,2.f);

                if(colorSwitch == -1 && r < 255)
                {
                    m_pauseOverlay.setColor(sf::Color(255,(unsigned int)g,(unsigned int)b,255));
                    pauseBg.setFillColor(sf::Color(0,(unsigned int)g,255-(unsigned int)b,200));
                }

                if(colorSwitch == 1 && r < 255)
                {
                    m_pauseOverlay.setColor(sf::Color(255,255-(unsigned int)g,255-(unsigned int)b,255));
                    pauseBg.setFillColor(sf::Color(0,255-(unsigned int)g,(unsigned int)b,200));
                }
            }
...

First thing is first - we check to see if the game is paused, which is triggered when the player presses "P", toggling the pauseSwitch variable (essentially multiplying by -1, giving it a value of -1 or 1). When the game is paused, we then trigger the sf::Time variable (messageFrames) to start incrementing by the frame time (TimePerFrame). We set the value of some floats (r, g, and b) to an interpolated value based on messageFrames.asSeconds(), which returns a float. As that occurs, we strobe the m_pauseOverlay (an sf::Text) and m_pauseBg (sf::RectangleShape) colors in and out using thresholds set by the colorSwitch variable. When messageFrames is greater than 2 seconds, the colorSwitch variable is multiplied by -1. The last parameter in the interpolate::sineEaseIn(messageFrames.asSeconds(),0.f,255.f,2.f) function keeps the interpolation locked in over 2 seconds, otherwise the color changes in the pause overlay would be very sudden and erratic.

Onward to the next section. This snippet contains the logic for showing the intro messages at the beginning of each wave of enemies. The function we will be looking at is the showIntro(sf::RenderWindow& window, sf::Time dt) function, which is a member function of the level class itself (the code is towards the bottom of level.cpp). It first appears a bit after the pause code:


...
    //show the intro text boxes at the beginning of each wave
    if(m_wave < 4)
        showIntro(window, introFrames);

    if(introFrames.asSeconds() < 2.f)
        m_introMessage.setScale(fabs(interpolate::elasticEaseOut(introFrames.asSeconds(),0.f,m_scaleX*1.45f,2.f)),fabs(interpolate::elasticEaseOut(introFrames.asSeconds(),0.f,m_scaleX*1.45f,2.f)));
...

It simply checks that the player has entered wave 1, 2, or 3, and does an elastic interpolation on the pop-up box that gives it a kind of bouncy effect. The function name "showIntro" is a bit of a misnomer here, because no actual drawing is being done via this function. Which is a good thing, because it does not need to drag down the performance of the game when it is not in use. This function merely handles updates to its constituent parts and pieces when each new wave of enemies begins:


...
void nasic::level::showIntro(sf::RenderWindow& window, sf::Time dt)
{
    if(m_wave == 1)
    {
        m_introMessage = gui::button("Time to destroy \nsome alien ships!", m_myfont, sf::Vector2f(window.getSize().x/2.f, window.getSize().y/2.f), gui::style::clean);

        if(dt.asSeconds() < 2.f)
        {
            m_introMessage.setSize(32);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,m_scaleX*4.f));
        }

        if(dt.asSeconds() > 2.f && dt.asSeconds() < 4.f)
        {
            m_introMessage.setSize(32);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,m_scaleX*4.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("Press the arrow \nkeys to move...");
        }

        else if(dt.asSeconds() > 4.f && dt.asSeconds() < 6.f)
        {
            m_introMessage.setSize(32);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,m_scaleX*4.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("Press the space \nbar to fire...");
        }

        else if(dt.asSeconds() > 6.f && dt.asSeconds() < 8.f)
        {
            m_introMessage.setSize(64);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,-m_scaleX*8.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("Ready...");
        }

        else if(dt.asSeconds() > 8.f && dt.asSeconds() < 10.f)
        {
            m_introMessage.setSize(64);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,-m_scaleX*4.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("Go!!!");
        }
    }

    if(m_wave == 2)
    {
        m_introMessage = gui::button("In N.A.S.I.C.,\nenemies come in waves.", m_myfont, sf::Vector2f(window.getSize().x/2.f, window.getSize().y/2.f), gui::style::clean);

        if(dt.asSeconds() < 2.f)
        {
            m_introMessage.setSize(32);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,m_scaleX*4.f));
        }

        if(dt.asSeconds() > 2.f && dt.asSeconds() < 4.f)
        {
            m_introMessage.setSize(32);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,m_scaleX*4.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("Destroy enemy ships\nas quickly and\naccurately as possible.");
        }

        else if(dt.asSeconds() > 4.f && dt.asSeconds() < 6.f)
        {
            m_introMessage.setSize(32);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,m_scaleX*4.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("The more quickly\nand accurately you\ndestroy each wave,\nthe higher your\nscore will be.");
        }

        else if(dt.asSeconds() > 6.f && dt.asSeconds() < 8.f)
        {
            m_introMessage.setSize(64);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,-m_scaleX*8.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("Ready...");
        }

        else if(dt.asSeconds() > 8.f && dt.asSeconds() < 10.f)
        {
            m_introMessage.setSize(64);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,-m_scaleX*4.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("Go!!!");
        }
    }

    if(m_wave == 3)
    {
        m_introMessage = gui::button("In each level of N.A.S.I.C.,\nyou will encounter a boss.", m_myfont, sf::Vector2f(window.getSize().x/2.f, window.getSize().y/2.f), gui::style::clean);

        if(dt.asSeconds() < 2.f)
        {
            m_introMessage.setSize(32);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,m_scaleX*4.f));
        }

        if(dt.asSeconds() > 2.f && dt.asSeconds() < 4.f)
        {
            m_introMessage.setSize(32);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,m_scaleX*4.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("After three waves of enemies,\na boss fight commences.");
        }

        else if(dt.asSeconds() > 4.f && dt.asSeconds() < 6.f)
        {
            m_introMessage.setSize(32);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,m_scaleX*4.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("Each boss has its\nunique weaknesses, and\nyou must learn to exploit them.");
        }

        else if(dt.asSeconds() > 6.f && dt.asSeconds() < 8.f)
        {
            m_introMessage.setSize(64);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,-m_scaleX*8.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("Ready...");
        }

        else if(dt.asSeconds() > 8.f && dt.asSeconds() < 10.f)
        {
            m_introMessage.setSize(64);
            m_introMessage.setLabelOffset(sf::Vector2f(0.f,-m_scaleX*4.f));
            m_introMessage.setScale(m_scaleX, m_scaleX);
            m_introMessage.setText("Go!!!");
        }
    }
}
...

Pretty simple - just update the message as the time passed to the function marches on. Looking a little closer, you will notice that the entire message lasts 10 seconds, and the message is dependent on which wave the player has entered into.

The next section to dive into deals with the bonus flash messages occurring when the player successfully defeats a wave of enemies. Only, we want to reward the player for accuracy, cat-like reflexes and speed, and for dodging enemy fire. We indicate to the player by displaying messages on the righthand side of the screen:


...
            ///////////////////////////////
            //updates for bonus messages
            ///////////////////////////////

            if(bonusFrames.asSeconds() < 3.f)
            {
                m_timeBonusMessage.move(-4.f*m_scaleX*interpolate::cubicEaseOut(bonusFrames.asSeconds(),0.f,1.f,3.f),0.f);
                m_survivalBonusMessage.move(-4.f*m_scaleX*interpolate::cubicEaseOut(bonusFrames.asSeconds(),0.f,1.f,3.f),0.f);
                m_accuracyBonusMessage.move(-4.f*m_scaleX*interpolate::cubicEaseOut(bonusFrames.asSeconds(),0.f,1.f,3.f),0.f);
                showBonus(window);
            }
            else
            {
                m_timeBonusMessage.setPosition(window.getSize().x*.9f, window.getSize().y/4.f);
                m_survivalBonusMessage.setPosition(window.getSize().x*.9f, window.getSize().y/6.f);
                m_accuracyBonusMessage.setPosition(window.getSize().x*.9f, window.getSize().y/12.f);
            }
...

You will see these objects later in the code, but for now just know that when the message is triggered by the end of the wave, this code ensures that the message slides in from the right via cubic interpolation, after which its position is clamped at about 1/10th of the screen size from the right. It quickly disappears after the motion has stopped.

Next stop is the player updates. When the player hits a button that controls his/her ship, it sets a boolean variable to true. When the button is released, it sets it to false. This makes it easier to read the event handling code that occurs at the top of every iteration of the game loop. Here is how we handle it:


...
            ////////////////////////////
            //perform player updates
            ////////////////////////////

            if(introFrames.asSeconds() > 10.f && pauseSwitch != 1)
            {
                //update the player
                hero.update(window, TimePerFrame);

                if(moveleft && hero.getPosition().x >= 0.f - hero.getAABB().x/2.f && pauseSwitch != 1)
                    hero.move(sf::Vector2f(-hero.getVel()*m_scaleX, 0.f),TimePerFrame);

                if(moveright && hero.getPosition().x <= m_winsizeX - hero.getAABB().x*2.f && pauseSwitch != 1)
                    hero.move(sf::Vector2f(hero.getVel()*m_scaleX, 0.f),TimePerFrame);

                if(moveup && hero.getPosition().y >= m_winsizeY/1.5f - hero.getAABB().y/2.f && pauseSwitch != 1)
                    hero.move(sf::Vector2f(0.f, -hero.getVel()*m_scaleX),TimePerFrame);

                if(movedown && hero.getPosition().y <= m_winsizeY*.85f - hero.getAABB().y && pauseSwitch != 1)
                    hero.move(sf::Vector2f(0.f, hero.getVel()*m_scaleX),TimePerFrame);

                if(ammoClock.getElapsedTime().asSeconds() < .01f)
                    hero.initAmmo(shooting, m_scaleY, m_shot);

                hero.updateProjectiles(TimePerFrame);
            }
...

First, the player is updated - this essentially is the code that tracks the state of the player and handles it accordingly, which is encapsulated in the player class. Then we have to check to make sure that the intro message has terminated, and that the game is not paused. There is no sense in updating the player unless these two conditions are met. If the player is moving left, the motion of the ship continues until its position is clamped at the left side of the screen. If the player is moving right, the motion of the ship continues until its position is clamped at the right side of the screen. If the player is moving down, it will continue to move until it hits the bottom. Last but not least, upward motion is clamped at about half way up the screen. It is also here in this block of code that the player's ammo is initialized and updated.

Next in line is collision detection/handling and updating the player's score. First, we check to see if the intro is still playing, and if it is, then we just update the animation for the enemy wave:


...
            //////////////////////////////////////////////////////////////
            //perform *ONLY* the enemy animation while intro is playing
            //////////////////////////////////////////////////////////////

            if(m_wave < 4 && !m_enemyWave.isEmpty() && introFrames.asSeconds() < 10.f)
            {
                m_enemyWave.animate(TimePerFrame);
            }
...

So, for the first 10 seconds of each wave, you should only see the message displayed, and the enemy sprites animating.

Next, is collision detection and handling. The first chunk of the collision logic deals with player projectiles that are colliding with the enemies:


...
            ///////////////////////////////////////
            //collision detection and resolution
            ///////////////////////////////////////

            if(m_wave < 4 && m_enemyWave.initStatus() && introFrames.asSeconds() > 10.f && pauseSwitch != 1)
            {
                if(!m_enemyWave.isEmpty())
                {
                    //update the player's bullets
                    hero.updateProjectiles(TimePerFrame);

                    //check collisions with enemies
                    playerToEnemyCollision = hero.checkEnemyCollisions(window, m_enemyWave.m_enemies, m_explosionSpr, m_explosion, m_explodBuff, m_hitBuff, m_expAnim, "explode", m_scaleX);
                    if(playerToEnemyCollision != nasic::player::hitList::miss)
                    {
                        //update the hit count
                        m_hits++;

                        //update the score
                        switch(playerToEnemyCollision)
                        {
                        case nasic::player::hitList::agravu:
                        {
                            m_score += 10;
                        }
                        break;

                        case nasic::player::hitList::delsiriak:
                        {
                            m_score += 10;
                        }
                        break;

                        case nasic::player::hitList::gluorn:
                        {
                            m_score += 50;
                        }
                        break;

                        case nasic::player::hitList::rhiians:
                        {
                            m_score += 50;
                        }
                        break;

                        case nasic::player::hitList::hit:
                            {
                                m_score += 0;
                            }
                            break;

                        default:
                            {
                                m_score += 0;
                            }
                            break;
                        }
                    }
                    else
                    {
                        //update the miss count
                        m_misses++;
                    }
...

Basically, there is a check up front on m_wave to make sure the player is still progressing through waves 1-3, a check to make sure that the enemy wave has been initialized/re-initialized, a check to make sure we are through the intro, and finally that the game is not paused. Next, the all-important check to see if the enemy list is empty, which indicates when we should increment m_wave so the player can move on to the next wave of enemies. If all these checks pass, we'll move forward with collision detection and score updates. The playerToEnemyCollision variable is an sf::Uint32 declared earlier in the program, which collects the return value from player.checkEnemyCollisions(...). If you look above, the signature in that method is absurdly long. Anyone who used this would be confused, which is why it should be refactored. However, it works and I'll explain how. To do that, crack open the source for player.cpp and look up the checkEnemyCollisions(...) function:


...
sf::Uint32 nasic::player::checkEnemyCollisions(sf::RenderWindow& window, std::list<nasic::enemy>& enemies, sf::Sprite& explosion, sf::Sound& explode, sf::SoundBuffer& buffer, sf::SoundBuffer& hitbuffer, thor::Animator<sf::Sprite, std::string>& explosionAnim, std::string animation, float scale)
{
    sf::Uint32 tempHit;

    std::list<nasic::ammo>::iterator ammoIt;
    for(ammoIt = m_playerAmmo.begin(); ammoIt != m_playerAmmo.end(); ++ammoIt)
    {
        if((*ammoIt).getPosition().y < 0.f)//erase the ammo if it leaves the screen
        {
            ammoIt = m_playerAmmo.erase(ammoIt);

            //std::cout<<m_playerAmmo.size()<<std::endl;
            tempHit = nasic::player::hitList::miss;
        }
    }

    std::list<nasic::enemy>::iterator enemIt;
    for(enemIt = enemies.begin(); enemIt != enemies.end(); ++enemIt)
    {
        std::list<nasic::ammo>::iterator pAmmoIt;
        for(pAmmoIt = m_playerAmmo.begin(); pAmmoIt != m_playerAmmo.end(); ++pAmmoIt)
        {
            if(pAmmoIt->getPosition().y >= enemIt->getPosition().y
                    && pAmmoIt->getPosition().y <= enemIt->getPosition().y + scale*enemIt->getAABB().y
                    && pAmmoIt->getPosition().x >= enemIt->getPosition().x
                    && pAmmoIt->getPosition().x <= enemIt->getPosition().x + scale*enemIt->getAABB().x)
            {
                //erase the bullet from the list
                pAmmoIt = m_playerAmmo.erase(pAmmoIt);

                enemIt->damage(1);//damage the enemy

                if(enemIt->getState() == nasic::enemy::enemyState::dead)
                {
                    //set the explosion position to the enemy location
                    explosion.setPosition(enemIt->getPosition().x, enemIt->getPosition().y);

                    //set the explosion sound buffer and play it
                    explode.setBuffer(buffer);
                    explode.play();

                    //play the explosion animation
                    explosionAnim.playAnimation(animation, false);

                    //erase the enemy from the list
                    enemIt = enemies.erase(enemIt);

                    //return results for updating the score
                    switch(enemIt->getType())
                    {
                    case nasic::enemy::enemyType::Agravu:
                    {

                        tempHit = nasic::player::hitList::agravu;
                    }
                    break;

                    case nasic::enemy::enemyType::Delsiriak:
                    {
                        tempHit = nasic::player::hitList::delsiriak;
                    }
                    break;

                    case nasic::enemy::enemyType::Gluorn:
                    {
                        tempHit = nasic::player::hitList::gluorn;
                    }
                    break;

                    case nasic::enemy::enemyType::Rhiians:
                    {
                        tempHit = nasic::player::hitList::rhiians;
                    }
                    break;

                    default:
                        break;
                    }
                }

                else if(enemIt->getState() != nasic::enemy::enemyState::dead)
                {
                    //no score update, but we keep track of the hits ;)
                    explode.setBuffer(hitbuffer);
                    explode.play();

                    tempHit = nasic::player::hitList::hit;
                }
            }
        }
    }

    return tempHit;
}
...
Todd  And the algorithm goes thusly...

The function itself returns an sf::Uint32, which is mapped to an enum holding the types of collisions that can occur. The player can hit one of four enemy types which dictate the score. However, the first "for loop" iterates over the player's projectiles to see if they leave the top of the screen - if they do then the function returns a miss (nasic::player::hitList::miss).

The next section consists of nested "for loops". The outer loop iterates over enemies, and the inner loop iterates over the player's projectiles - in that order because the inner loop is executed first, and we want to check the projectiles against the enemies. So, that said, if the ammo overlaps the enemy sprite, some events occurr. First, we erase the ammo from the std::list<nasic::ammo> container that belongs to the player, then take the pointer to the enemy and apply damage to it via enemIt->damage(1). Then, most importantly, there is an additional check on the enemy's current state to see if it is dead, if it is not, the loop breaks there and returns a hit (nasic::player::hitList::hit) which will be used to calculate a bonus back in the game loop in level.cpp. However, in the event that the enemy is dead, the loop continues. The sprite representing the explosion that was passed in to the function is positioned to where the ship is, an animation is played for the explosion, then a sound is played, and subsequently, the enemy is removed from the std::list<nasic::enemy> passed in to the function. Finally, there is a check on the enemy's type, and that value is returned by the collision function. Whew....did you get all that??

So, returning to the playerToEnemyCollision value in level.cpp, there is a check to make sure that it is not equal to nasic::player::hitList::miss. If this condition is true, a hit is recorded via m_hits++. Else, m_misses is incremented. These two variables will be used to calculate the hit/miss ratio after each wave and reward the player if they are accurate enough. You'll also notice there is a switch/case on that variable. There are two things to notice in here. The default in the switch statement is to increment the score by 0, and the same if a nasic::player::hitList::hit is recorded. This prevents the score from either incrementing into infinity or being reset to 0 - neither of which are preferable. Other than that, the score is incremented based on the enemy killed.

In the step that follows, we'll check collisions against the player's ship from enemy fire. Here is the block that I'm referring to:


...
                    ///////////////////////////////////////////////////////////////
                    //do all updates for the enemies after the intro is over
                    //and *ONLY* if the list is not empty
                    //doing otherwise will result in a crash because the iterator
                    //will point to enemies that do not exist in our list
                    ///////////////////////////////////////////////////////////////

                    m_enemyWave.initProjectiles(TimePerFrame, m_scaleX);
                    m_enemyWave.updateEnemies(TimePerFrame, m_scaleX);

                    //check collisions with enemies - because the value
                    //we're retrieving is an enumerated type and a miss = 0,
                    //we need to check if the value returned by collision is > 0
                    enemyToPlayerCollision = m_enemyWave.checkPlayerCollisions(window, hero, m_scaleX, m_explosionSpr, m_explosion, m_explodBuff, m_expAnim, "explode");

                    if(enemyToPlayerCollision > 0)
                    {
                            life.setFillColor(sf::Color(255,255,0,200));

                            switch(enemyToPlayerCollision)
                            {
                            case nasic::enemyWave::hitList::agravu:
                            {
                                if(hero.getHealth() <= 0 && m_numLives > 0)
                                {
                                    hero.heal(maxHealth);
                                    m_numLives -=1;
                                    if(m_lives.size() > 0)
                                        m_lives.pop_back();
                                }
                                else if(hero.getHealth() <=0 && m_numLives == 0) //player is a complete loser...
                                {
                                    m_levelstate = levelstate::lost;
                                    return;
                                }
                                else
                                    hero.damage(10);
                            }
                            break;

                            case nasic::enemyWave::hitList::delsiriak:
                            {
                                if(hero.getHealth() <= 0 && m_numLives > 0)
                                {
                                    hero.heal(maxHealth);
                                    m_numLives -=1;
                                    if(m_lives.size() > 0)
                                        m_lives.pop_back();
                                }
                                else if(hero.getHealth() <=0 && m_numLives == 0)//player is a complete loser...
                                {
                                    m_levelstate = levelstate::lost;
                                    return;
                                }
                                else
                                    hero.damage(10);
                            }
                            break;

                            case nasic::enemyWave::hitList::gluorn:
                            {
                                if(hero.getHealth() <= 0 && m_numLives > 0)
                                {
                                    hero.heal(maxHealth);
                                    m_numLives -=1;
                                    if(m_lives.size() > 0)
                                        m_lives.pop_back();
                                }
                                else if(hero.getHealth() <=0 && m_numLives == 0) //player is a complete loser...
                                {
                                    m_levelstate = levelstate::lost;
                                    return;
                                }
                                else
                                    hero.damage(20);
                            }
                            break;

                            case nasic::enemyWave::hitList::rhiians:
                            {
                                if(hero.getHealth() <= 0 && m_numLives > 0)
                                {
                                    hero.heal(maxHealth);
                                    m_numLives -=1;
                                    if(m_lives.size() > 0)
                                        m_lives.pop_back();
                                }
                                else if(hero.getHealth() <=0 && m_numLives == 0) //player is a complete loser...
                                {
                                    m_levelstate = levelstate::lost;
                                    return;
                                }
                                else
                                    hero.damage(20);
                            }
                            break;

                            default:
                                break;
                            }
                    }

                    else
                    {
                        life.setFillColor(sf::Color(0,255,0,200));
                    }

                }
...

Essentially the same thing is going on here (hey, I try to be consistent...). Only instead of playerToEnemyCollision, we have a variable aptly named enemyToPlayerCollision, and it maps to the enemyWave class method checkPlayerCollisions(..). The real bread and butter begins in the switch statment on enemyToPlayerCollision. The first thing that occurs if an enemy successfully fires upon the player, is a check on the player's health. If the player's health is less than or at zero and the number of lives the player has left is greater than zero, we refill the player's health and decrement the number of lives the player has left. While we're in there, we update the HUD element representing the player's lives by doing a pop_back on the m_lives container. If the player's lives reach zero (and this is important!!), a state change in the level is triggered - and the level object retuns with a game over (levelstate::lost). Subsequently, when the player loses, the game goes to the nasic::Game::lose state and returns to the main menu after Killer laughs derisively at them. If all those checks fail, then the player's health is simply decremented according to the type of enemy that fired upon them. No sweat.

Next, we'll talk about the code that is executed after each enemy wave is defeated. Here it is:


...
else
                {
                    //do the time bonus calculation
                    if(timeClock.asSeconds() < 30.f)
                        m_timeBonusFactor = 3;

                    else if(timeClock.asSeconds() < 60.f)
                        m_timeBonusFactor = 2;

                    else
                        m_timeBonusFactor = 1;

                    m_score = m_score*m_timeBonusFactor;//multiply score by bonus factor

                    //do the survival bonus calculation
                    if(m_numLives == initialLives)
                        m_survivalBonus = 10000*m_wave;//multiply by wave so it scales with difficulty to achieve the bonus

                    else if(m_numLives == initialLives - 1)
                        m_survivalBonus = 1000*m_wave;

                    else
                        m_survivalBonus = 0;

                    m_score = m_score+m_survivalBonus;

                    //do the accuracy bonus calculation
                    float tempRate = (float)m_hits/(m_hits+m_misses);
                    accuracyRatio = (int)100*(tempRate);

                    if(accuracyRatio > 90)
                        m_accuracyBonus = 10000;
                    else if(accuracyRatio > 75)
                        m_accuracyBonus = 5000;
                    else if(accuracyRatio > 50)
                        m_accuracyBonus = 1000;
                    else if(accuracyRatio < 50)
                        m_accuracyBonus = 0;

                    m_score = m_score+m_accuracyBonus;

                    bonusFrames = sf::Time::Zero;

                    //std::cout<<"Time: "<<timeClock.asSeconds()<<"\nInitial lives: "<<initialLives<<"\nNew lives: "<<m_numLives<<"\nHits: "<<m_hits<<"\nMisses: "<<m_misses<<"\nHit ratio: "<<accuracyRatio<<std::endl;
                    //std::cout<<"Time bonus: "<<m_timeBonusFactor<<"\nSurvival bonus: "<<m_survivalBonus<<"\nAccuracy bonus: "<<m_accuracyBonus<<std::endl;

                    ++m_wave;
                    timeClock = sf::Time::Zero;//reset the timer for bonuses
                    introFrames = sf::Time::Zero;
                    m_enemyWave.setInitStatus(false);
                    m_enemyWave.purgeEnemies();
                    m_enemyWave.init(window, 4, 10, m_scaleX);
                }
            }
...

If you've been following along in the source code, the "else" clause continues on from the check on the enemy list. If the enemy list is empty, then you've reached this point - which should make sense to you. The first stop in this block is the part about the time bonus factor (m_timeBonusFactor). Simply put, if the timer in the lower left of the screen is less than 30 seconds at the close of the current wave, then we set the time bonus factor to 3. If it is between 30 and 60 seconds we give them a bonus of x2. Otherwise the player gets no time bonus :(. After the calculation is done, the score is updated by multiplying by this factor. The survival bonus (m_survivalBonus) is calculated based on the player's initial number of lives at the start of the waves less the number of lives lost during the wave. It is further tied to the wave number to reward the player more for having more lives as the game progresses. The final number for the survival bonus is then added to the player's score. In the section that follows this, the accuracy ratio is computed based on the number of hits/(number of hits+number of misses)*100 to get the percentage cast as an integer. The ratio is then further analyzed, where the break points are set at 90, 75, and 50. If the player has a hit-to-miss ratio less than that they get no accuracy bonus. The bonus is then added to the score and the bonus frames are set to zero. Setting the bonus frames to zero ensures that the next time the bonuses are calculated, the text appears on the screen to the same effect (slides in from the right).

After the bonuses are calculated, the m_wave variable is incremented, the game timer in the lower left corner is reset, the intro frames (introFrames) are reset to zero so the next intro can play, the enemies are purged (there should not be any traces left...but just in case), the initialization status of the enemy wave is set to false, and the enemy wave is re-initialized for the next wave. Pretty cool huh? ;)


...
else if(m_wave == 4)
            {
                doBossFight = true;
                //a little housekeeping to reset boss intro frames
                ++tempcounter;
                if(tempcounter == 1)
                {
                    m_enemyWave.setInitStatus(false);
                    m_enemyWave.purgeEnemies();
                    bossIntroFrames = sf::Time::Zero;
                    killerDrone.play();
                }
            }
...

Assuming the player has made it through all 3 waves of enemies (...it's tough I know :D), they enter the boss fight. Here is the beginning of this section of the game:


...
if(doBossFight)
            {
                //////////////////////////////////////
                //perform boss animation updates
                //////////////////////////////////////

                m_killer.updateEyeColor(TimePerFrame);
                m_killer.animateMandibles(TimePerFrame, m_scaleX);

                //show the boss intro messages
                showBossIntro(window,bossIntroFrames);

                if(bossIntroFrames.asSeconds() < 1.f)
                    m_killer.move(0.f, m_scaleX*10.f*interpolate::expoEaseOut(bossIntroFrames.asSeconds(),0.f,1.f,1.f));

                m_killer.speak(TimePerFrame);
...

In the first little subsection of code, the animations for Killer are initiated and the intro is triggered. The first check querys the frame time for the intro, and while it is less than 1 second, the boss flies in in a herky-jerky motion to try and scare the player (I wonder if it actually makes anyone jump...?). Anyhow, Killer makes an appearance via interpolation of it's position from offscreen to onscreen using exponential easing - which is what makes him come screaming in and come to a stop. He also speaks for the first time. There are, however, some interesting details to show you. Open up killer.cpp and have a look at the speak(...) method:


...
void nasic::killer::speak(sf::Time dt)
{
    if(m_speechSwitch == 1 && m_playSwitch)
    {
        m_vocals.setBuffer(m_laughBuffer);
    }

    else if(m_speechSwitch == -1 && m_playSwitch)
    {
        m_vocals.setBuffer(m_tauntBuffer);

    }

    if(m_killerState == killerState::dead)
    {
        m_vocals.setBuffer(m_deadBuffer);
        m_playSwitch = true;
    }

    if(m_speechCounter == 1)
    {
        m_vocals.play();
    }

    /////////////////////////////////////////////
    //do the housekeeping necessary to play
    //the sounds and stop them from repeating
    /////////////////////////////////////////////

    if(m_speechFrames.asSeconds() > 10.f)//say something every 10 seconds
    {
        m_speechSwitch *= -1;
        m_playSwitch = true;
        m_speechFrames = sf::Time::Zero;
    }

    else
    {
        m_playSwitch = false;
        m_speechFrames += dt;
    }

    if(!m_playSwitch)
        m_speechCounter = 0;

    else
        ++m_speechCounter;

}
...

This method ensures that Killer will be taunting the player throughout the entire fight - however long that might be. There are a few housekeeping variables here, but it is relatively straight-forward. Every 10 seconds, Killer will speak - sometimes he laughs and sometimes he says "Destroy all humanoids" - which I think is a direct rip from the game Berzerk. There is a switch (m_speechSwitch) that is multiplied by -1 every 10 seconds to ensure that it changes back and forth from laughter to "Destroy all humanoids" by swapping sf::SoundBuffer objects. There is one special condition: if Killer dies we swap in a special buffer (m_deadBuffer) and play it no matter what.

The next snippet you run into basically states, when the boss intro is finished after 10 seconds, we'll start performing updates. Many, if not all, the methods called here are explained more fully in the boss tutorial referenced at the top with a link. Take a look at that tutorial if you'd like a better understanding.


...
if(bossIntroFrames.asSeconds() > 10.f && pauseSwitch != 1)
                {
                    //////////////////////////////////////
                    //perform boss updates
                    //////////////////////////////////////

                    m_killer.updateMotion(TimePerFrame, m_scaleX, m_scaleY);
                    m_killer.updateState();

                    //////////////////////////////////////////
                    //perform boss ammo and particle updates
                    //////////////////////////////////////////

                    //instantiate as many particles as possible ;)
                    m_killer.initParticles(thorDistr());

                    //update particles
                    m_killer.updateParticles(TimePerFrame,m_scaleX);
                    m_killer.fireAmmo(TimePerFrame, m_scaleX, m_scaleY);
...

Diving back into the main game logic, we left off near the end. The rest of the code from here on is pretty much explained earlier. After the updates are made, as seen above, similar collision routines are called and the collisions are handled in much the same way. If the player hits Killer in the eyes, damage him and they score points. Else, they do not. The main difference is that Killer, in the spirit of games like Smash TV and Total Carnage, fires an absurd number of projectiles of different types at the player, and it's virtually impossible not to get hit - I'm betting most people lose a couple lives. ;)

The final important thing I need to mention is that the when the level object returns, the game object checks the state of the level with levelState(). This tells the game whether you won, lost, rage-quit, or whatever happened by eturning an sf::Uint32 which is mapped to this enum:


...
            enum levelstate
            {
                uninitialized,
                playing,
                paused,
                lost,
                won,
                exit
            };
...

Much of the other code in level.cpp is well-commented and fairly easy to follow. Most of the draw calls are conditional based on what wave the player has entered. Stay tuned and in the next tutorial, I'll show you how to package up your game in a windows installer with some goodies. Thanks for reading, as always, and stay tuned for part 6!