SFML community forums

General => SFML projects => Topic started by: Draugr on August 21, 2013, 01:51:21 am

Title: Procedurally generated starfield
Post by: Draugr on August 21, 2013, 01:51:21 am
Hello all,

for a vertical shoot 'em up game I created a scrolling background with stars. I wanted it to have a parallax effect with differently sized stars, scrolling in different speed. I also wanted it to be created procedurally and completely random since I intend to expand it to be more beautiful in my game in the end (with procedurally created galaxies and stuff). It uses C++11 stuff from <random> and Lambdas, so you will have to enable it for your compiler. Asides from this the project does not use any files so you can just paste and compile the code. :)

I post this for anyone to use and, more importantly, to get comments on how it is done. This is my first try with sfml, so please tell me if I use something wrong or in a way that causes bad performance.

Thanks for any comments and best regards
Draugr

Here's a video preview - thanks to eXpl0it3r!

http://www.youtube.com/watch?feature=player_embedded&v=72F_YPXQxAI (http://www.youtube.com/watch?feature=player_embedded&v=72F_YPXQxAI)

Here comes the code:

#include <SFML/Graphics.hpp>
#include "Starfield.hpp"

int main() {

        sf::Vector2u screenDimensions(800, 600);
        sf::RenderWindow window(sf::VideoMode(screenDimensions.x, screenDimensions.y, 32), "Game main window", sf::Style::Titlebar | sf::Style::Close);

        window.setSize(screenDimensions);
        window.setFramerateLimit(60);

        //create an empty black image onto which the starfield will be painted every frame
        sf::Image starsImage;
        starsImage.create(screenDimensions.x, screenDimensions.y, sf::Color::Black);

        sf::Texture starsTexture;
        starsTexture.loadFromImage(starsImage);
        starsTexture.setSmooth(false);

        sf::Sprite starsSprite;
        starsSprite.setTexture(starsTexture);
        starsSprite.setPosition(0, 0);

        Starfield backgroundStars(screenDimensions.x, screenDimensions.y);

        //Game loop
        while (window.isOpen()) {
                sf::Event event;

                while (window.pollEvent(event)) {

                    switch (event.type) {
                        case sf::Event::Closed:
                            window.close();
                            break;

                        //Keypress related events
                        case sf::Event::KeyPressed:
                            if(event.key.code == sf::Keyboard::Escape || event.key.code == sf::Keyboard::Return){
                                window.close();
                            }
                            break;

                        default:
                            break;
                    }
                }

                starsTexture.loadFromImage(starsImage);
                backgroundStars.drawStarfield(starsTexture);

                window.clear(sf::Color(0, 0, 0));
                window.draw(starsSprite);
                window.display();

                backgroundStars.updateStarfield();
        }

        return 0;

}
 

#ifndef STARFIELD_H
#define STARFIELD_H

#include "Star.hpp"
#include <SFML/Graphics.hpp>
#include <random>
#include <vector>
#include <ctime>

using std::vector;

class Starfield
{
    public:
        Starfield();
        Starfield(int, int);
        ~Starfield() {}

        void updateStarfield();
        void drawStarfield(sf::Texture&);

    protected:

        int maxSmallStars;
        int maxMediumStars;
        int maxLargeStars;

        sf::Uint16 x_Size;
        sf::Uint16 y_Size;

        vector<Star> smallStars;
        vector<Star> mediumStars;
        vector<Star> largeStars;

        std::default_random_engine re_x;
        std::default_random_engine re_y;
        std::uniform_int_distribution<int> my_distribution_x;
        std::uniform_int_distribution<int> my_distribution_y;

        sf::Image smallStarImage;
        sf::Image mediumStarImage;
        sf::Image largeStarImage;
};

//starfield won't work without proper data
Starfield::Starfield(): maxSmallStars(0), maxMediumStars(0), maxLargeStars(0), x_Size(800), y_Size(600)
{
}

