Today I'm going to show you a basic flappy bird clone that I created, and how you can go about making a similar game. I'll essentially run through my code with you and explain how it works every step of the way. This game is built to run on an ATtiny85 clocked at 1MHz, with an I2C OLED display. Onward!
Step 1: The Hardware
If you're not interested in actually building a circuit for the game but understanding the theory behind it, you can skip this step.
My game requires two buttons, an ATtiny85, an I2C OLED 128x64px screen, and some source of power. My Instructable Business Card/Game Console: ATtiny85 and OLED Screen shows you how to build a board that has everything you need on it! If you're not interested in making my board, get out your breadboard and I'll tell you the specifications. You can use any variant of the ATtiny85. I recommend OLED displays like this one.
- Wire one side of two pushbuttons to pins 2 and 3 on your ATtiny85. On these wires, also add a 10k ohm resistor connected to ground (pull-down resistor).
- Wire the other side of these pushbuttons to voltage. When the button isn't pressed, the pin state will be low. When it is pressed, the pin state will be high.
- Connect pin 7 to the SCL pin on your display, and pin 5 to the SDA pin on your display. Wire the power pins (VCC and GND) on the display accordingly.
- Lastly, connect pin 4 on the ATtiny to ground and pin 8 to voltage.
Step 2: The Software!
Attached here is a folder with all the files you need to run the game. Two of the files, FlappyBird.ino and WallFunctions.h, are very well commented for your reading pleasure. You can upload the FlappyBird.ino sketch to your ATtiny85 (at 1MHz), and play away! If learning the theory behind this game interests you, or you want to make your own game, then read on!
There are still a few problems with my game. I2C is not the best way to transmit data by any means. According to this post, the display can only accept data at about 100KHz, so even if we bump up the ATtiny clock speed to 8MHz the I2C channel will still be the bottleneck. The display can output about 10fps maximum. The more separate images the display needs to draw, the slower the whole process is. Thus, my game is quite easy as you can't get the walls to move very fast across the screen! Below are some challenges for you if you think you're up to the task:
- To make the game harder, see if you can get the spacing between the walls to be 2 instead of 4. It shouldn't be too hard once you understand how my program works :). Please post in the comments if you can get it working!
- Another thing my game is lacking is a score system and a way to display the score and save it. See if you can implement one!
- Lastly, rather than having the walls move one column at a time, try getting each wall to move one pixel at a time for smoother motion.
Step 3: Flappy Bird ATtiny85: Theory
Even though the display we're using has 64 pixels of height to use for the game, it's only possible to position elements in blocks of 8 pixels. Thus, there are only 8 possible y-coordinates. To make things easier, in the software I divided up the whole screen in this manner, making a grid of 16 blocks by 8 blocks, where each block is 8x8 pixels. To fill each block, every sprite in the game is 8x8 pixels. This makes everything MUCH easier to manage. If you look at the image above, you can see how I divided up the screen. Each wall is composed of 6 blocks, with a hole 2 blocks in height, making the total height of each wall 8 blocks.
In the program, each wall is represented as a struct, called Wall. Each Wall struct has two properties - holePosition and column. 'holePosition' is a number 0-6, as there are only 7 possible positions in a wall of 8 blocks for a hole 2 blocks high. 'column' is a number 0-15, as there are 16 columns of blocks possible on the screen.
Step 4: WallFunctions.h
Check out the file WallFunctions.h. Contained in here are all the functions needed to draw the wall on screen. Open it up and you can follow along as I explain what's happening. The reason these are all in a separate file is because the Arduino compiler isn't great, so it won't let you pass in structs as arguments into a function unless they're declared in a separate header file along with any functions that call for a struct. Thus, I'm organizing my code this way.
drawWallSequence() is used to display a wall on the screen. Arguments a-f are just used to determine whether or not the block of the wall should be filled in with a wall sprite or left open with a blank sprite. The whole wall is drawn at the given column on-screen.
drawWall() takes a Wall struct as an argument. Based on the randomly-generated position of its hole, it draws the wall using the drawWallSequence() function that I just explained.
Step 5: FlappyBird.ino
Now open up FlappyBird.ino. You can read my comments for all the includes to see what they do. First, let's look at the setup function.
In setup(), I'm doing a few things. The first few lines are used to enable pin change interrupts on our two buttons. We need the pin change interrupts for a very important reason. If we were just to check the state of the button in loop(), then oftentimes a button would be pressed while the ATtiny is busy drawing the graphics on-screen, so the button press would be missed entirely. The pin change interrupt ensures that the button press is ALWAYS registered, regardless of what else is going on. If the ATtiny85 had multiple threads we could just listen for the button change on another thread, but it doesn't, so we can't.
In loop(), most of the lines are explained quite well with comments. I'm just going to talk generally about what's going on and how the game mechanics work rather than going over every line again. Mainly what's accomplished inside of loop() is controlling the timing for moving the walls, and creating new walls when old ones move off-screen.
When it's detected later in the game that the bird hit a wall, the function gameOver() is called. This just indefinitely displays a game over graphic on the screen until the ATtiny is reset.
checkSpriteRefresh() is called a lot throughout the game (before every function that requires drawing graphics on-screen). This function detects if the bird has changed position, and moves the bird accordingly. We don't want to do this in the ISR because if the ISR is called while the ATtiny was trying to draw graphics on the screen, it can mess a lot of things up.
ISR(PCINT0_vect) is called when pin 4 or pin 3 changes state. Thus, it's triggered every time either button is pressed, and when it's released. In this function I check to see if the bird can move where the player is trying to move it, and if the ISR wasn't triggered by fluctuations when the button is being pressed (that's what debouncing is).
moveWalls() is the function responsible for every wall movement. A for loop inside of this function cycles through the array of walls and moves each one one column to the left each time it's called, then also clears the pixels where the wall once was. Also inside here is the code that checks to see if it's game over. This is done by looking at the hole position of any wall that's at column 0 (where the bird is), then seeing if the bird is at any position other than the two hole blocks. If it is, that means the bird collided with a wall!
Step 6: That's It!
Thanks for making it all the way through this Instructable. I hope you learned something about really simple game design! Of course, there are many many ways you can make this same game. If you have any ideas, questions, problems, and more I'd love to hear them! Please post them in the comments. If you make your own game then also post it in the comments. Thanks!