In this episode we will add in the players tank and give them the ability to control it in the left or right direction. This will require some buttons putting onto our board. First the full source code (expand and copy as required). Once compiled and uploaded to your Arduino the display should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
#include <Adafruit_SSD1306.h> #include <Adafruit_GFX.h> // DISPLAY SETTINGS #define OLED_ADDRESS 0x3C #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 // Input settings #define FIRE_BUT 6 #define RIGHT_BUT 5 #define LEFT_BUT 4 // Alien Settings #define NUM_ALIEN_COLUMNS 7 #define NUM_ALIEN_ROWS 3 #define X_START_OFFSET 6 #define SPACE_BETWEEN_ALIEN_COLUMNS 5 #define LARGEST_ALIEN_WIDTH 11 #define SPACE_BETWEEN_ROWS 9 #define INVADERS_DROP_BY 4 // pixel amount that invaders move down by #define INVADERS_SPEED 12 // speed of movement, lower=faster. // Player settings #define TANKGFX_WIDTH 13 #define TANKGFX_HEIGHT 8 #define PLAYER_X_MOVE_AMOUNT 2 #define PLAYER_Y_START 56 #define PLAYER_X_START 0 // Status of a game object constants #define ACTIVE 0 // graphics // aliens const unsigned char InvaderTopGfx [] PROGMEM = { B00011000, B00111100, B01111110, B11011011, B11111111, B00100100, B01011010, B10100101 }; const unsigned char InvaderTopGfx2 [] PROGMEM = { B00011000, B00111100, B01111110, B11011011, B11111111, B01011010, B10000001, B01000010 }; const unsigned char PROGMEM InvaderMiddleGfx []= { B00100000,B10000000, B00010001,B00000000, B00111111,B10000000, B01101110,B11000000, B11111111,B11100000, B10111111,B10100000, B10100000,B10100000, B00011011,B00000000 }; const unsigned char PROGMEM InvaderMiddleGfx2 [] = { B00100000,B10000000, B00010001,B00000000, B10111111,B10100000, B10101110,B10100000, B11111111,B11100000, B00111111,B10000000, B00100000,B10000000, B01000000,B01000000 }; const unsigned char PROGMEM InvaderBottomGfx [] = { B00001111,B00000000, B01111111,B11100000, B11111111,B11110000, B11100110,B01110000, B11111111,B11110000, B00111001,B11000000, B01100110,B01100000, B00110000,B11000000 }; const unsigned char PROGMEM InvaderBottomGfx2 [] = { B00001111,B00000000, B01111111,B11100000, B11111111,B11110000, B11100110,B01110000, B11111111,B11110000, B00111001,B11000000, B01000110,B00100000, B10000000,B00010000 }; // Player grafix const unsigned char PROGMEM TankGfx [] = { B00000010,B00000000, B00000111,B00000000, B00000111,B00000000, B01111111,B11110000, B11111111,B11111000, B11111111,B11111000, B11111111,B11111000, B11111111,B11111000, }; // Game structures struct GameObjectStruct { // base object which most other objects will include signed int X; signed int Y; unsigned char Status; //0 active, 1 exploding, 2 destroyed }; struct AlienStruct { GameObjectStruct Ord; }; struct PlayerStruct { GameObjectStruct Ord; }; // general global variables Adafruit_SSD1306 display(1); //alien global vars //The array of aliens across the screen AlienStruct Alien[NUM_ALIEN_COLUMNS][NUM_ALIEN_ROWS]; // widths of aliens // as aliens are the same type per row we do not need to store their graphic width per alien in the structure above // that would take a byte per alien rather than just three entries here, 1 per row, saving significnt memory byte AlienWidth[]={8,11,12}; // top, middle ,bottom widths char AlienXMoveAmount=2; signed char InvadersMoveCounter; // counts down, when 0 move invaders, set according to how many aliens on screen bool AnimationFrame=false; // two frames of animation, if true show one if false show the other // Player global variables PlayerStruct Player; void setup() { display.begin(SSD1306_SWITCHCAPVCC,OLED_ADDRESS); InitAliens(0); InitPlayer(); pinMode(RIGHT_BUT, INPUT_PULLUP); pinMode(LEFT_BUT, INPUT_PULLUP); pinMode(FIRE_BUT, INPUT_PULLUP); } void loop() { Physics(); UpdateDisplay(); } void Physics() { AlienControl(); PlayerControl(); } void PlayerControl() { // user input checks if((digitalRead(RIGHT_BUT)==0)&(Player.Ord.X+TANKGFX_WIDTH<SCREEN_WIDTH)) Player.Ord.X+=PLAYER_X_MOVE_AMOUNT; if((digitalRead(LEFT_BUT)==0)&(Player.Ord.X>0)) Player.Ord.X-=PLAYER_X_MOVE_AMOUNT; } void AlienControl() { if((InvadersMoveCounter--)<0) { bool Dropped=false; if((RightMostPos()+AlienXMoveAmount>=SCREEN_WIDTH) |(LeftMostPos()+AlienXMoveAmount<0)) // at edge of screen { AlienXMoveAmount=-AlienXMoveAmount; // reverse direction Dropped=true; // and indicate we are dropping } // update the alien postions for(int Across=0;Across<NUM_ALIEN_COLUMNS;Across++) { for(int Down=0;Down<3;Down++) { if(Alien[Across][Down].Ord.Status==ACTIVE) { if(Dropped==false) Alien[Across][Down].Ord.X+=AlienXMoveAmount; else Alien[Across][Down].Ord.Y+=INVADERS_DROP_BY; } } } InvadersMoveCounter=INVADERS_SPEED; AnimationFrame=!AnimationFrame; ///swap to other frame } } int RightMostPos() { //returns x pos of right most alien int Across=NUM_ALIEN_COLUMNS-1; int Down; int Largest=0; int RightPos; while(Across>=0){ Down=0; while(Down<NUM_ALIEN_ROWS){ if(Alien[Across][Down].Ord.Status==ACTIVE) { // different aliens have different widths, add to x pos to get rightpos RightPos= Alien[Across][Down].Ord.X+AlienWidth[Down]; if(RightPos>Largest) Largest=RightPos; } Down++; } if(Largest>0) // we have found largest for this coloum return Largest; Across--; } return 0; // should never get this far } int LeftMostPos() { //returns x pos of left most alien int Across=0; int Down; int Smallest=SCREEN_WIDTH*2; while(Across<NUM_ALIEN_COLUMNS){ Down=0; while(Down<3){ if(Alien[Across][Down].Ord.Status==ACTIVE) if(Alien[Across][Down].Ord.X<Smallest) Smallest=Alien[Across][Down].Ord.X; Down++; } if(Smallest<SCREEN_WIDTH*2) // we have found smalest for this coloum return Smallest; Across++; } return 0; // should nevr get this far } void UpdateDisplay() { display.clearDisplay(); for(int across=0;across<NUM_ALIEN_COLUMNS;across++) { for(int down=0;down<NUM_ALIEN_ROWS;down++) { switch(down) { case 0: if(AnimationFrame) display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderTopGfx, AlienWidth[down], 8,WHITE); else display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderTopGfx2, AlienWidth[down], 8,WHITE); break; case 1: if(AnimationFrame) display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderMiddleGfx, AlienWidth[down], 8,WHITE); else display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderMiddleGfx2, AlienWidth[down], 8,WHITE); break; default: if(AnimationFrame) display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderBottomGfx, AlienWidth[down], 8,WHITE); else display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderBottomGfx2, AlienWidth[down], 8,WHITE); } } } // player display.drawBitmap(Player.Ord.X, Player.Ord.Y, TankGfx, TANKGFX_WIDTH, TANKGFX_HEIGHT,WHITE); display.display(); } void InitPlayer() { Player.Ord.Y=PLAYER_Y_START; Player.Ord.X=PLAYER_X_START; } void InitAliens(int YStart) { for(int across=0;across<NUM_ALIEN_COLUMNS;across++) { for(int down=0;down<3;down++) { // we add down to centralise the aliens, just happens to be the right value we need per row! // we need to adjust a little as row zero should be 2, row 1 should be 1 and bottom row 0 Alien[across][down].Ord.X=X_START_OFFSET+(across*(LARGEST_ALIEN_WIDTH+SPACE_BETWEEN_ALIEN_COLUMNS))-(AlienWidth[down]/2); Alien[across][down].Ord.Y=YStart+(down*SPACE_BETWEEN_ROWS); } } } |
So the first addition you should notice are these three lines
9 10 11 12 |
// Input settings #define FIRE_BUT 6 #define RIGHT_BUT 5 #define LEFT_BUT 4 |
We are setting these “constants” to the digital pins 4 ,5 and 6 of the Arduino. These will be the pins that our buttons are connected to. The next lines are related to the player:
24 25 26 27 28 29 |
// Player settings #define TANKGFX_WIDTH 13 #define TANKGFX_HEIGHT 8 #define PLAYER_X_MOVE_AMOUNT 2 #define PLAYER_Y_START 56 #define PLAYER_X_START 0 |
Mostly self explanatory. The PLAYER_X_MOVE_AMOUNT is the number of pixels the players tank moves at a time. The X and Y for the player is the players start position. Lines 106 to 115 (of the main code) define the players “Tank” graphics. If you are not sure about this then please refer to episode 2 where we discuss the graphics in more detail.
Next we have added a structure for the player:
130 131 132 |
struct PlayerStruct { GameObjectStruct Ord; }; |
Just like the Invader (AlienStruct) in the previous episode this at present just holds the position of the players tank.
Line 156 initialises the player variable as a global variable (accessible to all parts of the code)
155 156 |
// Player global variables PlayerStruct Player; |
The setup routine has been expanded quite a lot:
159 160 161 162 163 164 165 166 167 168 169 |
void setup() { display.begin(SSD1306_SWITCHCAPVCC,OLED_ADDRESS); InitAliens(0); InitPlayer(); pinMode(RIGHT_BUT, INPUT_PULLUP); pinMode(LEFT_BUT, INPUT_PULLUP); pinMode(FIRE_BUT, INPUT_PULLUP); } |
The InitPlayer initialise the player (more on that shortly) and then we set some pin modes for the Arduino pins that we will connect our buttons to. We are using the Arduino built in constant INPUT_PULLUP. This means these pins will be used for inputs and that we will used the Arduinos internal 10K (10,000) Ohm resistors to connect them (pull them up) to +5V. Why do we do this?…
Floating Inputs
On most electronics inputs, whether they are microprocessors (like the Arduino) or other types of chips, they should always be connected to something if you intend to use them in some way. If they are left “floating” i.e. with no connection to anything then they can give false values. That is to say they could report a 1 (+v) or 0 (gnd) input when actually the pin has not been set to any value by the user. Now traditionally in these situations you simply add a 10K resistor (although any high value resistor will usually be OK) to the input pin and connect it to either the positive or negative voltage. If connected to the positive then the pin will always report a “1” unless you deliberately connect it to the negative supply rail (gnd for our purposes). If connected to the 0V then it will always report a “0” unless we deliberately connect it to the +v rail. In either case the input value is most definitely a 1 or a 0. If you do not do this then you could get spurious 1‘s or 0‘s on the input pin when it is not deliberately connected to a voltage rail. The designers of the Atmel processor used by the Arduino spoilt us by putting some 10K resistors inside the chip, nice 🙂 So we don’t need to add them ourselves, to use them we just need to use INPUT_PULLUP setting to enable this and the pin will automatically be connected to the +ve rail via a 10K resistor.
So this pin will always report a “1” when we read it unless we connect it to the 0V rail. This is important to note when we come to wire up our pins to the push switches and when we come to read the values from the pins and make sense of them.
Wiring the buttons
We have three buttons to wire up, Left,Right and Fire to Arduino pins 4,5,6 respectively. Diagram and real life wiring below (Click for large view):
Actual real world wiring
So as can be seen when a button is pressed it connects that pin to ground (0V). When we look at that pins value it will read a 0 of pressed and a 1 if not pressed.
Player Control
So where do we scan for these button presses, within the Physics routines mentioned in the last episode, here is the function again with an important addition:
176 177 178 179 |
void Physics() { AlienControl(); PlayerControl(); } |
Let’s look in more detail at the PlayerControl function:
182 183 184 185 186 187 188 |
void PlayerControl() { // user input checks if((digitalRead(RIGHT_BUT)==0)&(Player.Ord.X+TANKGFX_WIDTH<SCREEN_WIDTH)) Player.Ord.X+=PLAYER_X_MOVE_AMOUNT; if((digitalRead(LEFT_BUT)==0)&(Player.Ord.X>0)) Player.Ord.X-=PLAYER_X_MOVE_AMOUNT; } |
We scan for two button presses at present (as fire is not yet implemented). If the RIGHT_BUT pin is 0 then we’ve pressed the right move button, BUT we only update the players X position if the end of the tank is still on screen – (Player.Ord.X+TANKGFX_WIDTH<SCREEN_WIDTH), If it is we simply increase the players X position by the amount of pixels the player moves per move (PLAYER_X_MOVE_AMOUNT).
Similarly, the LEFT_BUT is scanned and if the pressed and the players X position is still on screen then the X position will be decremented by the player movement amount. This all ensures that the tank cannot go off the ends of the screen.
Displaying the player
Added to the UpdateDisplay function is just one line that plots the players tank
296 |
display.drawBitmap(Player.Ord.X, Player.Ord.Y, TankGfx, TANKGFX_WIDTH, TANKGFX_HEIGHT,WHITE); |
Initialising the player
Back at the start we saw that we called a player initialise routine, here it is:
301 302 303 304 |
void InitPlayer() { Player.Ord.Y=PLAYER_Y_START; Player.Ord.X=PLAYER_X_START; } |
At present it doesn’t do much apart from set the players initial start position. That’s it for this episode, you should now have some marching Invaders and a move-able Tank, it really should be starting to look like a playable game now 🙂 Next time we’ll look at firing a missile and implementing the destruction of those pesky Invaders!
Enjoy and Learn 🙂