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
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
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.
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);
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.
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());
}
}