Starfield::Starfield(int xResolution, int yResolution)
{

    x_Size = xResolution;
    y_Size = yResolution;

    //size of the different star sizes in pixels
    sf::Uint16 smallSize = 1;
    sf::Uint16 mediumSize = 2;
    sf::Uint16 largeSize = 4;

    //create the images that will be used to update the background texture
    smallStarImage.create(smallSize, smallSize, sf::Color::White);
    mediumStarImage.create(mediumSize, mediumSize, sf::Color::White);
    largeStarImage.create(largeSize, largeSize, sf::Color::White);

    //init random generator
    my_distribution_x = std::uniform_int_distribution<int>(0, xResolution);
    my_distribution_y = std::uniform_int_distribution<int>(0, yResolution);

    re_x.seed(std::time(0));
    re_y.seed(std::time(0)+24);

    //The higher reduceStars the fewer stars; classDifference sets the proportionality between small, medium and large stars. The higher the number, the fewer stars in each larger class.
    int reduceStars = 8;
    int classDifference = 3;

    maxSmallStars = (xResolution / (reduceStars * 10)) * (yResolution / reduceStars);
    maxMediumStars = (xResolution / (reduceStars * 10 * classDifference)) * (yResolution / (reduceStars * classDifference));
    maxLargeStars = (xResolution / (reduceStars * 10 * classDifference * classDifference)) * (yResolution / (reduceStars * classDifference * classDifference));

    //generate a start set of stars
    while((int)smallStars.size() <= maxSmallStars){
        smallStars.push_back(Star(my_distribution_x(re_x), my_distribution_y(re_y)));
    }

    while((int)mediumStars.size() <= maxMediumStars){
        mediumStars.push_back(Star(my_distribution_x(re_x), my_distribution_y(re_y)));
    }

    while((int)largeStars.size() <= maxLargeStars){
        largeStars.push_back(Star(my_distribution_x(re_x), my_distribution_y(re_y)));
    }
}

void Starfield::updateStarfield()
{

    //remove all stars that have exceeded the lower screen border
    smallStars.erase(remove_if(smallStars.begin(), smallStars.end(), [&](Star& p_Star){
        return (p_Star.getYPos() > y_Size);
    }
    ), smallStars.end());

    mediumStars.erase(remove_if(mediumStars.begin(), mediumStars.end(), [&](Star& p_Star){
        return (p_Star.getYPos() > y_Size);
    }
    ), mediumStars.end());

    largeStars.erase(remove_if(largeStars.begin(), largeStars.end(), [&](Star& p_Star){
        return (p_Star.getYPos() > y_Size);
    }
    ), largeStars.end());

    //move every star, according to its size to create a parallax effect
    for_each(smallStars.begin(), smallStars.end(), [&](Star& p_Star){
        p_Star.addYPos(1);
    }
    );

    for_each(mediumStars.begin(), mediumStars.end(), [&](Star& p_Star){
        p_Star.addYPos(2);
    }
    );

    for_each(largeStars.begin(), largeStars.end(), [&](Star& p_Star){
        p_Star.addYPos(4);
    }
    );

    //create new stars until the set limit is reached
    while((int)smallStars.size() <= maxSmallStars){
        smallStars.push_back(Star(my_distribution_x(re_x), 0));
    }

    while((int)mediumStars.size() <= maxMediumStars){
        mediumStars.push_back(Star(my_distribution_x(re_x), 0));
    }

    while((int)largeStars.size() <= maxLargeStars){
        largeStars.push_back(Star(my_distribution_x(re_x), 0));
    }
}

//update a target texture with all stars contained in this starfield
void Starfield::drawStarfield(sf::Texture& p_Texture)
{
    for(vector<Star>::iterator it = smallStars.begin(); it != smallStars.end(); ++it){
        p_Texture.update(smallStarImage, it->getXPos(), it->getYPos());
    }

    for(vector<Star>::iterator it = mediumStars.begin(); it != mediumStars.end(); ++it){
        p_Texture.update(mediumStarImage, it->getXPos(), it->getYPos());
    }

    system("clear");
    for(vector<Star>::iterator it = largeStars.begin(); it != largeStars.end(); ++it){
        p_Texture.update(largeStarImage, it->getXPos(), it->getYPos());
    }
}
#endif // STARFIELD_H
 

