In this episode we look at launching some missiles from the player tank and checking for collisions with the Invaders.
Also available on Youtube for those that like a more visual approach,
OK,Let’s go!
The full source code is available here, to use it (or just view it) just click expand and copy and paste to your Arduino Development software.
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 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 |
#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. #define INVADER_HEIGHT 8 // Player settingsc #define TANKGFX_WIDTH 13 #define TANKGFX_HEIGHT 8 #define PLAYER_X_MOVE_AMOUNT 2 #define PLAYER_Y_START 56 #define PLAYER_X_START 0 #define MISSILE_HEIGHT 4 #define MISSILE_WIDTH 1 #define MISSILE_SPEED 4 // Status of a game object constants #define ACTIVE 0 #define DESTROYED 2 // 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, }; static const unsigned char PROGMEM MissileGfx [] = { B10000000, B10000000, B10000000, B10000000 }; // 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; GameObjectStruct Missile; 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(); MissileControl(); CheckCollisions(); } 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; if((digitalRead(FIRE_BUT)==0)&(Missile.Status!=ACTIVE)) { Missile.X=Player.Ord.X+(TANKGFX_WIDTH/2); Missile.Y=PLAYER_Y_START; Missile.Status=ACTIVE; } } void MissileControl() { if(Missile.Status==ACTIVE) { Missile.Y-=MISSILE_SPEED; if(Missile.Y+MISSILE_HEIGHT<0) // If off top of screen destroy so can be used again Missile.Status=DESTROYED; } } 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 } } void CheckCollisions() { MissileAndAlienCollisions(); } void MissileAndAlienCollisions() { for(int across=0;across<NUM_ALIEN_COLUMNS;across++) { for(int down=0;down<NUM_ALIEN_ROWS;down++) { if(Alien[across][down].Ord.Status==ACTIVE) { if(Missile.Status==ACTIVE) { if(Collision(Missile,MISSILE_WIDTH,MISSILE_HEIGHT,Alien[across][down].Ord,AlienWidth[down],INVADER_HEIGHT)) { // missile hit Alien[across][down].Ord.Status=DESTROYED; Missile.Status=DESTROYED; } } } } } } bool Collision(GameObjectStruct Obj1,unsigned char Width1,unsigned char Height1,GameObjectStruct Obj2,unsigned char Width2,unsigned char Height2) { return ((Obj1.X+Width1>Obj2.X)&(Obj1.X<Obj2.X+Width2)&(Obj1.Y+Height1>Obj2.Y)&(Obj1.Y<Obj2.Y+Height2)); } 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++) { if(Alien[across][down].Ord.Status==ACTIVE){ switch(down) { case 0: if(AnimationFrame) display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderTopGfx, AlienWidth[down],INVADER_HEIGHT,WHITE); else display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderTopGfx2, AlienWidth[down],INVADER_HEIGHT,WHITE); break; case 1: if(AnimationFrame) display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderMiddleGfx, AlienWidth[down],INVADER_HEIGHT,WHITE); else display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderMiddleGfx2, AlienWidth[down],INVADER_HEIGHT,WHITE); break; default: if(AnimationFrame) display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderBottomGfx, AlienWidth[down],INVADER_HEIGHT,WHITE); else display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderBottomGfx2, AlienWidth[down],INVADER_HEIGHT,WHITE); } } } } // player display.drawBitmap(Player.Ord.X, Player.Ord.Y, TankGfx, TANKGFX_WIDTH, TANKGFX_HEIGHT,WHITE); //missile if(Missile.Status==ACTIVE) display.drawBitmap(Missile.X, Missile.Y, MissileGfx, MISSILE_WIDTH, MISSILE_HEIGHT,WHITE); display.display(); } void InitPlayer() { Player.Ord.Y=PLAYER_Y_START; Player.Ord.X=PLAYER_X_START; Missile.Status=DESTROYED; } 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); } } } |
New constant
Line 23 shows a new constant
#define INVADER_HEIGHT 8.
Which as it says just defines the height of the invaders. All the Space invaders have the same height so this makes things super easy for us.
The following lines define some properties of player missiles. The graphics height and width and also the speed that it travels at, the higher the number the faster. This is basically the number of pixels moved per game “click”. A game “click” is just one cycle though all the physics code (remember the physics code is the code that implements all the movement/game logic/ collision detection etc.
32 33 34 |
#define MISSILE_HEIGHT 4 #define MISSILE_WIDTH 1 #define MISSILE_SPEED 4 |
Line 38 adds a new status for game objects such as Invaders which allows us to flag if they have been destroyed and thus shouldn’t be displayed etc.
#define DESTROYED 2
We also define the very simple missile graphics which I won’t discuss further as we have covered this in some detail in earlier episodes.
Line 171 creates a global object for the one missile that the player can fire at a time:
GameObjectStruct Missile;
This is a game restriction/mechanic of the original arcade machine, it only allowed one player missile on screen at any one time.
The physics function has been updated to include two new aspects, the control of any fired missile and the collisions it may have:
192 193 194 195 196 197 |
void Physics() { AlienControl(); PlayerControl(); MissileControl(); CheckCollisions(); } |
Firing the missile
The firing is a player action, so quite logically this is controlled in the PlayerControl function, here are the changes:
206 207 208 209 210 211 |
if((digitalRead(FIRE_BUT)==0)&(Missile.Status!=ACTIVE)) { Missile.X=Player.Ord.X+(TANKGFX_WIDTH/2); Missile.Y=PLAYER_Y_START; Missile.Status=ACTIVE; } |
The first line is making the decision of If Player has pressed fire AND there isn’t an active missile , if this condition is satisfied then we set up the firing of the missile. Line 208 sets the missiles X position to the middle of the players tank (where the barrel is). Line 209 sets the missiles Y to the players Y, and so the missile is ready to launch from the players tank barrel. We then mark the status of the missile to Active. This will be used by several routines, including this one as just noted at the start of the paragraph. If it isn’t active for example then the display will not plot it to screen and if active then you are not allowed to fire any more missiles.
MissileControl()
214 215 216 217 218 219 220 221 222 |
void MissileControl() { if(Missile.Status==ACTIVE) { Missile.Y-=MISSILE_SPEED; if(Missile.Y+MISSILE_HEIGHT<0) // If off top of screen destroy so can be used again Missile.Status=DESTROYED; } } |
This is a very simple piece of code which basically just changes the missiles position according to the speed constant mentioned earlier. It also checks if it’s gone off the top of the screen( Y less than 0). If so it marks the missiles status as destroyed and then another can be fired by the player.
CheckCollisions()
This will be expanded in later episodes and for now it simply calls another function which is….
MissileAndAlienCollisions()
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
void MissileAndAlienCollisions() { for(int across=0;across<NUM_ALIEN_COLUMNS;across++) { for(int down=0;down<NUM_ALIEN_ROWS;down++) { if(Alien[across][down].Ord.Status==ACTIVE) { if(Missile.Status==ACTIVE) { if(Collision(Missile,MISSILE_WIDTH,MISSILE_HEIGHT,Alien[across][down].Ord,AlienWidth[down],INVADER_HEIGHT)) { // missile hit Alien[across][down].Ord.Status=DESTROYED; Missile.Status=DESTROYED; } } } } } } |
This goes through all the Invaders and for those that are still Active it checks if the missile and this Invader are in collision, to do that it uses this line;
if(Collision(Missile,MISSILE_WIDTH,MISSILE_HEIGHT,Alien[across][down].Ord,AlienWidth[down],INVADER_HEIGHT))
This is just calling a function called Collision which takes 6 parameters (arguments). The first three are for the first of the objects that may be in collision, the next three are for the second object that may be colliding with the first object. If they are colliding it returns true else false. If true then the above routine marks this particular Invader as destroyed and marks the missile as destroyed also. At this point I could have coded the routine to end as the missile can only be in collision with one object at a time, but I didn’t. The game runs fast enough and it’s not an issue but it could be an area of improvement if you wished to do this.
Collision
The collision function is a single line of code that returns true if two objects are in collision or false otherwise. It takes just two coordinate structures for the objects in question. It also takes the width and height of the two objects in question. With this information it can be worked out if these two objects are overlapping (in collision). Note :Usually we would have a structure that included the coordinates and the width/depth of the object all in one, however this elegance has been compromised in order to save memory on the memory challenged processor used on our Arduinos. To put this into perspective even some of those early 1980’s home computers had more available memory than we have at our disposal with the Atmel processor we are using. However this processor was never designed to be the equivalent of a general purpose desktop computer!
283 284 285 286 |
bool Collision(GameObjectStruct Obj1,unsigned char Width1,unsigned char Height1,GameObjectStruct Obj2,unsigned char Width2,unsigned char Height2) { return ((Obj1.X+Width1>Obj2.X)&(Obj1.X<Obj2.X+Width2)&(Obj1.Y+Height1>Obj2.Y)&(Obj1.Y<Obj2.Y+Height2)); } |
The code above is relatively simple (being only 1 line) but sometimes the code can be hard to imagine what it’s doing in your head. So below I have created a hands example of this exact code. There are 4 conditions that must be met for a collision to have taken place, when any one of these conditions are true it highlights in red. When they are all true you will see the all conditions in red and the only time this happens is when the two objects overlap. Underneath the code I’ve also shown that actual numbers for each of the four conditions. Have a play with the controls to move object 1 whilst looking at the code and the coordinates as they change.
Displaying the missile
We have some very small additional code to our display routine which shouldn’t need any explanation.
366 367 368 |
//missile if(Missile.Status==ACTIVE) display.drawBitmap(Missile.X, Missile.Y, MissileGfx, MISSILE_WIDTH, MISSILE_HEIGHT,WHITE); |
Initialising the missile
The missile is set up prior to a game starting in the InitPlayer routine.
374 375 376 377 378 |
void InitPlayer() { Player.Ord.Y=PLAYER_Y_START; Player.Ord.X=PLAYER_X_START; Missile.Status=DESTROYED; } |
All we do is simply mark the missiles status as currently destroyed.
… And that’s it for this episode, if you run the code you will see we can fire a missile and destroy an Invader. Admittedly it just disappears for now and when you’ve cleared them all nothing happens. In the next episode we’ll look at adding the explosion animation and some scoring.
Enjoy and Learn 🙂