Top Down Shoot Em Up Mechanics - Part 2

 

In this segment, I'm going to quickly go over how to create hoards of enemies. One of the most important things you can do is think about how you are going to manage and update the entities in your game. There are many options, but one fairly flexible option is the scene graph. It is fairly easy to grasp and can give you a more hierarchical representation of the entities in your game. There are two main approaches (at least that I am aware of at this time) to designing a scene graph. One method involves relying on inheritance design principles and one involves relying on component design principles.

I have decided to implement a simpler scene graph based on the one provided in the SFML Book. There is essentially one class that does most of the work for managing game entities - Node. If you scan the source repository for the SFML Book, you will find the SceneNode class. It is a bit more involved for my purposes here, so I simplified it to only manage insertion, removal, tranforms, and drawing at the moment. That said, when the game is complete, there will be a few more additional methods in this class. Here is a look at the resulting app:

The main difference in my class definition is what is passed to the update function. Rather than approach it naively, I have reasoned that most of the nodes I attach to the scene graph will need to have frame time, the coordinates of a target or goal they need to reach, and the bounds of that target or goal. Since the hoards will be following the player, this was the simplest way I could think of to do this for the time being. Here is the Node class definition:


#ifndef NODE_HPP
#define NODE_HPP

#include <GeneralUtilities.hpp>

#include <SFML/System/NonCopyable.hpp>
#include <SFML/System/Time.hpp>
#include <SFML/Graphics/Transformable.hpp>
#include <SFML/Graphics/Drawable.hpp>
#include <SFML/Graphics/RenderStates.hpp>
#include <SFML/Graphics/RenderTarget.hpp>

#include <memory>
#include <vector>

class Node : public sf::Transformable, public sf::Drawable, private sf::NonCopyable
{
    public:

        typedef std::unique_ptr<Node> ptr;

    public:
        Node();
        ~Node();

        void attachChild(ptr child);
        ptr detachChild(const Node& node);

        void update(sf::Time dt, sf::Vector2f target, sf::Vector2f bounds);
        virtual void updateCurrent(sf::Time dt, sf::Vector2f target, sf::Vector2f bounds);
        void updateChildren(sf::Time dt, sf::Vector2f target, sf::Vector2f bounds);

        virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;
        virtual void drawCurrent(sf::RenderTarget&, sf::RenderStates) const;
        virtual void drawChildren(sf::RenderTarget& target, sf::RenderStates states) const;

    private:
        Node* m_parent;
        std::vector<ptr> m_children;
};

#endif // NODE_HPP

Pretty simple. The methods are self-explanatory - but if you need more explanation you can find it in the SFML Book. The implementation can be found in the full repo that I will point you to later.

The second thing we must do is inherit from this Node class and create entities that will be attached to the scene graph. I've decided to make a game where the player will be chased by Samsquamptches.

Samsquamptches?


Well, not quite, but close enough - they are little green squares. Taking what was learned from the previous segment of this tutorial, we just need to move it to the Samsquamptch class and make it friendly to the scene graph. Here is the class definition for Samsquamptch:



#ifndef SAMSQUAMPTCH_HPP
#define SAMSQUAMPTCH_HPP

#include <Node.hpp>

#include <SFML/Graphics/RectangleShape.hpp>
#include <SFML/System/Time.hpp>
#include <SFML/System/Vector2.hpp>

#include <GeneralUtilities.hpp>

class Samsquamptch : public Node
{
    public:
        Samsquamptch();
        Samsquamptch(float aggressionLevel);
        ~Samsquamptch();

        sf::Uint32 const getHealth() const {return m_health;};
        void damage(int d);

        void setSize(float x, float y){m_rect.setSize(sf::Vector2f(x,y));};
        sf::Vector2f const getSize() const {return m_rect.getSize();};

        void setFillColor(sf::Color c){m_rect.setFillColor(c);};

        virtual void drawCurrent(sf::RenderTarget& target, sf::RenderStates states) const;

        virtual void updateCurrent(sf::Time dt, sf::Vector2f target, sf::Vector2f bounds);

     private:
        sf::Vector2f direction(sf::Vector2f target);
        void acquireTarget(sf::Vector2f target, sf::Vector2f bounds);

    private:
        sf::RectangleShape m_rect;

        sf::Vector2f m_velocity;
        sf::Vector2f m_bounds;
        sf::Vector2f m_target;
        float m_angle;

        sf::Uint32 m_health;
        bool m_alive;
        float m_aggressionLevel;
};

#endif // SAMSQUAMPTCH_HPP

There is a small amount of duplication here - setters and getters for size and fill color. Other than that, this is a reasonably clean interface. The main things to notice are that the virtual functions from the Node class for drawCurrent and updateCurrent are implemented in the Samsquamptch class. This is so that the base class Node, which calls updateCurrent() and drawCurrent() via the plain draw() and update() calls, will use the appropriate function when performing operations on child and parent nodes when we make calls from the scene graph. 

In the end, this class does the same thing as the previous segment, except now we have the beginnings of some scene management. Since most of the code did not change from the previous example, you can just look at the source and check part 1 of this tutorial if you need to revisit.

