Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Procedurally generated starfield  (Read 14061 times)

0 Members and 1 Guest are viewing this topic.

Draugr

  • Newbie
  • *
  • Posts: 5
    • View Profile
Procedurally generated starfield
« 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!



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
 
« Last Edit: August 21, 2013, 02:56:29 pm by Draugr »

eXpl0it3r

  • SFML Team
  • Hero Member
  • *****
  • Posts: 11030
    • View Profile
    • development blog
    • Email
AW: Procedurally generated starfield
« Reply #1 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. ;)
Official FAQ: https://www.sfml-dev.org/faq.php
Official Discord Server: https://discord.gg/nr4X7Fh
——————————————————————
Dev Blog: https://duerrenberger.dev/blog/

Ixrec

  • Hero Member
  • *****
  • Posts: 1241
    • View Profile
    • Email
Re: Procedurally generated starfield
« Reply #2 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?
« Last Edit: August 21, 2013, 02:28:23 am by Ixrec »

Draugr

  • Newbie
  • *
  • Posts: 5
    • View Profile
Re: Procedurally generated starfield
« Reply #3 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.

Ixrec

  • Hero Member
  • *****
  • Posts: 1241
    • View Profile
    • Email
Re: Procedurally generated starfield
« Reply #4 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.
« Last Edit: August 21, 2013, 02:34:41 am by Ixrec »

Ixrec

  • Hero Member
  • *****
  • Posts: 1241
    • View Profile
    • Email
Re: Procedurally generated starfield
« Reply #5 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);
« Last Edit: August 21, 2013, 02:58:25 am by Ixrec »

Draugr

  • Newbie
  • *
  • Posts: 5
    • View Profile
Re: Procedurally generated starfield
« Reply #6 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.

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Re: Procedurally generated starfield
« Reply #7 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:
  • If you use std::size_t instead of int or sf::Uint16 for sizes, you don't need to cast.
  • You can use nullptr for null pointer literals, instead of 0 or NULL (see std::time() call).
  • Type inference: Replace vector<Star>::iterator with auto.
  • Range-based for: Iterate with for (Star& s : mediumStars) -- is simpler than std::for_each() + lambda, but requires latest compilers.
  • You shouldn't define the default constructor if it can't meaningfully initialize the object.
  • There's no need to define an empty destructor, the compiler will generate it.
  • Even if not necessary, list parameter names also in the function declarations, because they make up the API (the part the programmers and tools look at to know how to use your functionality).
  • Star::getXPos() should be const-qualified.
  • In some places you could simplify code by using vectors instead of two separate coordinates.
  • return; at the end of void functions is useless.
  • It's enough if you use one random engine and two distributions, you won't note any dependency between X and Y.
  • It might be personal style, but parentheses around return statements are not necessary; return is not a function.
  • You should avoid using in headers and specify std:: before all occurrences of the standard library, even if your compiler is not strict here.
  • Instead of <SFML/Graphics.hpp>, you can include specific headers such as <SFML/Graphics/Sprite.hpp>. It's not important in this code, but can be crucial to reduce compile times in bigger projects.
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

Ixrec

  • Hero Member
  • *****
  • Posts: 1241
    • View Profile
    • Email
Re: Procedurally generated starfield
« Reply #8 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 :)


Draugr

  • Newbie
  • *
  • Posts: 5
    • View Profile
Re: Procedurally generated starfield
« Reply #9 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?

eXpl0it3r

  • SFML Team
  • Hero Member
  • *****
  • Posts: 11030
    • View Profile
    • development blog
    • Email
AW: Procedurally generated starfield
« Reply #10 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.

Here's also a preview:

« Last Edit: August 21, 2013, 01:58:02 pm by eXpl0it3r »
Official FAQ: https://www.sfml-dev.org/faq.php
Official Discord Server: https://discord.gg/nr4X7Fh
——————————————————————
Dev Blog: https://duerrenberger.dev/blog/

Draugr

  • Newbie
  • *
  • Posts: 5
    • View Profile
Re: Procedurally generated starfield
« Reply #11 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. :)
« Last Edit: August 21, 2013, 02:37:06 pm by Draugr »

eXpl0it3r

  • SFML Team
  • Hero Member
  • *****
  • Posts: 11030
    • View Profile
    • development blog
    • Email
Re: Procedurally generated starfield
« Reply #12 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.
Official FAQ: https://www.sfml-dev.org/faq.php
Official Discord Server: https://discord.gg/nr4X7Fh
——————————————————————
Dev Blog: https://duerrenberger.dev/blog/

Dreded

  • Newbie
  • *
  • Posts: 3
    • View Profile
Re: Procedurally generated starfield
« Reply #13 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.

Ixrec

  • Hero Member
  • *****
  • Posts: 1241
    • View Profile
    • Email
Re: Procedurally generated starfield
« Reply #14 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?