#ifndef STAR_H
#define STAR_H

#include <SFML/Graphics.hpp>

class Star
{
    public:
        Star() {}
        Star(sf::Uint16, sf::Uint16);
        ~Star() {}
        sf::Uint16 getXPos();
        sf::Uint16 getYPos() const;
        void setXPos(sf::Uint16);
        void setYPos(sf::Uint16);
        void addYPos(sf::Uint16);

    private:
        sf::Uint16 xPos;
        sf::Uint16 yPos;
};

Star::Star(sf::Uint16 p_X_Pos, sf::Uint16 p_Y_Pos)
{
    xPos = p_X_Pos;
    yPos = p_Y_Pos;
}

sf::Uint16 Star::getXPos()
{
    return xPos;
}

sf::Uint16 Star::getYPos() const
{
    return yPos;
}

void Star::setXPos(sf::Uint16 x)
{
    xPos = x;
    return;
}

void Star::setYPos(sf::Uint16 y)
{
    yPos = y;
    return;
}

void Star::addYPos(sf::Uint16 y)
{
    yPos += y;
    return;
}
#endif // STAR_H
 
Title: AW: Procedurally generated starfield
Post by: eXpl0it3r on August 21, 2013, 02:20:20 am
Oh the looks nice! :)
As a heads up though, the implementation of a class does not belong into the header, but should be split into a source file. Do you got the header with the decleration and the source file with the definition/implementation. ;)
Title: Re: Procedurally generated starfield
Post by: Ixrec on August 21, 2013, 02:20:34 am
This looks really cool but unfortunately it doesn't seem to work for me at the moment.  I'm using MSVC++ 2010 Express.

1) You need to add std:: in front of the remove_if and for_each algorithms.

2) The p_Texture.update() call in Starfield::drawStarfield() crashes in certain cases, specifically on {xPos=477 yPos=600}, and I think this is related to 600 being the height of the window because of the console error message I get:
Assertion failed: y + height <= m_size.y, file ... Texture.cpp, line 325

Edit: Tried changing the random distributions to avoid that edge case.  Now instead of crashing instantly the program scrolls nicely for a fraction of a second and then crashes after one of the stars gets too low.

3) The system("clear"); command is "not recognized" on my system.  What is it supposed to do anyway?
Title: Re: Procedurally generated starfield
Post by: Draugr on August 21, 2013, 02:27:58 am
Thanks for the comments :)

I'm developing on Linux and compiling with gcc. Guess I'll have to get MSVC on Windows and try it to learn a bit about keeping the code portable.

The code is compiling for me exactly as I posted it, so I'm not sure if that with remove_if and for_each is another MS related thing.

I haven't had any crashes so far, even when keeping it running for 10 minutes, so I'm not sure why it crashes in some cases. Any further help or comments would be very much appreciated.

Also a question: is there really a good reason for keeping declaration and definition separated in two files? I find it annoying when I have to switch between files and haven't found a good reason so far.
Title: Re: Procedurally generated starfield
Post by: Ixrec on August 21, 2013, 02:30:23 am
I think I got it working fine on my system now.  The workaround was fairly simple.

void Starfield::drawStarfield(sf::Texture& p_Texture)
{
    for(vector<Star>::iterator it = smallStars.begin(); it != smallStars.end(); ++it){
                if(it->getYPos() < 600)
        p_Texture.update(smallStarImage, it->getXPos(), it->getYPos());
    }

    for(vector<Star>::iterator it = mediumStars.begin(); it != mediumStars.end(); ++it){
                if(it->getYPos() < 599)
        p_Texture.update(mediumStarImage, it->getXPos(), it->getYPos());
    }

    for(vector<Star>::iterator it = largeStars.begin(); it != largeStars.end(); ++it){
                if(it->getYPos() < 597)
        p_Texture.update(largeStarImage, it->getXPos(), it->getYPos());
    }
}

Edit: No crashes for a while, so this definitely fixed it.  Apparently you can update() outside the texture boundaries on Linux but not on Windows?

