Flappy Bird Clone on the ATmega328 (Arduino UNO)
It has been a while now since I bought a small starter kit to tinker with my Arduino UNO and the ATmega328p. Truth is, after making my first amazing blinking led project I somehow forgot about its existence until recently. I ordered tiny TFT screens for a project I have in mind related to monitoring my data plan, since they were quite cheap I bought two of them so I wouldn't worry about having to wire/unwire a single screen to experiment with it.
I can't hide the fact that I felt like a child when the package arrived, it was time to play with the Arduino UNO again. First thing I wanted to make was a little game, I was sure it was possible to create one for the ATmega328 however I had never done such thing and I was worried about how fast I could display graphics on screen. The ATmega328 has 2KB SRAM, 32KB flash and while able to run at 20Mhz, is running at 16Mhz on the Arduino UNO board. The Sainsmart 1.8" TFT screen has a ST7735 chip and is wired to the Arduino UNO board using SPI, the screen resolution is 128x160 and by default displays in portrait mode. Coordinate system might seems a bit strange since 0, 0 is at the top left corner of the screen.
Enough with the talking, for some reason apparently a lot of devs at some point make a Flappy Bird clone, I had to make one as well:
Wiring:
I used a mini-breadboard to wire a push button, a 10K Ohm resistor and the TFT screen. Wiring the Sainsmart 1.8" TFT screen to the Arduino UNO was really easy thanks to this article over Tweaking4All.
Program approach:
The process of making the game was in multiple steps:
- test push button and interact with screen
- make the push button move a placeholder shape (our player)
- test player collision with the ground
- display a shape for the pipe moving accross the screen and find the fastest way to draw it
- test player collision with the pipe
- set a simple scoring system
- add better movement to the player (velocity, gravity)
- try using a sprite for our player instead of a shape
- try detailing the pipe as well
- try making a (limited) environment
It's really not an exhaustive list and allowed me to have a working game while worrying about the graphics later by using flat shapes as placeholders. That way I was able to understand if the game was slow because of a poorly coded game logic or because of the multiple drawing calls while trying to make the graphics less simple than flat shapes. (hint: multiple drawing calls are often the real issue)
The game loop is broken into three parts:
- update the game (player and objects positions, check for push button, etc..)
- draw (background, pipe, player, score)
- check for collision (if the player passed a pipe successfully increase score, if he hit a pipe or the ground exit the loop)
The code is using two libraries:
- Adafruit GFX Library: github.com/adafruit/Adafruit-GFX-Library
- Adafruit ST7735 Library: github.com/adafruit/Adafruit-ST7735-Library
The program needs both of these to interact with the screen. If you look at the graphic test example or the library source you will notice a couple functions to draw lines, shapes and pixels on screen:
- fillScreen(uint16_t color)
- drawPixel(int16_t x, int16_t y, uint16_t color)
- drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color)
- drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color)
- fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color)
Testing functions and finding best methods:
The first test I did was to check how fast I could fill the screen with a color using the fillScreen function and I quickly forget about that idea since it was terribly slow. You can see that by yourself if you run the graphic test example coming with the ST7735 library. It's not like we could have stored the complete buffer in memory either since we are limited on that part as well. There is a method called double-buffering where we draw inside a hidden buffer and display (swap) it on screen in a single drawing call. This method would need at least 128x160x2 of memory to store each color at its respective position, 2 bytes per color since it is an unsigned short (uint16_t). This represents 40960 bytes, too much to handle for the ATmega328.
In short: not enough memory, too slow to re-draw the full screen.
The only apparent solution I had was to use a 'brush method', since I'm not clearing the screen to draw on a clean canvas on each loop iteration I have to take care and erase all the 'dirty' pixels left behind after I moved something on screen. Here is a poorly made diagram to illustrate this method with an object moving on the y axis:
We just need to take care and draw with the background color over the trails of dirty pixels. For the bird we save its y position on each iteration into an old_y variable and before drawing it's new position we 'erase' the old bird with the background color at old_y. The same method will applies for the pipe and everything moving on screen. We save a lot of drawing by limiting the calls to only the location where we know the pixels are dirty. This technique also allows us to draw shapes from a single line, the pipe for example is moving on the x axis from the right of the screen to the left and loop. So in order to draw the pipe we just need to draw a 1 pixel line accross the screen with the pipe color, from top to bottom. After moving the pipe on the next iteration the dirty pixels will fill the shape giving the illusion we drew a rectangle. We finally draw another line with the background color at the pipe x position + pipe width.
The pipe gap to let the bird fly through is really simple. Each time the pipe comes out of the screen I randomly generate a position for the gap and save it into gap_y. I draw the upper part of the pipe from 0 (top of the screen) to gap_y, then I draw the lower part from gap_y + GAPHEIGHT to the bottom of the screen. To calculate collision with the bird I use a bounding box collision method, it simply checks if the bounding box of the bird is outside the bounding box of the gap when passing inside a pipe:
// checking for bird collision with pipe
if (bird.x+BIRDW >= pipe.x-BIRDW2 && bird.x <= pipe.x+PIPEW-BIRDW) {
// bird entered a pipe, check for collision
if (bird.y < pipe.gap_y || bird.y+BIRDH > pipe.gap_y+GAPHEIGHT) break;
else passed_pipe = true;
}
// if bird has passed the pipe increase score
else if (bird.x > pipe.x+PIPEW-BIRDW && passed_pipe) {
passed_pipe = false;
// ...
}
I would later use the same technique to add details to the pipe, a single black pixel was enough to draw a border on the pipe. Same thing with the floor by drawing a couple distanced small lines with two different colors to create the tiles.
After finding how to draw the player and pipe fast enough I started adding better movement for the bird. I constantly add velocity to its y axis and calculate it by adding the result of the gravity multipled by delta time:
bird.vel_y += GRAVITY * delta;
bird.y += bird.vel_y;
The delta time is calculated by subtracting the current time with the old time from the previous iteration:
old_time = current_time;
current_time = millis();
delta = (current_time-old_time)/1000;
For more information about the game loop you can read this great article on koonsolo.com titled deWITTERS Game Loop. I am using a constant game speed with maximum FPS method described in the article. Using a properly coded game loop will help you save precious cycles and draw more FPS.
Optimizing:
In order to get better performance out of the ATmega328 I had to do some tricks or go to a lower level and avoid using some functions provided by the Arduino library. The very first thing I did was replacing the call to digitalRead(pin) with direct port manipulation, it is well known that the digitalRead function use too many cycles. It's not an issue for many projects but this time we really need to avoid wasting time on such trivial operation. You can read about Port Manipulation on the official Arduino website.
Another thing I did was avoiding functions calls within the game loop inside my own code, it would be much cleaner to have the update and draw sections inside their own functions but it would waste cycles for no reason.
If I knew a value would not change I would store it in a temporary variable outside the loop instead of calculating it for each iteration. Same with constant and define values.
Instead of using the library's own drawPixel method I made an inline function calling setAddrWindow and pushColor. This function was used to clear and draw the bird sprite stored in an array but also to draw small details such as the black pipe borders. (called seam in the source)
There was no reason to redraw the floor (the 'dirt/mud' part) since nothing was moving in that area, adding the floor also saved some cycles since I would not draw and erase the pipe lines accross the full height of the screen anymore.
Final words, source code and small video:
This is how I made this simple clone of Flappy Bird on the ATmega328 and ST7735. The whole process took a couple days and was a fun project to work on.
You can find the project source on github: github.com/mrt-prodz/ATmega328-Flappy-Bird-Clone
Or watch a small video about the process on youtube: