In this episode we’ll look at plotting several Aliens on screen, moving and animating them. If you’ve not seen the first episode then it would be a good idea to look back at that one (see the menus above). If you’ve not set up and got your OLED screen working then you need to look at the page for setting that up here.
An accompanying YouTube video has also been produced if you like your information more visual!
Ok, that out of the way let’s move one…
The code shown below will display an array of aliens on screen, we’ll sort out moving and animating them a little later on but for now let’s not get too far ahead of ourselves.
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 |
#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 // graphics // aliens const unsigned char InvaderTopGfx [] PROGMEM = { B00011000, B00111100, B01111110, B11011011, B11111111, B00100100, B01011010, B10100101 }; 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 InvaderBottomGfx [] = { B00001111,B00000000, B01111111,B11100000, B11111111,B11110000, B11100110,B01110000, B11111111,B11110000, B00111001,B11000000, B01100110,B01100000, B00110000,B11000000 }; // Game structures struct GameObjectStruct { // base object which most other objects will include signed int X; signed int Y; }; 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 void setup() { display.begin(SSD1306_SWITCHCAPVCC,OLED_ADDRESS); InitAliens(0); } void loop() { UpdateDisplay(); } 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: display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderTopGfx, AlienWidth[down], 8,WHITE); break; case 1: display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderMiddleGfx, AlienWidth[down], 8,WHITE); break; default: display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderBottomGfx, 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))-down; Alien[across][down].Ord.Y=YStart+(down*SPACE_BETWEEN_ROWS); } } } |
Constants
Lines 4-14 define some constants that we will use in our code. My style for naming these is as follows. All capital letters and underscores between words. Note that these are not really constants in a true C programming sense, they are as you can see “definitions” created with the define keyword. There is a subtle difference between C constants and defines but for most purposes it does not matter and I prefer to use the define keyword for these sort of settings. Throughout the articles I will use the word constant to refer to these rather than define. Be aware that the subtle difference referred to can occasionally catch you out but for these examples everything will work just fine. Reading through the constants should be fairly explanatory as to what they are used for but in addition here is a diagram explaining the alien settings in particular.
The constant LARGEST_ALIEN_WIDTH is the width of the largest alien, used alongside SPACE_BETWEEN_ALIEN_COLUMNS to set the spacing between columns of aliens.
Defining the graphics
All graphics are simply definitions of where we want to show a pixel (or not). For simple black and white images a binary “1” would mean a white pixel and a 0 would mean black. In fact the graphics library used from Adafruit will plot a white pixel where there is a “1” defined and where there is a “0” it will not do anything – effectively leaving whatever is already on the screen there. So a white pixel already on screen would remain on screen if it had already been plotted previously. This can be a very useful behaviour for more advanced games but for Space Invaders it really doesn’t matter.
Look below at the definition for one of the invaders;
31 32 33 34 35 36 37 38 39 40 41 |
static const unsigned char PROGMEM InvaderMiddleGfx []= { B00100000,B10000000, B00010001,B00000000, B00111111,B10000000, B01101110,B11000000, B11111111,B11100000, B10111111,B10100000, B10100000,B10100000, B00011011,B00000000 }; |
It’s actually this one (zoomed in), looking above and at this image you can see that a 0 represents a black (or no pixel) and a 1 represents a white (or switched on pixel). So for this project I looked at the original graphics and created the binary representation of it. An important note, although the invader is physically only 11 pixels wide we must define up to a full byte. 8 bits fits in the first byte across and then we only use a further (maximum) 3 bits of the second byte but we must pad it out after the last pixel (last “1”) with 0’s otherwise the image will not look right. Why? Well the compiler if it sees this line;
B10000000
will correctly see it as the value 128 (which is the decimal equivalent of this byte). However if we put
B1
and no trailing 0’s it will see it as the decimal value “1” and this is what will be stored, but what does decimal “1” look like when stored as a byte? Like this
B00000001
Which is dramatically different to what we actually intended. Why must it store as a byte (8 bits)? Well that is the fundamental unit of memory from basically the massive growth in micro-computers from the late 1970’s to the early 80’s. And it’s this value that has kind of stuck as the basic unit of memory. So all 8 bits of a byte are always used to to store a value. So a B1 is a the number 1 and the number 1 as an 8bit byte is 00000001. So if you intend it to be the left most bit that is a 1 then you must explicitly put in the remaining 0’s to give B10000000.
Looking at in decimal, if you want to talk about the number 1000, you would not write just 1 and presume the person reading knew you meant a 1000, you would put in the correct amount of 0’s and that’s all we’re doing here.
Game Structures
In this game we introduce the concept of structures. These are just convenient ways of grouping related data together to make the program more readable/maintainable and easier to write in the first place. In future game builds we will move onto more advanced Object Orientated programming which has a basis in the structure concept, but for now we’ll keep it slightly more simple. Here is the basic structure that all our game elements (invaders, mothership etc.) use;
55 56 57 58 59 |
struct GameObjectStruct { // base object which most other objects will include signed int X; signed int Y; }; |
It basically defines two variables X and Y, which unsurprisingly store the X and Y pos of a character. Some of you may be wondering why we set them as a “signed int”, why not a byte as the screen is only 128 pixel positions across and 64 pixel positions down. Well initially it was like that in the first draft of this game, but as the program developed there was a requirement to have some objects partially or wholly off screen, the mothership for one and missiles and bombs etc. This meant we needed signed numbers so we can store negatives and numbers above 127 (last pixel position of the screen, goes 0-127). This meant that bytes were no longer an option despite taking up half the space as an int. An unsigned byte can only store 0-255, a signed byte only -128 to +127.
Next we build on this structure with one specific to the invaders themselves:
61 62 63 |
struct AlienStruct { GameObjectStruct Ord; }; |
This structure at present consists of just the previous structure and if this was the only thing that was in the AlienStruct then of course this would make no sense, but later this structure will be added to. So at present this structure represents one single Invader but we need several on screen. What is needed as an array of aliens and so we use an array to store many of these aliens. The following code creates this array:
71 |
AlienStruct Alien[NUM_ALIEN_COLUMNS][NUM_ALIEN_ROWS]; |
This creates NUM_ALIEN_COLUMNS across by NUM_ALIEN_ROWS down and in our case that’s 7 columns across by 3 rows down. At present though the X and Y values could contain random 0’s or at best all be set to 0, which is not what we want, we need a routine to initialise the aliens to their starting positions on screen and here it is:
113 114 115 116 117 118 119 120 121 122 |
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))-down; Alien[across][down].Ord.Y=YStart+(down*SPACE_BETWEEN_ROWS); } } } |
So at present all this does is set the original positions on screen for all the invaders using a combination of the constants defined earlier and a new array called AlienWidth.
The AlienWidth array
All Invaders have a height of 8, so this can be fixed in code. However their width varies on their type but every row is the same type. We could have stored within the alien structure a variable that stated the width of this particular alien/Invader and normally I would do it that way. However with a eye on the limited variable storage we have on the Arduino processor, we can see that each row of Invaders is of the same type, so we only need to store the width information for an invader on a per row basis. So this 3 item array is populated with the 3 width values for each row saving around 18 bytes for our particular number of aliens. If you had more columns you would save more. This array was defined and populated here:
76 |
byte AlienWidth[]={8,11,12}; // top, middle ,bottom widths |
Displaying those pesky Invaders
Showing them on the screen is a relatively simple matter of just looping through the alien array and plotting them at their XY coordinates. The code below achieves this and is called in the main Arduino loop:
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
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: display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderTopGfx, AlienWidth[down], 8,WHITE); break; case 1: display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderMiddleGfx, AlienWidth[down], 8,WHITE); break; default: display.drawBitmap(Alien[across][down].Ord.X, Alien[across][down].Ord.Y, InvaderBottomGfx, AlienWidth[down], 8,WHITE); } } } display.display(); } |
The code plots invaders from left to right and then top to bottom, so looking at the earlier screen shot above the top left Invader is plotted first then the next one along until we reach the right most Invader. We then move down to the next row and start from the left again. Within the loop a decision is made as to which invader to actually plot. We have three rows and a different Invader per row so all we do within the plotting part of the code is check which row we are plotting and plot the appropriate Invader. The switch statement does this.
And that’s it for now, next time we’ll move onto moving and animating the invaders.
Enjoy and Learn 🙂