Edit2: Yep: http://sfml-dev.org/documentation/2.0/classsf_1_1Texture.php#a87f916490b757fe900798eedf3abf3ba
Quote
No additional check is performed on the size of the image, passing an invalid combination of image size and offset will lead to an undefined behaviour.
Title: Re: Procedurally generated starfield
Post by: Ixrec on August 21, 2013, 02:43:30 am
An equivalent and probably better fix I found is changing updateStarfield() like this:

1) I put the move block above the remove block.
2) In the remove comparisons I used y_Size minus the size of the star instead of just y_Size.

//move every star, according to its size to create a parallax effect
    std::for_each(smallStars.begin(), smallStars.end(), [&](Star& p_Star){
        p_Star.addYPos(1);
    }
    );

    std::for_each(mediumStars.begin(), mediumStars.end(), [&](Star& p_Star){
        p_Star.addYPos(2);
    }
    );

    std::for_each(largeStars.begin(), largeStars.end(), [&](Star& p_Star){
        p_Star.addYPos(4);
    }
    );

    //remove all stars that have exceeded the lower screen border
    smallStars.erase(std::remove_if(smallStars.begin(), smallStars.end(), [&](Star& p_Star){
        return (p_Star.getYPos() > y_Size-1);
    }
    ), smallStars.end());

    mediumStars.erase(std::remove_if(mediumStars.begin(), mediumStars.end(), [&](Star& p_Star){
        return (p_Star.getYPos() > y_Size-2);
    }
    ), mediumStars.end());

    largeStars.erase(std::remove_if(largeStars.begin(), largeStars.end(), [&](Star& p_Star){
        return (p_Star.getYPos() > y_Size-4);
    }
    ), largeStars.end());
 

Admittedly, for your long-term plans you probably want a way to draw "partial stars" at the bottom of the screen (unless your galaxies will only be 4x4 pixels).  For that I think your only choice is making the texture taller so you can still draw entirely on the texture even when the window appears to be cutting them off.

Edit: I mean making the Image taller, not the texture.  Specifically, adding this +4 in the main file is the best fix I've found so far:

starsImage.create(screenDimensions.x, screenDimensions.y+4, sf::Color::Black);
Title: Re: Procedurally generated starfield
Post by: Draugr on August 21, 2013, 10:34:35 am
Thanks for the input, good points :)

I think making the image taller will be hard when I want to add various space objects of which I do not yet know the size. Currently I think the best general strategy is to define each objects size and use it to specify the range in which a new object of that type will be placed. E.g. large stars with a horizontal size of 4 pixels will only be created in the range of [0, 596].

Thanks a lot for pointing out that updating over the edges is undefined behaviour. Checking the space for updating images is pretty unconvenient, though. I remember using directX about 10 years ago where you could define masks for this purpose where everything overlapping the defined area was simply cut off. I guess this was the reason why I assumed this would work.
Title: Re: Procedurally generated starfield
Post by: Nexus on August 21, 2013, 10:49:42 am
Is there a screenshot? So that not everbody must compile the code to see how it looks :)

I like that you're using the STL, RAII and C++11 functionality! Your code looks quite good, maybe some smaller points that you could still improve:
Title: Re: Procedurally generated starfield
Post by: Ixrec on August 21, 2013, 10:59:02 am
Is there a screenshot? So that not everbody must compile the code to see how it looks :)

