In episode 3 we move onto making the Invaders move left to right and back again across the screen dropping down nearer to your player tank in every pass. We’ll also introduce their animation.
This episode is also available to view on line below:
The full code is shown below (click to expand as it is quite large).
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 |
#include <Adafruit_SSD1306.h> #include <Adafruit_GFX.h> // DISPLAY SETTINGS #define OLED_ADDRESS 0x3C #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 // 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. // 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 }; static const unsigned char PROGMEM InvaderMiddleGfx []= { B00100000,B10000000, B00010001,B00000000, B00111111,B10000000, B01101110,B11000000, B11111111,B11100000, B10111111,B10100000, B10100000,B10100000, B00011011,B00000000 }; static const unsigned char PROGMEM InvaderMiddleGfx2 [] = { B00100000,B10000000, B00010001,B00000000, B10111111,B10100000, B10101110,B10100000, B11111111,B11100000, B00111111,B10000000, B00100000,B10000000, B01000000,B01000000 }; static const unsigned char PROGMEM InvaderBottomGfx [] = { B00001111,B00000000, B01111111,B11100000, B11111111,B11110000, B11100110,B01110000, B11111111,B11110000, B00111001,B11000000, B01100110,B01100000, B00110000,B11000000 }; static const unsigned char PROGMEM InvaderBottomGfx2 [] = { B00001111,B00000000, B01111111,B11100000, B11111111,B11110000, B11100110,B01110000, B11111111,B11110000, B00111001,B11000000, B01000110,B00100000, B10000000,B00010000 }; // 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; }; // 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 void setup() { display.begin(SSD1306_SWITCHCAPVCC,OLED_ADDRESS); InitAliens(0); } void loop() { Physics(); UpdateDisplay(); } void Physics() { AlienControl(); } 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); } } } display.display(); } 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); } } } |
We’ve introduced two more constants to support the alien movement on screen, they are:
16 17 |
#define INVADERS_DROP_BY 4 #define INVADERS_SPEED 12 |
The INVADERS_DROP_BY definition is how many pixels the Invaders will move down when they reach the edge of the display. The INVADERS_SPEED is how fast they move across the screen at the start of a new level, the lower the number the faster they will go, which is kind of counter-intuitive I know, but the programming is a little easier that way. I’ve used an initial value of 12, which seems about right. Be warned that values should be between 0 and 127 as we use a counter variable later on in combination with this value which is a signed byte and the largest value for a signed byte is 127.
Status of game objects
In the vast majority of games something can be destroyed and Invaders is obviously no different in this respect. As every object in this game can be active or destroyed we need a variable within the GameObjectStruct structure to identify if this object is “active” or not (destroyed). The following shows this change to GameObjectStruct.
95 96 97 98 99 100 |
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 }; |
We now have a new variable Status that represents an objects current status. To Support this we have a new #define as well:
19 20 |
// Status of a game object constants #define ACTIVE 0 |
So to set a game object to active you would use the ACTIVE constant/define, something like this:
MyGameObject.Status=ACTIVE;
Animations
Space Invaders has very simple animations, for each alien character there are two separate graphics defined (i.e. two frames of animation) . We change between each one every time we move the Invaders. We just need to keep track of which one is currently being displayed. Each extra frame of animation for each Invader is simply named with a “2” at the end, as shown here for the middle Invader:
59 60 61 62 63 64 65 66 67 68 |
static const unsigned char PROGMEM InvaderMiddleGfx2 [] = { B00100000,B10000000, B00010001,B00000000, B10111111,B10100000, B10101110,B10100000, B11111111,B11100000, B00111111,B10000000, B00100000,B10000000, B01000000,B01000000 }; |
The variable that keeps track of which animation to display is this one:
121 |
bool AnimationFrame=false; // two frames of animation, if true show one if false show the other |
If it’s false we will display one frame of the Invaders animation, if true the other frame. When and how we toggle this from value to another will be discussed later.
Other variables involved in the Invaders Movement
119 120 |
char AlienXMoveAmount=2; signed char InvadersMoveCounter; // counts down, when 0 move invaders, set according to how many aliens on screen |
AlienXMoveAmount : The number of pixels that the Invaders will move at a time. This changes as the game speeds up towards the end of the level. The higher the number the quicker they appear to move.
InvadersMoveCounter : Used in conjunction with INVADERS_SPEED constant. This counter gets set to INVADERS_SPEED and then is decremented once per game cycle. When it reaches 0 we move the Invaders and reset back to the INVADERS_SPEED value. More on this later.
The loop
The Arduino loop function has the following code:
131 132 133 134 135 |
void loop() { Physics(); UpdateDisplay(); } |
Just two functions, the first Physics() is basically all the game logic and rules, from moving the Invaders to checking for collisions. Here it is below;
137 138 139 |
void Physics() { AlienControl(); } |
Not much to it! Well, yet. There will be more in this routine in later episodes but for now the only thing we are implementing are the Invaders movements with the function AlienControl(), shown below:
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 |
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 } } |
This entire routine exists just to move the invaders on screen, that is to say alter their X and Y positions, it does not plot them to the display, that is done separately by the UpdateDisplay() which is called just after the Physics routine. It is normal and good practice to separate out your game physics/ logic/rules from the final display to screen. It makes code easier to read, more manageable and disconnects any internal coordinate units we may use from the actual display. What do I mean by that? Well, what if we were suddenly blessed with a screen display of 256×128 (twice the resolution). It would be relatively trivial in the display routine to make our game work on the new display without changing any of the physics code at all. That could be all the same and we just scale to the new display in our display code. If you have plotting to screen all intertwined with your game logic it becomes an unmanageable mess even without considering different displays, so keep them separate 🙂
Let’s go through the routine.
143 144 145 146 |
if((InvadersMoveCounter--)<0) { // all the code of the routine omitted for clarity } |
If you remember (hopefully, it was only a few lines ago!) InvadersMoveCounter is basically controlling the speed of the Invaders across the screen. Looking at the line its value is first decremented by 1 and then a check is made to see if it is less than 0. If so then the Invaders are moved else we basically do nothing and exit the routine ready to be called again. At the beginning this variable is set to INVADERS_SPEED , which itself is set to 12. So every 12 cycles of the main loop we update the Invaders position. At least at the start of the level that is the case for their speed.
As an aside this line “if((InvadersMoveCounter–)<0)” may have some “C” purists shouting that it could have been written in a different slightly more compact way and in by-gone days it may have been a little faster executing. However modern compilers should ensure there is no speed penalty and for beginners the way the line is written is more approachable for beginners or those more used to strongly typed programming languages. If you have no idea what I may be referring to then don’t worry and move on but for those that do and were ready to comment I have deliberately chosen not to write it in a more perhaps C style.
So if it is time to update the Invaders positions (InvadersMoveCounter is less than 0) then we go on to line 145 and create a local variable:
145 |
bool Dropped=false; |
Dropped is an indicator of whether the Invaders have reached the end of the screen and are dropping down. We need to know this as if it is true we only move down and do not update the X positions in this current Invader position update. The next three lines check if we are at the edge of screen and set and change some variables if we are:
146 147 148 149 150 |
if((RightMostPos()+AlienXMoveAmount>=SCREEN_WIDTH) |(LeftMostPos()+AlienXMoveAmount<0)) // at edge of screen { AlienXMoveAmount=-AlienXMoveAmount; // reverse direction Dropped=true; // and indicate we are dropping } |
The first line may look a little complex but it can be broken down into two parts, the check for the Invaders going beyond the right edge of the screen and the check for them going beyond the left edge. This is the check for the right:
(RightMostPos()+AlienXMoveAmount>=SCREEN_WIDTH)
The function (which we will look at shortly) RightMostPos returns the right most Invaders X position. Obviously the right most Invader changes as the game progresses as some are destroyed, so this routine simply scans the Invaders for the rightmost one that is still active and returns that X position. We then add to that, the amount we would like them to move by, AlienXMoveAmount. If this total is more than or equal to the screen width (SCREEN_WIDTH) we have therefore hit the right hand edge.
The left hand edge check is very similar,
(LeftMostPos()+AlienXMoveAmount<0)
If the left most Invader + AlienXMoveAmount is less than 0 we have hit the left hand edge of the screen. But… some of you may be wondering how that could possibly work, how could adding the left most position which must be 0 or above to a movement amount ever be a value less than 0? Well if we are moving to the left the AlienXMoveAmount will actually be a negative number, i.e. if move to the right it would be, i.e. +2 (or just 2), if to the left it would be -2. So if the LeftMostPos was 0 we would have the following sum to be worked out of we were moving left:
0+AlienXMoveAmount
and if AlienXMoveAmount is -2 then the line becomes
0+-2
Adding a minus number to a positive ends up subtracting it from that positive number. So the answer would be -2 and less than 0 so we know we’ve hit the left hand edge.
OK, so if we have gone beyond the left or right edge we will execute the following code:
148 149 |
AlienXMoveAmount=-AlienXMoveAmount; // reverse direction Dropped=true; // and indicate we are dropping |
Whichever edge we have reached the code is the same as we need to indicate we are dropping, Dropped=true. We then need to reverse the direction of movement, the line
AlienXMoveAmount=-AlienXMoveAmount.
does this reverse of direction by changing the mathematical sign of the movement value. Let’s look at a couple of examples, if AlienXMoveAmount was 2 then the line above (if we filled in the numbers) would be:
AlienXMoveAmount=-2
and so we have swapped a +2 to a -2. But what if it is already -2, well let’s put the numbers in again:
AlienXMoveAmount=–2
We now have a minus minus 2 (–2) , one thing learnt from school is that if you minus a minus number you get a positive number so the above becomes
AlienXMoveAmount=2
and we’ve swapped the direction of movement.
Updating the Invaders positions
151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
// 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; } } } |
Here we just loop through every single Invader and the key lines are these:
156 157 158 159 160 161 162 |
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; } |
If the invader is active (i.e. not destroyed) we update its position. If we are currently dropping (remember this is set above) we update its Y position only else we update its X position, which will move it either to the left or right depending on the mathematical sign of AlienXMoveAmount, discussed earlier.
The last two lines of this routine reset the Invader Speed Counter back to its default and flip the animation frame to the next ready for the display routine.
165 166 |
InvadersMoveCounter= INVADERS_SPEED; AnimationFrame=!AnimationFrame; ///swap to other frame |
The ! symbol in C (and other languages) means simply “invert” or make opposite and applies only to True and false expressions (Boolean values). So false would become true and true would become false etc.
The Left and Right most positions
In the previous routine we needed to know the left or right most positions of the Invaders and introduced two routines called LeftMostPos and RightMostPos, lets have a look at the code for LeftMostPos.
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
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<NUM_ALIEN_ROWS){ 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 smallest for this coloum return Smallest; Across++; } return 0; // should nevr get this far } |
We set up two counters Across and Down to loop through all the Invaders and also we set a variable (Smallest) to store the currently smallest X position currently found which we initialise to a massive number of twice the screen width, which will be bigger than any current X position. Now we could have gone through every single Invader comparing their X position with the smallest X position and if that value was smaller storing it in the variable Smallest. Then at the end of the routine just return that variable, however there is a more efficient way. The smallest value for X will always be in the left most Invader column as all the other columns occur after this then they must be higher and thus cannot be smaller. So within the loop we go from the left column (Across=0) to the right column. We then scan down each invader in this column (Down=0 to last row). If the Invader is active we check if there X value is less than the current Smallest X value
if(Alien[Across][Down].Ord.X<Smallest)
If it is then we store this as the current smallest value
Smallest=Alien[Across][Down].Ord.X
When we’ve checked all the rows in a column we check if the Smallest variable has changed from what it was initially set at if(Smallest<SCREEN_WIDTH*2). If so we know we have now found the smallest X position for the left most Invader and we exit the routine with return Smallest.
The RightMostPos function is very simaler with just the small matter of having to also include an Invaders width into the calculation, so I won’t go over that but leave it for the reader to examine.
The display function “UpdateDisplay”
This was discussed in part 2 and has been altered only slightly with the addition of a decision as to which animation frame to display, for example for the top Invader (row 0) these are the lines that accomplish this:
224 225 226 227 |
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); |
So if AnimationFrame is true draw the normal InvaderTopGfx graphic else draw InvaderTopGfx2 graphic (which of course it the other frame of the animation.
That wraps up this episode, next time we’ll look at adding a player “Tank” character and making it move in response to button presses.
Enjoy and Learn 🙂