Skip to content

Commit

Permalink
naming updates and some micro-optimizations
Browse files Browse the repository at this point in the history
  • Loading branch information
mrdcvlsc committed Oct 26, 2023
1 parent a6b918e commit 8511b2c
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 136 deletions.
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@

A Flappy Bird like game that uses a feedforward neural network to play the game, and a genetic algorithm to learn how to play the game.

Anyone can [download the released executables](https://github.com/mrdcvlsc/flappy-ffnn-ga/releases), extract it and run the program, these release executables only supports x86-64 or 64-bit systems: [DOWNLOAD HERE](https://github.com/mrdcvlsc/flappy-ffnn-ga/releases)
Anyone can [download the released executables (click here to download)](https://github.com/mrdcvlsc/flappy-ffnn-ga/releases) and extract it then run the program, these release executables are only supported on x86-64 or 64-bit systems.

In windows, one might need to install [Microsoft Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022) in their system if the release versions of the executables is not working.
In windows, one might need to install the [Microsoft Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022) in their system if the release versions of the executables is not working.
18 changes: 9 additions & 9 deletions src/bird.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ sf::Vector2f Bird::target_gap = {0.f, 0.f};
std::uniform_int_distribution<size_t> Bird::rand_color(0, 255);

Bird::Bird() :
sf::RectangleShape({SIZE, SIZE}),
sf::RectangleShape({SIZE_N, SIZE_N}),
neural_net(),
time_lived(0.f),
lifetime(0.f),
speed(JUMP_SPEED * 0.6f),
fitness(0.f),
dead(false)
Expand All @@ -35,9 +35,9 @@ void Bird::jump()
void Bird::update(float dt)
{
if (!dead) {
time_lived += dt;
lifetime += dt;

if (getPosition().y < Bird::SIZE * 0.5f) {
if (getPosition().y < Bird::SIZE_N * 0.5f) {
speed = GRAVITY * 0.04f;
}

Expand All @@ -53,9 +53,9 @@ void Bird::update(float dt)

void Bird::reset()
{
time_lived = 0.f;
speed = JUMP_SPEED * 0.6f;
setPosition(START_X, START_Y);
lifetime = 0.f;
speed = JUMP_SPEED * 0.6f;
dead = false;
}

Expand All @@ -74,9 +74,9 @@ void Bird::become_offspring(Bird const &b1, Bird const &b2)

/////////////////////// Birds ///////////////////////

Birds::Birds() : collection(), population(Birds::INITIAL_POPULATION)
Birds::Birds() : collection(), population(Birds::MAX_POPULATION)
{
for (size_t i = 0; i < INITIAL_POPULATION; ++i) {
for (size_t i = 0; i < MAX_POPULATION; ++i) {
collection.push_back(Bird());
}

Expand All @@ -89,7 +89,7 @@ void Birds::reset()
bird.reset();
}

population = INITIAL_POPULATION;
population = MAX_POPULATION;
}

void Birds::update(float dt)
Expand Down
8 changes: 4 additions & 4 deletions src/bird.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

struct Bird : public sf::RectangleShape
{
/// Default bird side dimension length (SIZE x SIZE).
static constexpr float SIZE = 20.f;
/// Default bird side dimension length (SIZE_N x SIZE_N).
static constexpr float SIZE_N = 20.f;

/// Starting x position of the bird in the 2D world.
static constexpr float START_X = static_cast<float>(WINDOW_WIDTH) / 5.f;
Expand All @@ -38,7 +38,7 @@ struct Bird : public sf::RectangleShape
static std::uniform_int_distribution<size_t> rand_color;

FFNN neural_net;
float time_lived;
float lifetime;
float speed;
float fitness;
bool dead;
Expand All @@ -54,7 +54,7 @@ struct Bird : public sf::RectangleShape

struct Birds : public sf::Drawable
{
static constexpr size_t INITIAL_POPULATION = 512;
static constexpr size_t MAX_POPULATION = 512;

std::vector<Bird> collection;
size_t population;
Expand Down
8 changes: 4 additions & 4 deletions src/collision.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ size_t bird_collision(Bird &bird, Pipes const &pipes)
bool hit_top_pipe1 = false, hit_btm_pipe1 = false;
bool hit_top_pipe2 = false, hit_btm_pipe2 = false;

sf::FloatRect top_pipe1_bounds = pipes.pairs[pipes.front_pipe].top.getGlobalBounds();
sf::FloatRect btm_pipe1_bounds = pipes.pairs[pipes.front_pipe].btm.getGlobalBounds();
sf::FloatRect top_pipe1_bounds = pipes.pairs[pipes.front_idx].top.getGlobalBounds();
sf::FloatRect btm_pipe1_bounds = pipes.pairs[pipes.front_idx].btm.getGlobalBounds();

sf::FloatRect top_pipe2_bounds = pipes.pairs[(pipes.front_pipe + 1) % Pipes::COUNT].top.getGlobalBounds();
sf::FloatRect btm_pipe2_bounds = pipes.pairs[(pipes.front_pipe + 1) % Pipes::COUNT].btm.getGlobalBounds();
sf::FloatRect top_pipe2_bounds = pipes.pairs[(pipes.front_idx + 1) % Pipes::COUNT].top.getGlobalBounds();
sf::FloatRect btm_pipe2_bounds = pipes.pairs[(pipes.front_idx + 1) % Pipes::COUNT].btm.getGlobalBounds();

sf::FloatRect bird_bounds = bird.getGlobalBounds();

Expand Down
6 changes: 3 additions & 3 deletions src/ffnn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ float FFNN::feedforward()
return output_layer(0, 0);
}

void FFNN::update_inputs(float bird_pipe_distance, float bird_gap_distance)
void FFNN::update_inputs(float pipe_x_distance, float gap_y_distance)
{
input_layer(0, 0) = bird_pipe_distance;
input_layer(1, 0) = bird_gap_distance;
input_layer(0, 0) = pipe_x_distance;
input_layer(1, 0) = gap_y_distance;
}

void FFNN::mutate()
Expand Down
2 changes: 1 addition & 1 deletion src/ffnn.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ struct FFNN

float feedforward();

void update_inputs(float bird_pipe_distance, float bird_gap_distance);
void update_inputs(float pipe_x_distance, float gap_y_distance);

/// \brief randomly mutate the current weights.
void mutate();
Expand Down
71 changes: 35 additions & 36 deletions src/gamestats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,73 @@
const sf::Time GameStats::TIME_PER_FRAME = sf::seconds(1.f / static_cast<float>(GameStats::FRAME_LIMIT));

GameStats::GameStats() :
game_clock(),
fps_clock(),
timeSinceLastUpdate(sf::Time::Zero),
clock_game(),
clock_fps(),
update_time_elapsed(sf::Time::Zero),
generation(1u),
current_population(Birds::INITIAL_POPULATION),
total_population(Birds::INITIAL_POPULATION),
population(Birds::MAX_POPULATION),
fps(0.f)
{
if (!m_font.loadFromFile("calibril.ttf")) {
if (!font_style.loadFromFile("calibril.ttf")) {
throw std::runtime_error("Error loading calibril.ttf");
}

m_fps_txt = sf::Text("FPS : " + std::to_string(fps), m_font, FONT_SIZE);
m_fps_txt.setFillColor(sf::Color::Black);
text_fps = sf::Text("FPS : " + std::to_string(fps), font_style, FONT_SIZE);
text_fps.setFillColor(sf::Color::Black);

m_time_txt = sf::Text("Game Time : 0", m_font, FONT_SIZE);
m_time_txt.setFillColor(sf::Color::Black);
m_time_txt.setPosition({m_time_txt.getPosition().x, m_fps_txt.getPosition().y + m_fps_txt.getCharacterSize()});
text_time = sf::Text("Game Time : 0", font_style, FONT_SIZE);
text_time.setFillColor(sf::Color::Black);
text_time.setPosition({text_time.getPosition().x, text_fps.getPosition().y + text_fps.getCharacterSize()});

m_generation_txt = sf::Text("Generation : " + std::to_string(generation), m_font, FONT_SIZE);
m_generation_txt.setFillColor(sf::Color::Black);
m_generation_txt.setPosition(
{m_generation_txt.getPosition().x, m_time_txt.getPosition().y + m_time_txt.getCharacterSize()}
text_generation = sf::Text("Generation : " + std::to_string(generation), font_style, FONT_SIZE);
text_generation.setFillColor(sf::Color::Black);
text_generation.setPosition(
{text_generation.getPosition().x, text_time.getPosition().y + text_time.getCharacterSize()}
);

m_population_txt = sf::Text(
"Population : " + std::to_string(current_population) + '/' + std::to_string(total_population), m_font, FONT_SIZE
text_population = sf::Text(
"Population : " + std::to_string(population) + '/' + std::to_string(Birds::MAX_POPULATION), font_style, FONT_SIZE
);
m_population_txt.setFillColor(sf::Color::Black);
m_population_txt.setPosition(
{m_population_txt.getPosition().x, m_generation_txt.getPosition().y + m_generation_txt.getCharacterSize()}
text_population.setFillColor(sf::Color::Black);
text_population.setPosition(
{text_population.getPosition().x, text_generation.getPosition().y + text_generation.getCharacterSize()}
);
}

void GameStats::draw(sf::RenderTarget &target, sf::RenderStates states) const
{
target.draw(m_fps_txt, states);
target.draw(m_time_txt, states);
target.draw(m_generation_txt, states);
target.draw(m_population_txt, states);
target.draw(text_fps, states);
target.draw(text_time, states);
target.draw(text_generation, states);
target.draw(text_population, states);
}

/// \brief calculate current FPS & Game Time.
/// \warning STRICTLY SHOULD ONLY BE CALLED AT THE VERY END OF A FRAME!.
void GameStats::update()
{
m_fps_txt.setString("FPS : " + std::to_string(static_cast<unsigned int>(fps)));
m_time_txt.setString("Game Time : " + std::to_string(game_clock.getElapsedTime().asSeconds()));
fps = 1.f / fps_clock.getElapsedTime().asMilliseconds() * 1000.f;
text_fps.setString("FPS : " + std::to_string(static_cast<unsigned int>(fps)));
text_time.setString("Game Time : " + std::to_string(clock_game.getElapsedTime().asSeconds()));
fps = 1.f / clock_fps.getElapsedTime().asMilliseconds() * 1000.f;
}

/// \brief updates the population text display.
void GameStats::population_update(size_t deaths)
void GameStats::record_deaths(size_t deaths)
{
current_population -= deaths;
m_population_txt.setString(
"Population : " + std::to_string(current_population) + '/' + std::to_string(total_population)
population -= deaths;
text_population.setString(
"Population : " + std::to_string(population) + '/' + std::to_string(Birds::MAX_POPULATION)
);
}

void GameStats::new_generation()
{
generation++;
m_generation_txt.setString("Generation : " + std::to_string(generation));
text_generation.setString("Generation : " + std::to_string(generation));

current_population = Birds::INITIAL_POPULATION;
population = Birds::MAX_POPULATION;
fps = 0.f;
game_clock.restart();
fps_clock.restart();
timeSinceLastUpdate = sf::Time::Zero;
clock_game.restart();
clock_fps.restart();
update_time_elapsed = sf::Time::Zero;
}
13 changes: 7 additions & 6 deletions src/gamestats.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ class GameStats : public sf::Drawable

static const sf::Time TIME_PER_FRAME;

sf::Clock game_clock, fps_clock;
sf::Time timeSinceLastUpdate;
sf::Clock clock_game, clock_fps;
sf::Time update_time_elapsed;

size_t generation, current_population, total_population;
size_t generation, population;
float fps;

GameStats();
Expand All @@ -33,14 +33,15 @@ class GameStats : public sf::Drawable
/// \brief calculate current FPS & Game Time.
/// \warning STRICTLY SHOULD ONLY BE CALLED AT THE VERY END OF A FRAME!.
void update();
void population_update(size_t deaths);

void record_deaths(size_t deaths);

void new_generation();

private:

sf::Text m_fps_txt, m_time_txt, m_generation_txt, m_population_txt;
sf::Font m_font;
sf::Text text_fps, text_time, text_generation, text_population;
sf::Font font_style;
};

#endif
58 changes: 28 additions & 30 deletions src/genetic_algo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,31 @@
void GeneticAlgorithm::get_inputs(Birds &birds, Pipes const &pipes)
{
// calculate front/last pipe gap position.
auto top_pipe1 = pipes.pairs[pipes.front_pipe].top;
auto top_pipe2 = pipes.pairs[(pipes.front_pipe + 1) % Pipes::COUNT].top;
auto top_pipe1 = pipes.pairs[pipes.front_idx].top;
auto top_pipe2 = pipes.pairs[(pipes.front_idx + 1) % Pipes::COUNT].top;

Pipe nearest_pipe = top_pipe2;

if (top_pipe1.getPosition().x + Pipe::WIDTH >= Bird::START_X - Bird::SIZE / 2.f) {
if (top_pipe1.getPosition().x + Pipe::WIDTH >= Bird::START_X - Bird::SIZE_N / 2.f) {
nearest_pipe = top_pipe1;
}

for (auto &bird: birds.collection) {
// calculate the nearest pipe
Bird::target_gap =
sf::Vector2f(nearest_pipe.getPosition().x + Pipe::WIDTH, PipePair::GAP / 2.f + nearest_pipe.getSize().y);
// calculate the nearest pipe
Bird::target_gap =
sf::Vector2f(nearest_pipe.getPosition().x + Pipe::WIDTH, PipePair::GAP / 2.f + nearest_pipe.getSize().y);

for (auto &bird: birds.collection) {
// normalize the input 1 of the neural network
float input1_range = (Pipes::START_X + Pipe::WIDTH) - (Bird::START_X - Bird::SIZE / 2.f);
float input1 = (Bird::target_gap.x - Bird::START_X) - Bird::SIZE / 2.f;
float input1_normalized = input1 / input1_range;
float max_bird_to_pipe_x_distance = (Pipes::START_X + Pipe::WIDTH) - (Bird::START_X - Bird::SIZE_N / 2.f);
float input1 = (Bird::target_gap.x - Bird::START_X) - Bird::SIZE_N / 2.f;
float input1_normalized = input1 / max_bird_to_pipe_x_distance;

// normalize the input 2 of the neural network
float bird_position_y_range = (WINDOW_HEIGHT - Bird::SIZE / 2.f) - (Bird::SIZE / 2.f);
float max_bird_to_gap_y_distance = (WINDOW_HEIGHT - Bird::SIZE_N / 2.f) - (Bird::SIZE_N / 2.f);
float input2_normalized =
(Bird::target_gap.y - bird.getPosition().y + bird_position_y_range) / bird_position_y_range * 2.f;
(Bird::target_gap.y - bird.getPosition().y + max_bird_to_gap_y_distance) / max_bird_to_gap_y_distance * 2.f;

// feed the normalized input to the network and apply feedforward.
// feed the normalized inputs to the network (feedforward).
bird.neural_net.update_inputs(std::abs(input1_normalized), input2_normalized);
float output = bird.neural_net.feedforward();

Expand All @@ -45,8 +45,8 @@ void GeneticAlgorithm::rank_fitness(Birds &birds)
{
// calculate fitness for each bird.
for (auto &bird: birds.collection) {
float base_gap_close_fitness = 40.f * bird.neural_net.input_layer(1, 0);
bird.fitness = base_gap_close_fitness + bird.time_lived * 20.f;
float bird_to_gap_y_distance = bird.neural_net.input_layer(1, 0);
bird.fitness = 40.f * bird_to_gap_y_distance + bird.lifetime * 20.f;
}

std::sort(birds.collection.begin(), birds.collection.end(), [](Bird &a, Bird &b) { return a.fitness > b.fitness; });
Expand All @@ -55,26 +55,24 @@ void GeneticAlgorithm::rank_fitness(Birds &birds)
void GeneticAlgorithm::apply_mutations(Birds &birds)
{
// apply random mutations to the bad birds.
constexpr size_t fit_size = static_cast<size_t>(static_cast<float>(Birds::INITIAL_POPULATION) * MUTATION_CUT_OFF);
for (size_t i = fit_size; i < birds.collection.size(); ++i) {
constexpr size_t fit_n = static_cast<size_t>(static_cast<float>(Birds::MAX_POPULATION) * FITTEST_TO_KEEP);
for (size_t i = fit_n; i < birds.collection.size(); ++i) {
birds.collection[i].apply_random_mutation();
}

std::shuffle(birds.collection.begin() + fit_size, birds.collection.end(), rand_engine);
std::shuffle(birds.collection.begin() + fit_n, birds.collection.end(), rand_engine);

// breed the good birds to fill the missing population.
constexpr size_t unfit_size = (Birds::INITIAL_POPULATION - fit_size) * MUTATION_KEEP_BAD;
constexpr size_t spliced_size = fit_size + unfit_size;
constexpr size_t offspring_size = Birds::INITIAL_POPULATION - spliced_size;

size_t curr_offspring_born = 0;

for (size_t i = 0; i < fit_size && curr_offspring_born < offspring_size; ++i) {
for (size_t j = i + 1; j < fit_size && curr_offspring_born < offspring_size; ++j) {
birds.collection[spliced_size - 1 + curr_offspring_born].become_offspring(
birds.collection[i], birds.collection[j]
);
curr_offspring_born++;
constexpr size_t unfit_n = (Birds::MAX_POPULATION - fit_n) * UNFIT_TO_KEEP;
constexpr size_t splice_n = fit_n + unfit_n;
constexpr size_t offspring_n = Birds::MAX_POPULATION - splice_n;

size_t curr_offsprings = 0;

for (size_t i = 0; i < fit_n && curr_offsprings < offspring_n; ++i) {
for (size_t j = i + 1; j < fit_n && curr_offsprings < offspring_n; ++j) {
birds.collection[splice_n - 1 + curr_offsprings].become_offspring(birds.collection[i], birds.collection[j]);
curr_offsprings++;
}
}
}
4 changes: 2 additions & 2 deletions src/genetic_algo.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

struct GeneticAlgorithm
{
static constexpr float MUTATION_CUT_OFF = 0.2f;
static constexpr float MUTATION_KEEP_BAD = 0.50f;
static constexpr float FITTEST_TO_KEEP = 0.2f;
static constexpr float UNFIT_TO_KEEP = 0.50f;

/// \brief feed inputs to the neural network of each bird, will cause each bird to jump or not jump.
void get_inputs(Birds &birds, Pipes const &pipes);
Expand Down
Loading

0 comments on commit 8511b2c

Please sign in to comment.