Creating a Parallax Scrolling Starfield with SFML and THOR

You probably already know how to make a parallax scrolling background. But in this tutorial, I'll show you how to implement it in a more flexible way and hopefully give you some ideas on improving and extending it for your own use. This class was created for my Space Invaders clone series.There really isn't a whole lot to it, so let's go ahead and dive into the details. Almost forgot...if you'd like to skip the tutorial and go straight to the source, check out a copy on github.

The definition consists of a constructor that takes two parameters, an update function, and an override of the sf::Drawable::draw() function. Here is the header (starfield.hpp):


#ifndef STARFIELD_HPP
#define STARFIELD_HPP

#include <vector>
#include <random>
#include <SFML/Graphics.hpp>
#include <THOR/Math.hpp>

namespace nasic
{
    class starfield : public sf::Drawable
    {
        public:
            starfield(sf::RenderWindow& window, sf::Uint32 style);
            ~starfield();

            void update(sf::RenderWindow& window, sf::Time dt);
            virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;

            enum starStyle
            {
                smallStars,
                allStars,
                starsAndPlanets
            };

        private:
            sf::CircleShape star;
            std::vector<sf::circleshape> stars;
            static sf::Uint32 m_style;
    };
}

#endif // STARFIELD_HPP

In this class, we inherit from sf::Drawable. We do this because later, when we use this in our games, we will appreciate the clarity and simplicity of the interface. In other words, we will be able to simply say "window.draw(starfield);" instead of the clunkier "starfield.draw(window);" which is less clear and inconsistent with what you normally encounter with stock SFML drawables (i.e. sf::Sprite, sf::RectangleShape, etc.). To do this, you must override the pure virtual function with your own drawing routine. It is really simpler than it sounds, as you'll see in the implementation of the class. Additionally, we have an sf::CircleShape and a std::vector container to hold the shapes we generate. Also, we do some housekeeping with the sf::Uint32 m_style - so that based on the style of starfield we create we can implement and render it accordingly. Here is the implementation (starfield.cpp):


#include <starfield.hpp>

nasic::starfield::starfield(sf::RenderWindow& window, sf::Uint32 style)
{
    //set up starfield background
    using nasic::starfield;
    m_style = style;
    star.setFillColor(sf::Color(255,255,255,255));

    //set up for placing the stars on a uniform distribution
    std::mt19937 engine;
    std::uniform_int_distribution<int> distr(-100, window.getSize().x);
    auto randomizer = std::bind(distr, engine);
    thor::Distribution<int> thorDistr(randomizer);

    //for calculating a disturbance factor for offsetting stars
    //this helps to simulate clumping behavior >.<
    std::uniform_int_distribution<int> disturb(-50, 50);
    auto uniformdisturb = std::bind(disturb, engine);
    thor::Distribution<int> thorDisturb(uniformdisturb);

    //fill the container with stars
    //based on the style passed to the constructor
    std::vector<sf::CircleShape>::iterator starit;

    for(int i=0; i<75; ++i)
    {

        if(m_style == starStyle::allStars || m_style == starStyle::starsAndPlanets)
        {
            float x,y;
            x = thorDistr() + thorDisturb();
            y = thorDistr() + thorDisturb();//addition of disturbance for simulating clumping behavior found throughout the universe ;)

            star.setRadius(5.f);
            star.setFillColor(sf::Color(0, 0, 255, 100));
            star.setPosition(i * star.getGlobalBounds().width + x, i * star.getGlobalBounds().height + y);
            stars.push_back(star);
        }

        for(int j=0; j<75; ++j)
        {
            float x,y;
            x = thorDistr() + thorDisturb();//...again
            y = thorDistr() + thorDisturb();

            if(i == 1 && j == 25)
            {
                if(m_style == starStyle::starsAndPlanets)
                {
                    star.setRadius(50.f);
                    star.setFillColor(sf::Color(149, 69, 53, 255));
                    star.setPosition(i * star.getGlobalBounds().width + x, i * star.getGlobalBounds().height + y);
                    stars.push_back(star);
                }
            }
            else
            {
                star.setRadius(1.f);
                star.setFillColor(sf::Color(255, 255, 255, 255));
                star.setPosition(i * star.getGlobalBounds().width + x, j * star.getGlobalBounds().height + y);
                stars.push_back(star);
            }
        }
    }
}

nasic::starfield::~starfield()
{

}

void nasic::starfield::update(sf::RenderWindow& window, sf::Time dt)
{
    //vary the speeds according to the size
    //of each star to create the illusion of depth
    std::vector<sf::circleshape>::iterator starit;
    for(starit = stars.begin(); starit != stars.end(); ++starit)
    {
        if(starit->getPosition().y < window.getSize().x * 1.2f)
        {
            if(starit->getRadius() == 1.f)
                starit->move(0.f,dt.asSeconds()*5.f);
            else if(starit->getRadius() == 5.f)
                starit->move(0.f,dt.asSeconds()*10.f);
            else if(starit->getRadius() == 50.f)
                starit->move(0.f,dt.asSeconds()*50.f);
        }
        else
        {
            if(starit->getRadius() == 1.f || starit->getRadius() == 5.f)
                starit->setPosition(starit->getPosition().x, 0.f - (window.getSize().y * .1f));

            else if(starit->getRadius() == 50.f)
            {
                //another random generator for resetting position
                //and color of planet
                int randPos = thor::random(-100,(int)window.getSize().x);
                int randR = thor::random(0,255);
                int randG = thor::random(0,255);
                int randB = thor::random(0,255);
                starit->setFillColor(sf::Color(randR,randG,randB,255));
                starit->setPosition(randPos, 0.f - (window.getSize().y * .1f));
            }
        }
    }
}