Now, the hardest part, which turns out to not be *that* difficult, is constructing a scene graph from the Node class. You can see below, all you need is an empty Node instance to stuff more nodes into. Pretty easy, huh? I also like the additional care that was taken in the SFML Book implementation where layers were represented via a std::array taking as parameters a Node* pointer and the number of layers - at this point there is only one layer for Samsquamptches. This not necessary, you could just stuff the nodes into the sceneGraph object via attachChild() and go from there, but it makes things easier to follow, I think, when there is an identifier to accompany the nodes that you are attaching. Here is the main entry point of the application:


#include <iostream>
#include <string>
#include <memory>
#include <algorithm>
#include <utility>
#include <array>

#include <SFML/Graphics.hpp>

#include <GeneralUtilities.hpp>
#include <Node.hpp>
#include <Samsquamptch.hpp>
#include <Player.hpp>

enum Layer
{
    samsquamptches,
    layerCount
};

int main()
{
    sf::VideoMode desktop = sf::VideoMode::getDesktopMode();
    sf::RenderWindow window(sf::VideoMode(800,600,32), "The Hoards that Follow", sf::Style::Default);
    sf::Event e;

    Player player;
    player.setFillColor(sf::Color(200,0,0,255));
    player.setSize(sf::Vector2f(20.f,20.f));
    player.setPosition(window.getSize().x/2.f, window.getSize().y - player.getLocalBounds().height);

    //create an empty node to use as a scene graph
    Node sceneGraph;

    //set up the number of layers available in the scene graph
    std::array<Node*, layerCount> layers;

    for (std::size_t i = 0; i < layerCount; ++i)
    {
        Node::ptr layer(new Node);
        layers[i] = layer.get();

        sceneGraph.attachChild(std::move(layer));
    }

    sf::Font myfont;
    if(!myfont.loadFromFile("resources/fonts/contb.ttf"))
    {
        std::cerr<<"Could not load font"<<std::endl;
    }

    sf::Text status("Chasing....", myfont);
    status.setPosition(window.getSize().x/2.f, 0.f);
    status.setCharacterSize(32);
    status.setColor(sf::Color(200,100,0,255));

    sf::Time timePerFrame = sf::seconds(1.f/60.f);
    sf::Clock clock;
    sf::Time timeElapsed = sf::Time::Zero;
    sf::Time duration = sf::Time::Zero;

    //spawn and timing
    sf::Clock spawnTime;
    unsigned int squanchCounter = 0;

    while(window.isOpen())
    {
        while(window.pollEvent(e))
        {
            if(e.type == sf::Event::Closed)
            {
                window.close();
                return 0;
            }

            if(e.type == sf::Event::KeyPressed)
            {
                switch(e.key.code)
                {
                case sf::Keyboard::Escape:
                    {
                        window.close();
                        return 0;
                    }
                    break;

                case sf::Keyboard::Left:
                {
                    player.move(-10.f,0.f);
                }
                break;

                case sf::Keyboard::Right:
                {
                    player.move(10.f,0.f);
                }
                break;

                case sf::Keyboard::Up:
                {
                    player.move(0.f,-10.f);
                }
                break;

                case sf::Keyboard::Down:
                {
                    player.move(0.f,10.f);
                }
                break;

                default:
                    break;
                }
            }
        }

        if(spawnTime.getElapsedTime().asSeconds() > 2.f)
        {
            //spawn samsquamptches every 2 seconds
            std::unique_ptr<Samsquamptch> enemy(new Samsquamptch(50.f));
            enemy->setFillColor(sf::Color(0,200,0,255));
            enemy->setSize(30.f,30.f);
            enemy->setPosition(window.getSize().x/2.f,0.f);
            layers[samsquamptches]->attachChild(std::move(enemy));

            squanchCounter++;

            spawnTime.restart();
        }

        window.setTitle("Samsquamptches: " + std::to_string(squanchCounter));

        timeElapsed += clock.restart();
        while (timeElapsed > timePerFrame)
        {
            timeElapsed -= timePerFrame;

            //update sceneGraph
            sceneGraph.update(timePerFrame, player.getPosition(), sf::Vector2f(player.getSize().x,player.getSize().y));
        }

        window.clear();
        window.draw(player);

        //draw sceneGraph
        window.draw(sceneGraph);

        window.display();
    }
    return 0;
}

The main drawback to my specific hoard implementation is that there is a clumping/stacking effect as time marches forward. There are some ways of mitigating this behavior. Obviously, spawning them at different locations on the screen, spawning them on different time intervals (rather than simply every two seconds), and spawning them with variable aggression levels will help. There are some pretty cool solutions to this problem that are more difficult to implement, but the rewards are pretty cool. Check out the Craig Reynolds' site for some neat demonstrations and concepts related to steering behaviors.

You can find the full source for this example on Github. As a note, you will generally need a compiler that supports c++11 features. For me, this means using the TDM GCC (currently at 4.9.2 as of this writing). Personally, I can't live without things like std::to_string and std::unique_ptr now, so having a current compiler is pretty important. It makes your life a lot easier when you don't have to waste your time writing string conversion functions and calling delete on objects for every instance they need to be deleted. When std::unique_ptr goes out of scope, that's it - no messy clean up.