Dino Game

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.


The complete code

/*
 * 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
    }
}


Code Organization

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);
            }
        }
    }
}


The output

Dino game on RaspberryPi Pico

CMakeLists.txt

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)