(http://gyazo.com/a9a1bfd1b1ae6a6fcd6b8d28c6d2acc1.png)
Title: Re: Procedurally generated starfield
Post by: Draugr on August 21, 2013, 12:16:41 pm
Thanks a lot for the useful hints Nexus, I'm in the middle of finding back into C++ right now, so exactly such code reviews is what I need :)

Also thanks for posting the screenshot, even though the primary aim - the parallax effect - does not get over. I will try to record a video and put it on youtube, to embed it here or post the link.

Anyone knows a good Linux screen capture tool?
Title: AW: Procedurally generated starfield
Post by: eXpl0it3r on August 21, 2013, 12:33:36 pm
Unfortunately there's nothing like Open Broadcast Streamer for Linux, thus people mostly use FFMPEG directly. E.g. like this (http://stefan.boxbox.org/2012/08/29/screen-casting-in-linux-using-ffmpeg/).

Here's also a preview:

http://www.youtube.com/watch?v=72F_YPXQxAI
Title: Re: Procedurally generated starfield
Post by: Draugr on August 21, 2013, 02:31:30 pm
Big thanks for adding the video preview, I added it to the opening post. I took one with FFMPEG, but didn't have anything to cut the video. :)
Title: Re: Procedurally generated starfield
Post by: eXpl0it3r on August 21, 2013, 02:45:28 pm
Big thanks for adding the video preview, I added it to the opening post. I took one with FFMPEG, but didn't have anything to cut the video. :)
If you post the link with http:// in front, the forum software will embed the video.
Title: Re: Procedurally generated starfield
Post by: Dreded on September 01, 2013, 02:21:23 am
This is an amazing effect, when I compiled it on my system(minigw on windows) I could not get it to run...

after applying the following "fix"
Quote
void Starfield::drawStarfield(sf::Texture& p_Texture)
{
    for(vector<Star>::iterator it = smallStars.begin(); it != smallStars.end(); ++it){
        if(it->getYPos() < 600)
        p_Texture.update(smallStarImage, it->getXPos(), it->getYPos());
    }

    for(vector<Star>::iterator it = mediumStars.begin(); it != mediumStars.end(); ++it){
        if(it->getYPos() < 599)
        p_Texture.update(mediumStarImage, it->getXPos(), it->getYPos());
    }

    for(vector<Star>::iterator it = largeStars.begin(); it != largeStars.end(); ++it){
        if(it->getYPos() < 597)
        p_Texture.update(largeStarImage, it->getXPos(), it->getYPos());
    }
}
if I compiled several times it might run 1/5 times without an immediate crash but then would crash 2 seconds later but the program ran successfully in these 2 seconds.. also applying the other fix mentioned had no effect.
Title: Re: Procedurally generated starfield
Post by: Ixrec on September 01, 2013, 02:30:06 am
Since you're compiling it yourself anyway, could you run it in debug mode so you can tell him the exact error and where it happens?
Title: Re: Procedurally generated starfield
Post by: Dreded on September 01, 2013, 02:34:33 am
Sorry meant to include the error...
Assertion failed: x + width <= m_size.x, file D:\developpement\sfml\sfml\src\SFML\Graphics\Texture.cpp, line 324
Title: Re: Procedurally generated starfield
Post by: Ixrec on September 01, 2013, 03:02:47 am
Wait, you're getting the error on the x axis?  Wtf...

Well I guess you can try this:
starsImage.create(screenDimensions.x+4, screenDimensions.y, sf::Color::Black);

If that doesn't work try changing my other hotfixes so they apply to the x axis instead of the y axis.
Title: Re: Procedurally generated starfield
Post by: paxsentry on June 04, 2017, 08:36:18 pm
Just a little addition, these guards can help you to run just fine (windows build)

void Starfield::drawStarField(sf::Texture& texture)
{
    for (auto it = smallStars.begin(); it != smallStars.end(); ++it)
    {
        auto xCoord = it->getXPos();
        auto yCoord = it->getYPos();

        if ((xCoord > 0 && xCoord < 1280) && (yCoord > 0 && yCoord < 960))
            texture.update(smallStarImage, it->getXPos(), it->getYPos());
    }

    for (auto it = mediumStars.begin(); it != mediumStars.end(); ++it)
    {
        auto xCoord = it->getXPos();
        auto yCoord = it->getYPos();

        if ((xCoord > 0 && xCoord < 1279) && (yCoord > 0 && yCoord < 959))
            texture.update(mediumStarImage, it->getXPos(), it->getYPos());
    }

    for (auto it = largeStars.begin(); it != largeStars.end(); ++it)
    {
        auto xCoord = it->getXPos();
        auto yCoord = it->getYPos();

        if ((xCoord > 0 && xCoord < 1276) && (yCoord > 0 && yCoord < 956))
            texture.update(largeStarImage, it->getXPos(), it->getYPos());
    }
}