This program was my attempt to port my Google Dino game to the RaspberryPi Pico using Prof. Hunter Adam's VGA Library for the RaspberryPi Pico. I created a dino game on the RaspberryPi Pico and implemented an additional function to draw bitmaps. The game is controlled using a pushbutton attached to a GPIO pin. The resources for the project include the C SDK User Guide, the RP2040 Datasheet and Prof. Hunter's website.
/*
* Parth Sarthi Sharma (pss242@cornell.edu)
* Code based on examples from Raspberry Pi Foundation.
* This code is an implementation of the Google dino game on the
* Raspberry Pi Pico.
*/
#include <stdio.h> //The standard C library
#include <math.h> //The standard math library
#include "pico/stdlib.h" //Standard library for Pico
#include "hardware/pio.h" //The hardware PIO library
#include "hardware/dma.h" //The hardware DMA library
#include "pico/time.h" //The pico time library
#include "hardware/gpio.h" //The hardware GPIO library
#include "Bitmaps.h" //The bitmaps library
#include "vga_graphics.h" //The graphics library
#define SCORE_X_OFFSET1 5 //x offset for text
#define SCORE_X_OFFSET2 150 //x offset for variables
#define SCORE_Y_OFFSET1 5 //y offset for score
#define SCORE_Y_OFFSET2 30 //y offset for high score
#define GROUND_SIZE 20 //Number of elements in the ground array
#define GROUND_HEIGHT 60 //Height of the ground above the base
#define HEIGHT 480 //Height of the VGA screen
#define WIDTH 640 //Width of the VGA screen
#define OBS_W_0 10 //Width of obstacle type 0
#define OBS_H_0 20 //Height of obstacle type 0
#define OBS_W_1 15 //Width of obstacle type 1
#define OBS_H_1 30 //Height of obstacle type 1
#define OBS_W_2 30 //Width of obstacle type 2
#define OBS_H_2 20 //Height of obstacle type 2
#define SPEED 5 //Speed with which the ground moves
#define GRAVITY 1.2 //The gravity value
#define JUM_VEL 15 //Velocity with which the dino jumps
#define RUNNER_FRAMES 10 //Used for alternating between 2 dino running bitmaps
#define JUMP_PIN 2 //The pin to attach the pushbutton to
unsigned char exTime, obsType; //The extra time to maintain 30fps and the obstacle type
unsigned int score = 0, highScore = 0; //Score and high score
volatile char resetGame = 1; //Flag to indicate game reset
char runner = 0; //The variable to store which how many frames have elapsed since the running bitmap was changed
char scoreBuffer[40], highScoreBuffer[40]; //The buffer to display score and high score as strings
struct Player{ //Player structure to store the x-coordinate, y-coordinate, width, height, velocity, jumped and alive status of the dino
int x, y, w, h, alive;
volatile int vy, jumped;
};
struct Ground{ //Ground structure to store each small ground element with x-coordinate, y-coordinate and width
int x, y, w;
};
struct Obstacle{ //Obstacle structure to store the x-coordinate, y-coordinate and width of the obstacle
int x, w, h;
};
inline int randomRange(int min, int max){ //Function to generate a random number between min and max
return (rand() % (max - min)) + min;
}
struct Ground ground[GROUND_SIZE]; //Ground array
struct Obstacle obstacle; //The obstacle object
struct Player myPlayer; //The player object
void DinoJump(uint gpio, uint32_t events) { //The dino jump interrupt handler
if(!myPlayer.y & myPlayer.alive){ //If the dino is alive and at ground level
myPlayer.jumped = 1; //Change the jumped status to 1
myPlayer.vy = JUM_VEL; //Change the velocity
}
else if(!myPlayer.alive){ //If the dino is dead
resetGame = 1; //Set the reset flag
}
}
int main(){ //The program running on core 0
stdio_init_all(); //Initialize all of the present standard stdio types that are linked into the binary
initVGA(); //Initialize the VGA screen and functions
srand(1); //Random seed
int i; //Counter variable
myPlayer.x = 30; //Set the dino x-coordinate to be 30
myPlayer.y = 0; //Set the dino y-coordinate to be 0
myPlayer.w = 22; //Set the dino width to be 22
myPlayer.h = 25; //Set the dino height to be 25
myPlayer.vy = 0; //Set the dino velocity to be 0
myPlayer.alive = 1; //Set the dino alive flag to be true
for(i = 0; i < GROUND_SIZE; i++){ //For all elements ranging in the ground array
ground[i].x = randomRange(0, WIDTH); //Assign them a random x-coordinate
ground[i].y = randomRange(HEIGHT - GROUND_HEIGHT + 10, HEIGHT); //Assign them a random y-coordinate
ground[i].w = randomRange(3, 10); //Assign them a random width
}
obsType = randomRange(0, 3); //Get a random obstacle type
obstacle.x = WIDTH; //Set its x-coordinate to the right most edge
switch(obsType){ //Switch based on the obstacle type
//Obstacle type 0
case 0: obstacle.w = OBS_W_0;
obstacle.h = OBS_H_0;
break;
//Obstacle type 1
case 1: obstacle.w = OBS_W_1;
obstacle.h = OBS_H_1;
break;
//Obstacle type 2
case 2: obstacle.w = OBS_W_2;
obstacle.h = OBS_H_2;
break;
}
gpio_set_irq_enabled_with_callback(JUMP_PIN, GPIO_IRQ_EDGE_FALL, true, &DinoJump); //Attach the callback to the specified GPIO
sleep_ms(5000); //Sleep for 5 seconds to let the player sit down, have a drink and get ready
setTextColor(WHITE); //Set text colour to white
setTextSize(2); //Set text size to 2
setCursor(SCORE_X_OFFSET1, SCORE_Y_OFFSET1); //Set the cursor
writeString("Score: "); //Print the text
setCursor(SCORE_X_OFFSET1, SCORE_Y_OFFSET2); //Set the cursor
writeString("High Score: "); //Print the text
sprintf(scoreBuffer, "%u", score); //Send the score into the buffer array
sprintf(highScoreBuffer, "%u", highScore); //Send the highscore into the buffer array
setCursor(SCORE_X_OFFSET2, SCORE_Y_OFFSET1); //Set the cursor
writeString(scoreBuffer); //Print the score
setCursor(SCORE_X_OFFSET2, SCORE_Y_OFFSET2); //Set the cursor
writeString(highScoreBuffer); //Print the highscore
while(1){ //While eternity
unsigned long begin_time = (unsigned long)(get_absolute_time() / 1000); //Save the start time of the loop
if(resetGame){ //If the reset flag is set
fillRect(obstacle.x, (HEIGHT - GROUND_HEIGHT - ((obstacle.h / 2))), obstacle.w, obstacle.h, BLACK); //Clear the obstacle
fillRect(myPlayer.x, (HEIGHT - GROUND_HEIGHT - (myPlayer.y + (myPlayer.h / 2))), myPlayer.w, myPlayer.h, BLACK); //Clear the player
setTextColor(BLACK); //Set the text colour to black
setTextSize(4); //Set text size to 4
setCursor(30, 200); //Set the cursor
writeString("Till death do us `Parth'"); //Clear the text
setTextSize(2); //Set text size to 2
sprintf(scoreBuffer, "%u", score); //Send the score into the buffer array
setCursor(SCORE_X_OFFSET2, SCORE_Y_OFFSET1); //Set the cursor
writeString(scoreBuffer); //Clear the score
myPlayer.y = 0; //Reset the player height
myPlayer.vy = 0; //Reset the player velocity
myPlayer.alive = 1; //Resurrect the player
score = 0; //Reset the player score
setTextColor(WHITE); //Set the text colour to white
sprintf(scoreBuffer, "%u", score); //Send the score into the buffer array
setCursor(SCORE_X_OFFSET2, SCORE_Y_OFFSET1); //Set the cursor
writeString(scoreBuffer); //Print the score
obsType = randomRange(0, 3); //Get a random obstacle type
obstacle.x = WIDTH; //Set its x-coordinate to the right most edge
switch(obsType){ //Switch based on the obstacle type
//Obstacle type 0
case 0: obstacle.w = OBS_W_0;
obstacle.h = OBS_H_0;
break;
//Obstacle type 1
case 1: obstacle.w = OBS_W_1;
obstacle.h = OBS_H_1;
break;
//Obstacle type 2
case 2: obstacle.w = OBS_W_2;
obstacle.h = OBS_H_2;
break;
}
resetGame = 0; //Clear the reset flag
}
if(myPlayer.alive){ //If the player is alive
for(i = 0; i < GROUND_SIZE; i++){ //For all elements ranging in the ground array
drawHLine(ground[i].x, ground[i].y, ground[i].w, BLACK); //Clear the older line
ground[i].x -= SPEED; //Update the ground
if(ground[i].x + ground[i].w <= 0){ //If the ground element moves out of the frame
ground[i].x = WIDTH; //Reset the ground element's x-coordinate
ground[i].y = randomRange(HEIGHT - GROUND_HEIGHT + 10, HEIGHT); //Get a new y-coordinate for the ground element
ground[i].w = randomRange(3, 10); //Get a new width for the ground element
}
}
fillRect(myPlayer.x, (HEIGHT - GROUND_HEIGHT - (myPlayer.y + (myPlayer.h / 2))), myPlayer.w, myPlayer.h, BLACK); //Clear the player's older position
fillRect(obstacle.x, (HEIGHT - GROUND_HEIGHT - ((obstacle.h / 2))), obstacle.w, obstacle.h, BLACK); //Clear the obstacle's older position
if(myPlayer.jumped){ //If the player has jumped
myPlayer.y += 1; //Change its y position
myPlayer.jumped = 0; //Reset the jump flag
}
if(myPlayer.y > 0){ //If the y position is greater than 0
myPlayer.y += myPlayer.vy; //Change the y position based on the velocity
myPlayer.vy -= GRAVITY; //Change the velocity based on the gravity
}
else if(myPlayer.y < 0){ //If the y position is less than 0
myPlayer.y = 0; //Reset y position
myPlayer.vy = 0; //Reset the velocity
}
obstacle.x -= SPEED; //Change the obstacle x position based on speed of the frame
if(obstacle.x + obstacle.w < 0){ //If the obstacle goes out of the screen
obsType = randomRange(0, 3); //Get new obstacle type
obstacle.x = WIDTH + randomRange(0, 50); //Generate the new x-coordinate for the obstacle
switch(obsType){ //Switch based on the obstacle type
//Obstacle type 0
case 0: obstacle.w = OBS_W_0;
obstacle.h = OBS_H_0;
break;
//Obstacle type 1
case 1: obstacle.w = OBS_W_1;
obstacle.h = OBS_H_1;
break;
//Obstacle type 2
case 2: obstacle.w = OBS_W_2;
obstacle.h = OBS_H_2;
break;
}
setTextColor(BLACK); //Set the text colour to black
setCursor(SCORE_X_OFFSET2, SCORE_Y_OFFSET1); //Set the cursor
writeString(scoreBuffer); //Clear the score
score++; //Increment the score
sprintf(scoreBuffer, "%u", score); //Send the score into the buffer array
setTextColor(WHITE); //Set the text colour to white
setCursor(SCORE_X_OFFSET2, SCORE_Y_OFFSET1); //Set the cursor
writeString(scoreBuffer); //Write the new score
if(score > highScore){
setTextColor(BLACK); //Set the text colour to black
setCursor(SCORE_X_OFFSET2, SCORE_Y_OFFSET2); //Set the cursor
writeString(highScoreBuffer); //Clear the highscore
highScore++;
sprintf(highScoreBuffer, "%u", highScore); //Send the highscore into the buffer array
setTextColor(WHITE); //Set the text colour to white
setCursor(SCORE_X_OFFSET2, SCORE_Y_OFFSET2); //Set the cursor
writeString(highScoreBuffer); //Write the new highscore
}
}
for(i = 0; i < GROUND_SIZE; i++){ //For all elements ranging in the ground array
drawHLine(ground[i].x, ground[i].y, ground[i].w, WHITE); //Draw the ground lines
}
switch(obsType){ //Switch based on the obstacle type, draw the corrosponding bitmap
case 0: drawBitmap(obstacle.x, (HEIGHT - GROUND_HEIGHT - ((obstacle.h / 2))), obsTypeZer, obstacle.w, obstacle.h, WHITE, BLACK);
break;
case 1: drawBitmap(obstacle.x, (HEIGHT - GROUND_HEIGHT - ((obstacle.h / 2))), obsTypeOne, obstacle.w, obstacle.h, WHITE, BLACK);
break;
case 2: drawBitmap(obstacle.x, (HEIGHT - GROUND_HEIGHT - ((obstacle.h / 2))), obsTypeTwo, obstacle.w, obstacle.h, WHITE, BLACK);
break;
}
if(myPlayer.y > 0){ //If the player is jumping
drawBitmap(myPlayer.x, (HEIGHT - GROUND_HEIGHT - (myPlayer.y + (myPlayer.h / 2))), dinoJumpUp, myPlayer.w, myPlayer.h, WHITE, BLACK); //Draw the jump bitmap
}
else{ //If the player is not jumping, alternate bitmaps depending on the frames
if(runner > RUNNER_FRAMES / 2){
drawBitmap(myPlayer.x, (HEIGHT - GROUND_HEIGHT - (myPlayer.y + (myPlayer.h / 2))), dinoRunOne, myPlayer.w, myPlayer.h, WHITE, BLACK);
}
else{
drawBitmap(myPlayer.x, (HEIGHT - GROUND_HEIGHT - (myPlayer.y + (myPlayer.h / 2))), dinoRunTwo, myPlayer.w, myPlayer.h, WHITE, BLACK);
}
}
runner = (runner + 1) % RUNNER_FRAMES; //Increment the runner and modulo based on the upper limit of the frames
drawHLine(0, HEIGHT - GROUND_HEIGHT, WIDTH, WHITE); //Draw the ground line
if((myPlayer.x + myPlayer.w > obstacle.x) && (myPlayer.x < obstacle.x + obstacle.w) && (myPlayer.y < obstacle.h)){ //If the player has hit the obstacle
myPlayer.alive = 0; //Kill the player
setTextColor(WHITE); //Set text colour to white
setTextSize(4); //Set the text size as 4
setCursor(30, 200); //Set the cursor
writeString("Till death do us `Parth'"); //Print the message
}
}
exTime = 33 - ((unsigned long)(get_absolute_time() / 1000) - begin_time); //Calculate the amount of time to sleep for to achieve 30fps
printf("%u\n", exTime); //Print the time out
sleep_ms(exTime); //Sleep for that time
}
}
The code has a VGA library which has been explained really nicely by Prof. Adams, the Bitmap header file and the main file. I have explained the working of the Dino game here. The bitmap function I used for the PIC32 version has been described here. I modified it slightly to use VGA library provided. Lastly, I used this software to create the bitmaps.
void drawBitmap(short x, short y, const unsigned char *bitmap, short w, short h, unsigned char color, unsigned char bgColor){
if((x >= _width) || (y >= _height)) return;
if((x + w - 1) >= _width) w = _width - x;
if((y + h - 1) >= _height) h = _height - y;
short i, j, byteWidth = (w + 7) / 8;
for(int j = 0; j < h; j++){
for(int i = 0; i < w; i++){
if(pgm_read_byte(bitmap + j * byteWidth + i / 8) & (128 >> (i & 7))) {
drawPixel(i + x, j + y, color);
}
else{
drawPixel(i + x, j + y, bgColor);
}
}
}
}
cmake_minimum_required(VERSION 3.13)
include(pico_sdk_import.cmake)
project(Dino-Game-project)
pico_sdk_init()
add_executable(DinoGame)
pico_enable_stdio_usb(DinoGame 1)
pico_enable_stdio_uart(DinoGame 1)
pico_generate_pio_header(DinoGame ${CMAKE_CURRENT_LIST_DIR}/hsync.pio)
pico_generate_pio_header(DinoGame ${CMAKE_CURRENT_LIST_DIR}/vsync.pio)
pico_generate_pio_header(DinoGame ${CMAKE_CURRENT_LIST_DIR}/rgb.pio)
target_sources(DinoGame PRIVATE DinoGame.c vga_graphics.c)
target_link_libraries(DinoGame PRIVATE pico_stdlib hardware_pio hardware_dma hardware_adc hardware_irq pico_time)
pico_add_extra_outputs(DinoGame)