void nasic::starfield::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
    //consider an additional star class
    //to keep track of z-indices
    //...I'm of the opinion that in this case
    //that would be unnecessarily complex
    //feel free to improve upon it ;)

    //drawing shapes conditionally via separate loops
    //simulates the effects nicely enough and without fuss
    //because there aren't many variations on this theme

    std::vector<sf::CircleShape>::const_iterator starit;
    //draw small stars first...
    for(starit = stars.begin(); starit != stars.end(); ++starit)
    {
        if(starit->getRadius() == 1.f)
            target.draw(*starit, states);
    }

    //...then the larger stars....
    for(starit = stars.begin(); starit != stars.end(); ++starit)
    {
        if(starit->getRadius() == 5.f)
            target.draw(*starit, states);
    }

    //...and finally, the planets
    for(starit = stars.begin(); starit != stars.end(); ++starit)
    {
        if(starit->getRadius() == 50.f)
            target.draw(*starit, states);
    }
}

//instantiate static members
sf::Uint32 nasic::starfield::m_style = nasic::starfield::starStyle::smallStars;

The implementation simply fills the vector full of stars (our sf::CircleShape shapes in this case) based on the style passed to the constructor. If it is passed nasic::starStyle::smallStars, it will fill the vector with small sf::CircleShape shapes that are all white. If you pass in nasic::starStyle::allStars, it will fill it with some small white stars, and some slightly larger blue stars. Lastly, if you pass in nasic::starStyle::starsAndPlanets, you will get a large brownish-red planet in addition to white and blue stars.

Looking closer, you will notice we use a familiar 2-D loop for our stars. You probably encounter loops like this quite often in 2-D game development - no surprises here. Since it is well understood how this works, I won't get into details except to note that the stars are placed on the screen using the THOR math module and std::uniform_int_distribution, which gives the effect that the stars are placed on the screen on a uniform distribution. To aid in giving them a more clumpy realistic-looking distribution, I used the same method to add a distubance factor to each star. The THOR math module has already proven quite useful, even though I have not used many of its features. If you look at this one snippet from the update method:


...
int randPos = thor::random(-100,(int)window.getSize().x);
int randR = thor::random(0,255);
int randG = thor::random(0,255);
int randB = thor::random(0,255);
...

...you will notice immediately that we were able to generate random integers for both the planet position and its color without creating separate calls to srand(time(NULL)) and rand() for EACH instance where we want a random integer. How useful is that? You can find more great c++ as well as SFML-specific awesomeness in the SFML book. Also note the use in the draw method of const_iterator - this is absolutely necessary in overriding the draw function to draw elements from any c++ container. Remember, we are promising through draw(sf::RenderTarget& target, sf::RenderStates states) const that we are not going to alter the elements inside the vector - we are just going to draw them. const_iterator ALWAYS returns a const pointer, and is unmodifiable, which aligns with the policy of the draw function.

The update method simply moves the stars down the screen and resets them above, but outside, of the window for a seamless appearance. It does so according to the size of the star, simulating a depth-of-field effect and adding some realism to the star field. Here is a minimal complete example, which shows how we can generate, update, and draw the starfield with only three lines of code in addition to our SFML boilerplate:


#include <SFML/Graphics.hpp>
#include <starfield.hpp>

int main()
{
    sf::RenderWindow window(sf::VideoMode(800,600,32), "Starfield Example", sf::Style::Default);

    nasic::starfield stars(window, nasic::starfield::starStyle::starsAndPlanets);
    sf::Clock clock;
    sf::Time dt;

    sf::Event e;
    bool running = true;
    while(running)
    {
        while(window.pollEvent(e))
        {
            if(e.type == sf::Event::Closed)
            {
                window.close();
                return 0;
            }
        }
            dt = clock.restart();

            stars.update(window, dt);
            window.clear();
            window.draw(stars);
            window.display();
    }
    return 0;
}

The stack order of the shapes could be better implemented using a separate star class which does some housekeeping on the z-index. Additionally, you may notice a bit of lag from producing and updating, on the order of,  5,000+ stars using this method. There may well be more efficient ways to do this. However, if you pass in a fixed dt, you should not experience too much, if any, lag. If you aren't sure how to implement a fixed time step, that's just another reason to buy the SFML book - it will certainly help the SFML community and keep folks like Laurent, Jan, Artur, and Henrik producing great code. Have fun with it and experiment.