Tween
is not a very good name. How about Snowflake
? And why is it a struct
and exposing all of its implementation details. I would personally place it in a class, initialize its values via constructor, and give it a public function like fall()
or something to update it each frame.
class Snowflake
{
public:
void fall();
private:
sf::CircleShape snowflake;
float begin;
float change;
float time{ 0 };
float duration;
float current() const
{
return begin + change * easeInOutQuad(time / duration);
}
};
By putting the circle in the class you can update it internally. More on all that later.
float easeInOutQuad(float normalisedTime)
{
auto & t = normalisedTime;
return t < 0.5 ? 2*t*t : 2*t*(2 - t) - 1;
}
It seems a little unusual to assign a reference to the argument and then return the reference. I'm actually a little surprised this didn't boom. it's a trivially copied float
. Just pass it by value.
auto const inverseFramerate = 1.f / 60;
window.setFramerateLimit(static_cast<unsigned>(1 / inverseFramerate));
That is an odd way to write
window.setFramerateLimit(60);
Now you could replace the magic number 60 that I just passed here with a named constant but I would actually argue that this is one of those rare cases when you shouldn't. This will be the only call to this function and the name won't be more readable here. Most readers are likely to immediately recognize that the 60 is setting 60 fps.
No cast. No equation. easily readable. less lines.
auto const numberOfSnowflakes = 200;
Tween tweens[numberOfSnowflakes];
float yPositions[numberOfSnowflakes];
This is a much more useful named constant. However. Don't use C array[]
s. std::array
or std::vector
are better C++ containers. std::array
is static sized like the C-style array[]
and std::vector
is dynamically sized. Don't forget to #include
whichever you choose to use. You should keep the constant but switch to the more preferred constexpr
constexpr int number_of_snowflakes = 200;
auto generator = std::mt19937{};
A lot of your difficult to read code revolves around your use and misuse of this object. Allow me to show you a slightly easier to use way of handling PRNG. I'd wrap it all into its own named function and do away with the two harder to read and use lambdas. Then inside the function you seed the generator and declare it static
so it doesn't get destroyed when it goes out of scope.
//we take unsigned and return float in order to work with SFML's window size which is unsigned
float generateRandom(unsigned min, unsigned max)
{
float real_min = static_cast<float>(min);
float real_max = static_cast<float>(max);
std::random_device rd;
static const std::mt19937 generator(rd());
std::uniform_real_distribution<float> distribution(real_min, real_max);
return distribution(generator);
}
Now when you want a value between 0 - width you do
start = generateRandom(0, window.getSize().x)
This is a weird reimplementation of uniform_real_distribution
. You did it so you could work with negative ranges because you want your snowflakes to start off-screen. But all you need is an offset applied after the fact.
auto randomBetween = [&generator](float min, float max)
{
return std::generate_canonical<float, 10>(generator)*(max - min) + min;
};
Now we get to initialization. In the future it is best to declare and initialize variables together. If you need helper functions have them defined beforehand and ready to use rather than defining them in between instantiation and initialization.
for (auto i = 0; i < numberOfSnowflakes; ++i)
{
tweens[i].begin = randomBetween(0, window.getSize().x);
tweens[i].change = randomChangeInX();
tweens[i].duration = randomBetween(1, 5);
yPositions[i] = randomBetween(-20, window.getSize().y);
}
auto const defaultRadius = 5.f;
auto circle = sf::CircleShape{};
Most of this can be eliminated by doing two things. Move the sf::Circle
into our new Snowflake
class and we use default initialization within the class. You can also be completely rid of the second yPositions
array as the Snowflake
class circle can maintain its own position. You will have to pass the starting coordinates to the constructor because the Snowflake
class won't have full access to the sf::RenderWindow
class. Our class is now starting to look like this:
class Snowflake
{
public:
explicit Snowflake(sf::Vector2f start_position);
void fall();
private:
float change{ generateRandom(1, 10) };
float duration{ generateRandom(1, 5) };
float time{ 0 };
sf::Vector2f position;
sf::CircleShape snowflake{ default_radius / duration };
float current() const
{
return position.x + change * easeInOutQuad(time / duration);
}
};
You can do the initialization like so:
std::vector<Snowflake> snowflakes;
snowflakes.reserve(number_of_snowflakes);
for (int i = 0; i < number_of_snowflakes; ++i)
{
float x = generate_random(0, window.getSize().x);
float y = generate_random(-20, window.getSize().y);
sf::Vector2f start_position(x, y);
snowflakes.emplace_back(Snow(start_position));
}
This will give you a nice standard container of snowflakes at default initialized
So now we get to your animation loop. I see now why you did inverseFramerate
now, but it was a complicated way of having your snow fall at varying rates. As you have moved everything into the Snowflake
class you can have a much simpler animation loop.
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
{
window.close();
}
}
for (auto&& snow : snowflakes)
{
snow.fall(window.getSize());
}
window.clear();
for (auto&& snow : snowflakes)
{
snow.draw(window);
}
window.display();
}
All that leaves us with is implementing the fall()
and draw()
methods from above. draw()
is simple and would look something like this:
void draw(sf::RenderWindow& window) { window.draw(snowflake); }
and the fall()
method I just copied over your implementation details using my new class member variables. It looks something like this:
Put it all together and it would look like this:
#include <random>
#include <vector>
#include <SFML/Graphics.hpp>
constexpr int number_of_snowflakes = 200;
constexpr float single_frame = 1.F / 60;
constexpr float default_radius = 5.F;
// Maps input in the range [0..1] to an output in the range [0..1].
// Represents the piecewise function:
// y(x) = 2*x^2 when x < 0.5
// = -2*x^2 + 4*t - 1 when x >= 0.5
float ease_in_and_out(float normalised_time)
{
float t = normalised_time;
return t < 0.5 ? 2 * t*t : 2 * t*(2 - t) - 1;
}
//we take unsigned and return float in order to work with SFML's window size which is unsigned
float generate_random(unsigned min, unsigned max)
{
float real_min = static_cast<float>(min);
float real_max = static_cast<float>(max);
static std::random_device rd;
static std::mt19937 generator(rd());
std::uniform_real_distribution<float> distribution(real_min, real_max);
return distribution(generator);
}
class Snowflake
{
public:
explicit Snowflake(sf::Vector2f start_position) : position{ start_position }
{
snowflake.setPosition(position);
begin = start_position.x;
}
void fall(sf::Vector2u window_size)
{
time += single_frame;
if (time > duration)
{
begin += change;
change = generate_random(1, 10);
time = 0.F;
}
position.y = snowflake.getPosition().y;
position.y += 1 / duration;
if (position.y > window_size.y)
{
position.y = -10.F;
begin = generate_random(0, window_size.x);
change = generate_random(1, 10);
time = 0;
snowflake.setRadius(default_radius / duration);
}
position.x = shift_x();
snowflake.setPosition(position);
}
void draw(sf::RenderWindow& window) { window.draw(snowflake); }
private:
sf::Vector2f position;
float begin;
float change{ generate_random(1, 10) };
float duration{ generate_random(1, 5) };
float time{ 0 };
sf::CircleShape snowflake{ default_radius / duration };
float shift_x() const
{
return begin + change * ease_in_and_out(time / duration);
}
};
int main()
{
sf::RenderWindow window{ sf::VideoMode{500, 500}, "Snow" };
window.setFramerateLimit(60);
std::vector<Snowflake> snowflakes;
snowflakes.reserve(number_of_snowflakes);
for (int i = 0; i < number_of_snowflakes; ++i)
{
float x = generate_random(0, window.getSize().x);
float y = generate_random(0, window.getSize().y) - 20.F;
sf::Vector2f start_position(x, y);
snowflakes.emplace_back(Snowflake(start_position));
}
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
{
window.close();
}
}
for (auto&& snow : snowflakes)
{
snow.fall(window.getSize());
}
window.clear();
for (auto&& snow : snowflakes)
{
snow.draw(window);
}
window.display();
}
}
Could this still be improved? Sure. I left a few magic numbers behind, and we all know those are bad. The names are more descriptive and better describe what is happening but they could probably be improved. SFML allows for classes to inherit from sf::Drawable
and you can fix the syntax so it can be called window.draw(Snowflake);
instead of passing a reference to the window object to your class. I would also probably extract the fall()
method to a couple private helpers. This would
- hide the implementation details of the public method that don't need to be exposed.
- allow you to simplify some of the functionality. (There's a lot of resetting values to default.)
ctrl-a
to select all the click on the{}
on the top of the editor or pressctrl-k
to have the editor format for you. \$\endgroup\$