This was a project me and my project partner developed for our 3rd year Computer Graphics course. This project is based on OpenGL and is completely written in C++. I decided using C++ over Javascript, the language I had most experience in at that time, for multiple reasons. Firstly, OpenGL is itself written in C and thus has a stable, well written implementation for C++ and secondly, after working on a high level language like Javascript, I wanted to try out something different to gain a new perpective and C++ seemed like a good option.
In my 2nd year, I had worked on a basic demonstration of Genetic Algortihm and thought it would be a good idea to extend the project. Me and my project partner decided to create a Flappy Bird like game with a focus on using genetic algorithm to train the computer to play the game.
The link to the source code can be found at the bottom of the post.
Overview
The development of this project was divided into 2 major phases:-
Creating the Game Using OpenGL
The game consists of a simple rectangular bird-like object which only has the ability to jump. The aim is to time the jumps so as to cross the randomly generated obstacles without colliding. The user is scored based on the number of obstacles crossed, much like the original Flappy Bird game. The first phase involved implementing the logic and developing methods to render the game.
Implementing the ANN and Genetic Algorithm
The second phase involved laying out the code to enable the computer to understand and learn to play the game. This was done by attaching the bird with an Artificial Neural Network(ANN) that determined when to jump. Now, the challenge here was to train the ANN to achieve desired results. This training was done through genetic algorithm(GA) which was used to tweak the hyper paramaters of the network in order to maximise the final score.
The Game
Although this article will be more focused towards the genetic algorithm part of the project, I’ll be quickly going through the interesting parts of designing and implementing the game. For more information you can go through the source code linked below.
The game has 2 major entities whcih are implemented as C++ classes:-
The Obstacles
The Obstacles are defined in the
Obstacle
object that can be found in/includes/obstacles.h
with the corresponding definitions in/osbtacles.cpp
. The Obstacles have aposition
attribute corresponding to their position as well as anopening
indicating the height at which the opening in the obtacle is present.
class Obstacle {
public:
...
int position, opening;
Obstacle *next_obs;
Obstacle *prev_obs;
...
}
As we notice, the class has self referencing pointers to next and the previous obstacles in the list.
Another class ObstacleList
defined in the same files holds all the obstacles as a doubly linked list
with pointers to the first and last Obstacle
object of the list named obs_list_head
and obs_list_rear
. It
also implements the add_rear()
method to insert new obstacles to the rear of the list.
class ObstacleList {
public:
Obstacle* obs_list_head;
Obstacle* obs_list_rear;
ObstacleList();
void add_rear(Obstacle *obs); //adds obs to the rear of the linked list
void generate_obstacles(); // generate obstacles with random openings
void update_pos(); //update position of obstacles after each display cycle
...
};
The Bird
The Bird is defined in the
Bird
object that can be found in/includes/bird.h
with the corresponding definitions in/bird.cpp
The following snippet shows the important data memebers and variables of the
Bird
class.
class Bird
{
public:
...
Obstacle* next_obs = NULL;
...
/* update bird's position for the next frame based on gravity and
* user input */
void update_pos(Obstacle* o = NULL);
void jump();
/* Check if the upcoming obstacle has been crossed */
bool has_crossed(Obstacle* o = NULL);
bool has_collided(Obstacle* o = NULL);
...
The bird has a next_obs
variable the points to the upcoming object in the ObstacleList
. Further,
it has has_crossed()
and has_collided()
members to determine wether bird has crossed or collided with next_obs
The Neural Network
Now that the game was ready, the next phase involved adding some kind of intelligence to the bird so it could start jumping through the obstacles on its own. This was done through the use of an ANN. To implement the ANN we used a C++ library called Fast Artificial Neural Networks. FANN allows us to initialize a neural network by passing the number of input, output and hidden layers. Initially the neural network has randomized weights and returns a random output between 0.0 and 1.0 everytime the network is run.
With the neural network set up, we attached the ANN to a new class named NeuralBird
inherited from Bird
. The ANN was now acting as the brain of the bird telling it when to jump based on the distance from the obstacle. This was done using the should_jump()
method which ran the neural network for the given input and based on the output, decided whether the bird should jump or not. The following code snippet shows the partial implementation of should_jump()
method which takes the x and y distance from the Bird.next_obs
data memeber as input.
bool NeuralBird::should_jump(fann_type* input)
{
...
...
/* fann_runn runs the given ANN to produce an output which is
* then stored in jump_decision data member
*/
this->jump_decision = fann_run(this->ann, input);
// if the output is greater than 0.5 jump, else do nothing
if (this->jump_decision[0] > 0.5 && last_time >= time_bw_jumps) {
last_time = 0;
return true;
} else {
last_time++;
return false;
}
}
At this point, we had a NeuralBird
class which could play the game on its own but its
ANN still had random weights. The output of the ANN was unreliable which caused the
bird to flap around randomly not making any progress whatsoever. We needed a way to train and modify
the hyperparameters in order maximise the score.
Genetic Algorithm and the Training Phase
Now conventionally, one of the most popular ways to train an ANN is backpropogation but I wanted to experiment with training the network using solely genetic algorithm. I won’t be going into the detailed workings of GAs as there are many other good resources out there, rather I’ll be focusing on the parts that are relevant for the project.
So, the implementation of the GA is in the /includes/genetic.h
and genetic.cpp
files
in the source code. The following code snippet shows the important data memebers and methods
of the GA
class which implements the algorithm and the rest of the training is done in train.cpp
located in the root directory.
class GA {
public:
...
// A population of NeuralBird objects used for training
NeuralBird **population, **last_population;
ObstacleList ol;
...
void init_rand_pop();
// Simulate the game for the bird nb over the obstacle course ol and register the score
int simulate_game(NeuralBird *nb, ObstacleList ol);
// Simpulate the game for the current generation
void simulate_game_gen();
// Crossover 2 parents to create an offspring with qualities of both parents' ANNs
void crossover(NeuralBird *nb1, NeuralBird *nb2, NeuralBird **child);
// Use crossover() to create a new generation from the previous one
void evolve();
};
The implementation can rougly be grouped in the following steps which are repeated after every iteration.
Generating Random Population of
NeuralBird
ObjectsFirst, around 10
NeuralBird
objects were generated with random ANNs. These would act as our starting generation. EachNeuralBird
had its own ANN and same instance ofObstacleList
was passed to each of the birds to keep the training fair.Assigning a Fitness to Each Member of the Population
We ran a simulation of the game for each of the
NeuralBird
and assigned a fitness to it. In genetic algorithm, fitness of an organism is the measure of how close it is to the desired solution. In our case the fitness was simply the final score of the bird after the simulation was run. The functionsimulate_game()
is responsible for running the simulation for one bird which is then repeatedly called bysimulate_game_gen()
to do it for the whole generation. Their partial implementations are as follows.
int GA::simulate_game(NeuralBird *nb, ObstacleList ol)
{
...
...
// play till score exceeds 100 or the collision occurs
while (!nb->game_over && nb->score < 100) {
ol.update_pos();
nb->Bird::update_pos();
// Run the ANN after each loop to determine whether the bird should jump
if (nb->should_jump()) {
nb->Bird::jump();
}
}
}
Performing Selection Operation
Selection and crossover lie at the core of every genetic algorithm implementation. Selection involves selecting 2 parents from the current population to produce an offspring. The parents are selected in such a way that organisms with better fitness are more likely to be selected for crossover(explained in next section). That being said, we should also give other low performing organisms a chance to be selected to ensure randomness to some extent.
Our implementation involved selecting a certain number of winners from the population based on their fitnesses. Next, we selected some parents from the winners exclusively, and rest from all the organims randomly. This ensured that better genes were being propogated to next generation and at the same time maintained a factor of randomness by giving a chance to less fit organisms.
Performing Crossover Operation
The next step involved taking the selected parents and combining their characteristics into a new organism that would become a part of our next generation. This operations is known as crossover and is vital to the success of any GA. Selection and crossover must be performed multiple times until we get a whole new population.
In our case, we perforemed a single point crossover over the biases of the ANNs of the parent keeping rest of the connections same. This resulted in a new ANN which was then attached to a child
NeuralBird
object and added to the new population.Both, the selection and crossover are done in the
evolve()
function of theGA
class defined ingenetic.cpp
.Repeat the Previous Steps
Now that we had a new population, we again simulate the game and assign fitnesses and repeat the selection and crossover process. The stop condition is implementation dependent and could be after certain iterations or after a certain fitness is achieved. We stoped the evolution process as soon as any bird attained a score of 100. At which point we used a FANN to save the ANN of the winning bird to a file. This saved ANN could later be imported to be used to play the game.
Future Improvements
Although we got our project working successfully, we are well aware that our implementation is pretty naive. There are still a lot of factors that need to be considered while tuning the hyper paramaters. I stumbled upon the following paper which gives some great insight about better ways to crossover 2 neural networks. Further, genetic algorithms in itself are usually not enough to successfully train a network for practical use and are usually combined with other traditional training algorithms.
I also came across a method of reinforced learning called Q-Learning which provides a way of training the ANN using back propogation even in absence of test data. This is done thorugh special equations used to calculate rewards and penalties that are ultimately used to tweak the weights of the network. I have started working on a Q-Learning implementation of the same Flappy Birds game and plan to compare its results and efficiency with the GA approach.
Snapshots
I’ll be attaching a gif of the AI in action below along with the source code of the whole project. Feel free to go through the code and get back to me with any questions, doubts or suggetions!

Source Code: https://github.com/OjaswinM/flappy-bird-ai/tree/master
References
Machine Learning for Flappy Bird using Neural Network and Genetic Algorithm. A great resource that does something similar using Javascript and HTML. Helped me design my crossover and selection functions
Training Feedforward Neural Networks Using Genetic Algorithms - David J. Montana and Lawrence Davis Provides great insight about better implementations of crossover operations