Reckless Racer Arduino OLED Game, AdafruitGFX and Bitmaps Basics
by asmgabber in Circuits > Arduino
3823 Views, 9 Favorites, 0 Comments
Reckless Racer Arduino OLED Game, AdafruitGFX and Bitmaps Basics
In this tutorial we will be looking at how to use bitmaps using Adafruit_GFX.c library as sort of sprites in a game. The simplest game we could think of is a side scrolling lane changing car game, in the end our beta tester and assistant coder decided on "Reckless Racer" as a name, as its pretty reckless to drive the wrong way down the motorway!!.
The Design of our circuit is in the pictures included above and is detailed in our last project/tutorial here Snake Instructables which describes how the circuit works.
we will require
and please take a look at the Snake tutorial for the rest of the equipment.
Supplies
Installing Paint.net
We are using paint.net as the software is free so completely free to use you can download Paint.Net here.
To install paint.net double click the downloaded program and answer positively such next, yes, ok, i agree and the pictures above will give you a directions.
Drawing a Simple Splash Screen
When you are in paint.net create a new image by clicking File then new, set the image size to 1260x620 (see first pic) click ok when you have you new page draw a splash screen using only 2 colours black and white using the pencil tool(pic2),
when you have draw (or pasted in) your splash screen image, click on image then resize(image4), in the pop up change the size from 1260x620 to 126x62(2 pixels smaller than your display) (pic5) Click OK.
next click File menu then save as (pic6).
when the pop up apears in the file type drop down menu select BMP (bitmap).(pic7), type in a file name and click save, when the pop up appears set dithering to 0 and set to 8 bit, click ok (pic8).
Converting BMP to C Bitmap File
Now we need to convert our image to a format that the arduino can understand there are plenty of tools available to do this but my "go to" place the is marlin webs site bitmap converter tool ...
http://marlinfw.org/tools/u8glib/converter.html
So we start this section by using the above link to open the website which is shown in pic1
click on choose file and select the bitmap you created earlier (pic2)
marlin bitmap converter will automatically convert your image into c code left double click the code which should highlight the code, then right click and click copy(pic3)
next We create right click and create a new text document (pic4)
double click the new document, when opened right click and paste the code(pic5)
next we have to add the line near the top of the code #include <avr/pgmspace.h> this allows us to save the bitmap data to the flash memory on the arduino, then we rename the #define width, height and name to something easier to use these are highlighted in pic 6,
we rename them from the randomly generate characters we rename them to the underlined example below
#define LOGOWIDTH
#define LOGOHEIGHT
const unsigned char LOGOPIC [] PROGMEM
next click file then save as, save the file as logo.c close notepad, right click on logo.c and click copy.
Displaying a LOGO Using DrawBitmap
Now we load the arduino IDE and create new sketch and save it named as logoexample.ino, next as a cheat in the arduino ide click the file menu then save as, go back to the project folder right click and paste in the .c file (pic2) then click cancel, this saves you having to browse to the folder to paste in the file.
type the following code into the arduino IDE or download in ino.
(we recommend typing rather than copy and paste or using the files below its the best way to learn)
#include <c:\arduino\logoexample1\logo.c> /* this will vary depending on where you store
the ino usually in the folder C:\Users\~username\Documents\Arduino\project~name and this is how we link to our bitmap */
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
uint8_t bmpX,bmpY=0; /* reserve memory for 2 X 8 bit integers, we only need 8 bit ints as the value is never higher than 128 (pixels) so we can save space using 8 bit ints(which has a maximum value of 255) */
void setup() { delay(100); // give display etc time to power on display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // this is to initialise the display display.clearDisplay(); // start with a blank screen }
/* please note you dont have to type these comments in they are for reference.............. the command we are going to focus on is the display.drawBitmap, this is what draws our splash screen. (bmpX, is the X axis value on the screen where the X anchor point of the bitmap will be and bmpX and bmpY are the values we are interested in to create movement (bmpY, is the Y axis value on the screen where the Y anchor point of the bitmap will be we get the reference names as we defined them in logo.c (LOGOPIC, is the name of the bitmap in the #included file logo.c (LOGOWIDTH, is how many pixels accross (X) to draw the bitmap from the anchor point (LOGOHEIGHT, is how many pixels down (Y) to draw the bitmap from the anchor point the X and Y pixels accross can be entered manually but its just easier to use the ones predefined than remember them all (1, the last value is the colour as the screen is mono 0 black 1 white. okay start typing from next line :¬D lol*/ void loop() { display.clearDisplay(); // blank the screen // bitmap drawn from the top left,x,y,nameof bitmap, width X, height Y, colour display.drawBitmap(bmpX,bmpY,LOGOPIC,LOGOWIDTH,LOGOHEIGHT,1); display.display(); // this actually draws the buffer to the display ever }
upload you code you arduino and confirm that it is working (pic3).
Moving a Bitmap Sprite
using the earlier instructions use paint.net and make a new file make it 30x15 pixels (pic1) and draw a rough car our young designer starts with the windscreen first (pics 2&3).
again save it as a windows bmp file (as in step2), convert to a C bitmap(step3) and place the car.c file (or what ever you decide upon) in the same folder as a newly created arduino ino (sketch) file.
(p.s. remember to add the #include line in car.c this used to catch us out often)
First link your equivalent of car.c
#include <c:\arduino\logoexample2\car.c> #include <SPI.h> #include Adafruit_GFX.h> // <a href="https://github.com/adafruit/Adafruit-GFX-Library" rel="nofollow"> https://github.com/adafruit/Adafruit-GFX-Library </a>#include Adafruit_SSD1306 // <a href="https://github.com/adafruit/Adafruit_SSD1306" rel="nofollow"> https://github.com/adafruit/Adafruit-GFX-Library >
Adafruit_SSD1306 display(128,64); // set the resolution of the display
/* bmpX/bmpY we need these to be variables as changing these values and redrawing the screen is how we create movement animation effect. hitSide and hitTop is how we keep sprite in the screen */ uint8_t bmpX,bmpY=0; // reserve memory for 2 8 bit ints (0-255) we dont need larger 128 will be the biggest number used bool hitSide=0; bool hitTop=0;
void setup() { delay(100); // give display etc time to power on display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // this is to initialise the display display.clearDisplay(); // start with a blank screen
}
void loop() { display.clearDisplay(); // blank the screen // bitmap drawn from the top left,x,y,name of bitmap, width X, height Y, colour display.drawBitmap(bmpX,bmpY,CARSPRITE,CARWIDTH,CARHEIGHT,1); // display.display(); // this actually draws the buffer to the display ever /* this is how we track the edge of the screen and decide whether to add a pixel move top to bottom) or remove a pixel(move bottom to top) */ switch (hitSide) // this chooses the direction of the car based on the boole { case 0: bmpX++; break;
case 1: bmpX--; break; } // these 2 if statements sets the bool to true or false if(bmpX==0) { hitSide=0; } if(bmpX==96) // the width of the screen minus the car { hitSide=1; } // same as above for the Y axis if(bmpY==0) { hitTop=0; } if(bmpY==49) // screen height minus the car height { hitTop=1; } switch (hitTop) { case 0: bmpY++; break; case 1: bmpY--; break; }
}
you can see the program working in the video attached
Making the Driving Game
First We start by drawing a few different cars or obstacles like in the earlier stages of the tutorial making them 30x15 pixels. Then we convert them to c bitmaps and link in the code.
#include <C:\Arduino\car_sprite_game_v1.4\car.c> // these paths will have to change dependant // on where you store the files //edit: i have just found out if you replace // < > with " " you don't require the full path //with your own libraries #include <C:\Arduino\car_sprite_game_v1.4\car0.c>
#include <C:\Arduino\car_sprite_game_v1.4\car2.c> #include <C:\Arduino\car_sprite_game_v1.4\car3.c> #include <c:\Arduino\car_sprite_game_v1.4\crash.c>
#include <SPI.h> #include <Adafruit_GFX.h> // <a href="https://github.com/adafruit/Adafruit-GFX-Library" rel="nofollow"> https://github.com/adafruit/Adafruit-GFX-Library<...> #include <Adafruit_SSD1306 // <a href="https://github.com/adafruit/Adafruit_SSD1306" rel="nofollow"> https://github.com/adafruit/Adafruit-GFX-Library<... </a> <p>Adafruit_SSD1306 display(128,64); // define the display parameters</p>
define the variables and fixed values
// define input pins these are the pins on the arduino they never change so #define<br>#define INTPIN 3 // only pins 2 and 3 can be interupt pins on UNO #define UPPIN 4 // these are pins connected tp relevant switch #define DWNPIN 5 #define LFTPIN 6 #define RHTPIN 7 #define SND 9 // define directions
#define DIRUP 1 // these values is what the "snake" looks at to decide- #define DIRDOWN 2 // the direction the snake will travel #define DIRLEFT 3 #define DIRRIGHT 4
uint8_t dirPressed =0; // value to register direction to move on which pin went high
// booleans store which pin went high bool BUTUP =0; bool BUTDWN=0; bool BUTLFT=0; bool BUTRHT=0; // vars for the position of the car uint8_t carPosX=1; uint8_t carPosY[] ={0,16,32,48}; // needs value cahnging the the array
uint8_t lanePosArr[]={0,16,32,48}; // array to store where each lane is uint8_t carPosYCnt =0; uint8_t carYTmp=0; // variables for the lines in the road uint8_t roadLineX1=51; // these are predefined at the begining then the lines appear seamless uint8_t roadLineX2=102; uint8_t roadLineX3=153; uint8_t roadLineX4=254; uint8_t roadLineX5=200;
// this is how many pixels the play area moves at a time uint8_t drawSpeed = 4;
// vars for enemy0 uint8_t enemy0PosX=255; uint8_t enemy0PosY=0; uint8_t enemy1PosX=255; uint8_t enemy1PosY=0; uint8_t enemy2PosX=255; uint8_t enemy2PosY=0;
// varialble to random assign a lane number to obstacles
uint8_t laneGen=0; uint8_t laneGen0=0; uint8_t laneGen1=0; uint8_t laneGen2=0;
// score counter long score=0; // this is the score :/ lol long compare=0; // this stores the score at the last level up to compare back to long highScore=25; uint8_t metreCnt=0;
this is where we start the functions
// this is the set of commands if the interrupt is activated<br> void interruptressed() { delay(150); updateDirection(); } // update which value is in the direction var by checking the DIR bools //-------------------------- UPDATE DIRECTION (player) -------------------------- void updateDirection() { //Serial.println("updateDirection Called"); BUTUP= digitalRead(UPPIN); BUTDWN=digitalRead(DWNPIN); BUTLFT=digitalRead(LFTPIN); BUTRHT=digitalRead(RHTPIN); if(BUTUP==true) { dirPressed=DIRUP; } if(BUTDWN==true) { dirPressed=DIRDOWN; } if(BUTLFT==true) { dirPressed=DIRLEFT; } if(BUTRHT==true) { dirPressed=DIRRIGHT; }
}
//------------------------------- MOVE CAR ---------------------------------------- // this will update the screen the mover the car sprite
void moveCar() { switch (dirPressed) { case DIRUP : carPosYCnt--; carPosY[carPosYCnt]; tone(SND,100,100); if (carPosYCnt ==255) { carPosYCnt=0; } carYTmp=carPosY[carPosYCnt]; dirPressed=0; // Serial.println("carPosY up"); // Serial.println(carPosYCnt); break; case DIRDOWN : carPosYCnt++; tone(SND,100,100); if(carPosYCnt==4) { carPosYCnt=3; } // Serial.println("carPosY"); // Serial.println(carPosYCnt); carYTmp=carPosY[carPosYCnt]; dirPressed=0; break; // commented out car able to move left and right collision detection not that good yet /* case DIRLEFT : carPosX--; if(carPosX==0) { carPosX=1; } // Serial.println("carPosX"); // Serial.println(carPosX); dirPressed=0; break; */ case DIRRIGHT : // just for fun if you press right the game will make a noise tone(SND,100,50); // carPosX++; // if (carPosX==128) // { // carPosX=127; // } // Serial.println("carPosX"); // Serial.println(carPosX); // dirPressed=0; break; } updateDisplay(); }
//-------------------------- RANDOM POS X ------------------------------
uint8_t randomPosX() // these 2 routines just generate random position for the obstacles { uint8_t posValTmp=0; posValTmp= random(129,230); //Serial.println("random x"); //Serial.println(posValTmp); return(posValTmp); }
//--------------------------- RANDOM POS Y------------------------------------
uint8_t randomPosY() { uint8_t laneVal =0; laneVal= random(0,4); // add an extra lane for randomness i.e no object on screen while in that lane //Serial.println("RandomY"); //Serial.println(lanePosArr[laneVal]); return(lanePosArr[laneVal]); }
//-------------------------------SET GAME SPEED---------------------------- void setGameSpeed() // this stops the level going higher than 20 making game unplayable {
if(drawSpeed<21) { drawSpeed=drawSpeed+2; } }
// ------------------------------------ DETECT CRASH--------------------------------- void detectCrash() {
if(enemy0PosX<=28&&enemy0PosX>=0&&enemy0PosY==carYTmp) { // Serial.println("Game Over CRAASSSSHHHHHHHEEEEDDD into Traffic 0"); gameOver(); } if(enemy1PosX<=28&&enemy1PosX>=0&&enemy1PosY==carYTmp) { //Serial.println("Game Over CRAASSSSHHHHHHHEEEEDDD into traffic 1"); gameOver(); } if(enemy2PosX<=28&&enemy2PosX>=0&&enemy2PosY==carYTmp) { //Serial.println("Game Over CRAASSSSHHHHHHHEEEEDDD into traffic 2"); gameOver(); } }
these are the routines that draw the display.
//------------------------------- DRAW ROAD------------------------------------<br> void drawRoad() // X , Y, length,width { display.fillRect(roadLineX1,15,30,4,WHITE); display.fillRect(roadLineX1,30,30,4,WHITE); display.fillRect(roadLineX1,45,30,4,WHITE); display.fillRect(roadLineX2,15,30,4,WHITE); display.fillRect(roadLineX2,30,30,4,WHITE); display.fillRect(roadLineX2,45,30,4,WHITE); display.fillRect(roadLineX3,15,30,4,WHITE); display.fillRect(roadLineX3,30,30,4,WHITE); display.fillRect(roadLineX3,45,30,4,WHITE); display.fillRect(roadLineX4,15,30,4,WHITE); display.fillRect(roadLineX4,30,30,4,WHITE); display.fillRect(roadLineX4,45,30,4,WHITE); display.fillRect(roadLineX5,15,30,4,WHITE); display.fillRect(roadLineX5,30,30,4,WHITE); display.fillRect(roadLineX5,45,30,4,WHITE);
roadLineX1=roadLineX1-drawSpeed; roadLineX2=roadLineX2-drawSpeed; roadLineX3=roadLineX3-drawSpeed; roadLineX4=roadLineX4-drawSpeed; roadLineX5=roadLineX5-drawSpeed; display.display(); } //-----------------------------------------DRAW enemys ------------------------------------------- void enemysDraw() { // X, Y, bmp name, width, height, colour display.drawBitmap(enemy0PosX,enemy0PosY,ENEMY0,ENEMY0_WIDTH,ENEMY0_HEIGHT,1); enemy0PosX=enemy0PosX-drawSpeed; display.drawBitmap(enemy1PosX,enemy1PosY,ENEMY1,ENEMY1_WIDTH,ENEMY1_HEIGHT,1); enemy1PosX=enemy1PosX-drawSpeed; display.drawBitmap(enemy2PosX,enemy2PosY,ENEMY2,ENEMY2_WIDTH,ENEMY2_HEIGHT,1); enemy2PosX=enemy2PosX-drawSpeed; display.display(); if(enemy0PosX>231&&enemy0PosX<255) { enemy0PosX= randomPosX(); enemy0PosY= randomPosY(); // Serial.println(enemy0PosX); // Serial.println(enemy0PosY); } if(enemy1PosX>231&&enemy1PosX<255) { enemy1PosX=randomPosX(); enemy1PosY=randomPosY(); checkDuplicate(); }
if(enemy2PosX>231&&enemy2PosX<255) { enemy2PosX=randomPosX(); enemy2PosY=randomPosY(); } } //------------------------------------ UPDATE DISPLAY------------------------------------------------ void updateDisplay() { display.clearDisplay(); display.drawBitmap(carPosX,carPosY[carPosYCnt],CARSPRITE,30,15,1); display.fillRect(100,0,28,10,BLACK); display.setCursor(100,0); display.setTextColor(WHITE,BLACK); display.println(score); display.display();
}
//-------------------------wait for presss loop ------------------------- // this is the home screen code void waitForPress() { splashScreen(); bool waiting=0; // loop ends when this is true display.clearDisplay(); while(waiting==0) {
display.fillRect(19,20,90,32,BLACK); // blank background for text display.setTextColor(WHITE); display.setCursor(23,24); display.setTextSize(0); display.println("Reckless"); display.setCursor(36,34); display.println("Racer"); display.drawBitmap(74,24,CARSPRITE,CARWIDTH,CARHEIGHT,1); // x y w h r col display.drawRoundRect(21,21,86,23,4,WHITE); // border Snake display.drawRect(19,20,90,33,WHITE); // border box - 3 display.setCursor(25,43); display.setTextSize(0); // font back to normal display.println("press any key"); display.fillRect(0,0,127,8,BLACK); display.setCursor(10,0); display.print("High Score :"); // display the high score display.print(highScore); display.display(); waiting = digitalRead(INTPIN); // check to see if key pressed waiting will change to 1 ending while dirPressed=0; // reset button press to no direction } } //-------------------------------------------UPDATE GAME----------------------------------------- void updateGame() { moveCar(); drawRoad(); enemysDraw(); //enemy1Draw(); // enemy2Draw(); metreCnt++; detectCrash(); if(metreCnt==5)// adds a point for every 10 cycles to clock up score { metreCnt=0; score++; } if(score==compare+5) // speeds up the game every 5 points to a maximum of 20 speed { compare=score; setGameSpeed(); } noTone(SND); updateDisplay();
}
// ------------------------------ GAME OVER---------------------------------------------- // this routine draws the lines around the dead hero car then displays the game over screen
void gameOver() { tone(SND,200,200); // play sound uint8_t linePosX,linePosY,pixwidth,pixheight=0; // set vars to draw boxes around car linePosX=carPosY; linePosY=carYTmp; pixwidth=30; pixheight=15; display.drawRect(linePosX,linePosY,pixwidth,pixheight,WHITE); display.display(); for(int i=0;i<=26;i++) // this surrounds car in rectangles simulating explosion { linePosX=linePosX-2; linePosY=linePosY-2; pixwidth=pixwidth+4; pixheight=pixheight+4; display.drawRect(linePosX,linePosY,pixwidth,pixheight,BLACK); display.drawRect(linePosX,linePosY,pixwidth,pixheight,WHITE); display.display(); tone(SND,i*20,50); delay(10); } display.setTextSize(2); display.setTextColor(WHITE,BLACK); display.setCursor(10,23); tone(SND,50,500); display.print("GAME "); display.display(); delay(500); tone(SND,40,500); display.print("OVER"); display.setTextSize(0); display.display(); delay(3000); restartGame(); waitForPress(); }
// ----------------------------------------- RESTART GAME ------------------------------------------------------------
void restartGame() // this copys high score and resets all stats and generates random positions { if(score>=highScore) //check to see if score higher than high score { highScore=score; //single if statment to update high score }
score=0; drawSpeed=4; metreCnt=0; carPosYCnt=0; enemy0PosX=randomPosX(); enemy0PosY=randomPosY(); enemy1PosX=randomPosX(); enemy1PosY=randomPosY(); enemy2PosX=randomPosX(); enemy2PosY=randomPosY(); noTone(SND);
checkDuplicate();
} //------------------------------------------------- CHECK DUPLICATE----------------------------------------------------- void checkDuplicate() // these check to see if obstacles occupy the same game space { // Serial.println("duplicate checked"); if(enemy2PosX>230&&enemy2PosX<255) { while(enemy2PosY==enemy1PosY||enemy2PosY==enemy0PosY) { enemy2PosY=randomPosY(); } }
if(enemy0PosX>230&&enemy0PosX<255) { while(enemy0PosY==enemy1PosY||enemy0PosY==enemy2PosY) { enemy0PosY=randomPosY(); } } if(enemy2PosX>230&&enemy2PosX<255) { while(enemy2PosX==enemy1PosX||enemy2PosX==enemy0PosX||enemy2PosX>enemy1PosX&&enemy2PosX230&&enemy0PosX<255) { while(enemy0PosX==enemy1PosX||enemy0PosX==enemy2PosX||enemy0PosX>enemy1PosX&&enemy0PosX
//------------------------------------------- SPLASH SCREEN -----------------------------------
void splashScreen() { display.clearDisplay(); display.drawBitmap(0,0,CRASH,CRASHWIDTH,CRASHHEIGHT,1); display.display(); delay (2000); } //----------------------------------------------- SETUP ------------------------------------------------------------ void setup() { delay(100); // let things start up // Serial.begin(9600); // uncomment this and all the Serial. commands for fault diag display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.clearDisplay(); display.setTextColor(WHITE,BLACK); display.setTextWrap(false); display.dim(0); pinMode(INTPIN,INPUT); pinMode(UPPIN,INPUT); pinMode(DWNPIN,INPUT); pinMode(LFTPIN,INPUT); pinMode(RHTPIN,INPUT);
attachInterrupt(digitalPinToInterrupt(INTPIN),interruptressed,RISING); // randomly place obstructions enemy0PosX=randomPosX(); enemy0PosY=randomPosY(); enemy1PosX=randomPosX(); enemy1PosY=randomPosY(); enemy2PosX=randomPosX(); enemy2PosY=randomPosY(); checkDuplicate(); // check for duplicate locations // Serial.println("setup Complete"); splashScreen(); waitForPress(); } //---------------------------------------------------- LOOP ------------------------------------------------------
void loop() { updateGame(); }
and that's pretty much it, any modifications and feedback will be welcome. Issues we need to address screen flicker we need to look into how to lessen it and the enemy cars are still able to occupy the same space.