This program was my attempt recreate the Snake game) on the RaspberryPi Pico using Prof. Hunter Adam's VGA Library for the RaspberryPi Pico. The game is controlled using pushbuttons attached to 4 different GPIO pins(one for each direction the snake can move in. 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 famous snake game.
* It uses 4 GPIO pins to control the snake which has to eat the food
* in order to grow in length.
*/
#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 "vga_graphics.h" //The graphics library
#define HEIGHT 480 //Height of the VGA screen
#define WIDTH 640 //Width of the VGA screen
#define BOARD_HEIGHT (HEIGHT / 4) //The height if the board
#define BOARD_WIDTH (WIDTH / 4) //The width of the board
//The directions
#define UP 0 //The UP state
#define DOWN 1 //The DOWN state
#define LEFT 2 //The LEFT state
#define RIGHT 3 //The RIGHT state
//The GPIO pins
#define UP_PIN 3 //The UP GPIO pin
#define DOWN_PIN 2 //The DOWN GPIO pin
#define LEFT_PIN 5 //The LEFT GPIO pin
#define RIGHT_PIN 4 //The RIGHT GPIO pin
//Structure to hold the node (each point) of the snake.
//The snake is constructed using a linked list type data structure.
typedef struct Node{ //Node structure
short x, y; //The x coordinate and the y coordinate of the node
struct Node *next; //The next node of the snake
}Node;
Node *head = NULL; //The head of the snake
short food[2]; //The array to hold x-coordinate and the y-coordinate of the food
volatile char direction = RIGHT; //The current direction of the snake
char alive = 1; //The flag that determines if the snake is alive
volatile char reset = 1; //The reset flag
inline int randomRange(int min, int max){ //Function to generate a random number between min and max
return (rand() % (max - min)) + min;
}
void swap(short *x, short *y){ //Function to swap two shorts
if(*x != *y){
*x = *x ^ *y;
*y = *x ^ *y;
*x = *x ^ *y;
}
}
void genFood(){ //The function to generate the food
genAgain:
food[0] = randomRange(0, BOARD_WIDTH); //Get a random x coordinate
food[1] = randomRange(0, BOARD_HEIGHT); //Get a random y coordinate
Node *current = head; //Create a new current node
current = current->next; //Make the current node point to the next node
while(current != NULL){ //Iterate to the end of the list
if(food[0] == current->x && food[1] == current->y){ //If at any point the food lies on the body of the snake
goto genAgain; //Generate new coordinates for the food
}
current = current->next; //Else point to the next node
}
fillRect(food[0] << 2, food[1] << 2, 4, 4, YELLOW); //Draw a yellow rectangle for food
}
void addNode(short x, short y){ //Function to add a new node for the snake
if(head == NULL){ //If the linked list is empty
head = (Node *)malloc(sizeof(Node)); //Allocate memory for the head
head->x = x; //Set x-coordinate for the head
head->y = y; //Set y-coordinate for the head
head->next = NULL; //Make the next of head as NULL
fillRect(x << 2, y << 2, 4, 4, RED); //Draw a red rectangle for head
}
else{ //Else
Node *current = head; //Create a new pointer pointing to the head
while(current->next != NULL){ //Iterate to the last element of the list
current = current->next;
}
current->next = (Node *)malloc(sizeof(Node)); //Allocate new memory for the next node
current->next->x = x; //Set x-coordinate for the new node
current->next->y = y; //Set y-coordinate for the new node
current->next->next = NULL; //Set the next node as NULL
fillRect(x << 2, y << 2, 4, 4, GREEN); //Draw a green rectangle for the list
}
}
void move(short x, short y){ //Function to move the snake such that the new coordinates of the head are x, y
fillRect(x << 2, y << 2, 4, 4, RED); //Draw the new head
Node *current = head; //Create a new pointer pointing to the head
while(current != NULL){ //For all the elements of the snake's body
fillRect(current->x << 2, current->y << 2, 4, 4, GREEN); //Draw the new snake body
//Swap the coordinates and the current node coordinates
swap(&(current->x), &x);
swap(&(current->y), &y);
current = current->next; //Go over to the next node
if(current != NULL){ //If the current node is not null
if(current->x == head->x && current->y == head->y){ //If the snake eats itself
alive = 0; //Kill it
fillRect(head->x << 2, head->y << 2, 4, 4, RED); //Draw the head one last time
setTextColor(WHITE); //Set text colour to white
setTextSize(4); //Set the text size as 4
setCursor(100, 200); //Set the cursor
writeString("Death awaits us all"); //Print the message
break;
}
}
}
fillRect(x << 2, y << 2, 4, 4, BLACK); //Clear the oldest element
}
void eatAndMove(short x, short y){ //Function to eat the food and still move by increasing the length of the snake
fillRect(x << 2, y << 2, 4, 4, RED); //Draw the new head
Node *current = head; //Create a new pointer pointing to the head
while(current->next != NULL){ //For all the elements until the second last element of the snake's body
//Store the old values of x and y coordinates in the temporary placeholders
fillRect(current->x << 2, current->y << 2, 4, 4, GREEN); //Draw the new snake body
//Swap the coordinates and the current node coordinates
swap(&(current->x), &x);
swap(&(current->y), &y);
current = current->next; //Go over to the next node
}
current->next = (Node *)malloc(sizeof(Node)); //Allocate new memory for the next node
//Update the new coordinates of the body
fillRect(current->x << 2, current->y << 2, 4, 4, GREEN); //Draw the new snake body
//Swap the coordinates and the current node coordinates
swap(&(current->x), &x);
swap(&(current->y), &y);
current = current->next; //Go over to the next node
//Update the last element of the body
current->x = x;
current->y = y;
current->next = NULL; //Make the last element point to NULL
}
void moveUp(){ //The function to move UP
if(head->x == food[0] && head->y == food[1]){ //If the head is on the food
if(head->y == 0){ //If the head is on the top edge of the screen
eatAndMove(head->x, BOARD_HEIGHT - 1); //Eat the food and move to the bottom edge of the screen
}
else{ //Else
eatAndMove(head->x, head->y - 1); //Eat the food and move UP
}
genFood(); //Generate new food
}
else{ //Else
if(head->y == 0){ //If the head is on the top edge of the screen
move(head->x, BOARD_HEIGHT - 1); //Move to the bottom edge of the screen
}
else{ //Else
move(head->x, head->y - 1); //Move UP
}
}
}
void moveDown(){ //The function to move DOWN
if(head->x == food[0] && head->y == food[1]){ //If the head is on the food
if(head->y == BOARD_HEIGHT - 1){ //If the head is on the bottom edge of the screen
eatAndMove(head->x, 0); //Eat the food and move to the top edge of the screen
}
else{ //Else
eatAndMove(head->x, head->y + 1); //Eat the food and move DOWN
}
genFood(); //Generate new food
}
else{ //Else
if(head->y == BOARD_HEIGHT - 1){ //If the head is on the bottom edge of the screen
move(head->x, 0); //Move to the top edge of the screen
}
else{ //Else
move(head->x, head->y + 1); //Move DOWN
}
}
}
void moveLeft(){ //The function to move LEFT
if(head->x == food[0] && head->y == food[1]){ //If the head is on the food
if(head->x == 0){ //If the head is on the left edge of the screen
eatAndMove(BOARD_WIDTH - 1, head->y); //Eat the food and move to the right edge of the screen
}
else{ //Else
eatAndMove(head->x - 1, head->y); //Eat the food and move LEFT
}
genFood(); //Generate new food
}
else{ //Else
if(head->x == 0){ //If the head is on the left edge of the screen
move(BOARD_WIDTH - 1, head->y); //Move to the right edge of the screen
}
else{ //Else
move(head->x - 1, head->y); //Move LEFT
}
}
}
void moveRight(){ //The function to move RIGHT
if(head->x == food[0] && head->y == food[1]){ //If the head is on the food
if(head->x == BOARD_WIDTH - 1){ //If the head is on the right edge of the screen
eatAndMove(0, head->y); //Eat the food and move to the left edge of the screen
}
else{ //Else
eatAndMove(head->x + 1, head->y); //Eat the food and move RIGHT
}
genFood(); //Generate new food
}
else{ //Else
if(head->x == BOARD_WIDTH - 1){ //If the head is on the right edge of the screen
move(0, head->y); //Move to the left edge of the screen
}
else{ //Else
move(head->x + 1, head->y); //Move RIGHT
}
}
}
void changeDir(uint gpio, uint32_t events) { //The change direction interrupt handler
if(alive){ //If the snake is alive
switch(gpio){ //Switch based on the GPIO
case UP_PIN: direction = (direction == DOWN ? DOWN : UP); //If the command is to go UP and the snake isn't going DOWN, go UP
break;
case DOWN_PIN: direction = (direction == UP ? UP : DOWN); //If the command is to go DOWN and the snake isn't going UP, go DOWN
break;
case LEFT_PIN: direction = (direction == RIGHT ? RIGHT : LEFT); //If the command is to go LEFT and the snake isn't going RIGHT, go LEFT
break;
case RIGHT_PIN: direction = (direction == LEFT ? LEFT : RIGHT); //If the command is to go RIGHT and the snake isn't going LEFT, go RIGHT
break;
default: break;
}
}
else{ //If the snake is dead
reset = 1; //Set the reset flag
}
}
int main(){
short i; //An interator
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
gpio_set_irq_enabled_with_callback(UP_PIN, GPIO_IRQ_EDGE_FALL, true, &changeDir); //Attach the callback to the specified GPIO
gpio_set_irq_enabled_with_callback(DOWN_PIN, GPIO_IRQ_EDGE_FALL, true, &changeDir); //Attach the callback to the specified GPIO
gpio_set_irq_enabled_with_callback(LEFT_PIN, GPIO_IRQ_EDGE_FALL, true, &changeDir); //Attach the callback to the specified GPIO
gpio_set_irq_enabled_with_callback(RIGHT_PIN, GPIO_IRQ_EDGE_FALL, true, &changeDir); //Attach the callback to the specified GPIO
while(1){ //While eternity
if(reset){ //If the reset flag has been set
fillRect(0, 0, WIDTH, HEIGHT, BLACK); //Clear the screen
alive = 1; //Resurrect the snake
//Clear the linked list
Node *tmp; //Create a temporary node
while(head != NULL){ //While head exists
tmp = head; //Make the temp as head
head = head->next; //Head is the next of itself
free(tmp); //Free the temp
}
//Create the snake
for(i = 0; i < 8; i++){
addNode((BOARD_WIDTH / 2) - i, BOARD_HEIGHT / 2);
}
direction = RIGHT; //Set default direction to be RIGHT
genFood(); //Generate food
reset = 0; //Clear the reset flag
}
while(alive){ //While the snake is alive
unsigned long begin_time = (unsigned long)(get_absolute_time() / 1000); //Save the start time of the loop
switch(direction){ //Switch based on the direction the snake is moving in
case UP: moveUp(); //If the direction is UP, move UP
break;
case DOWN: moveDown(); //If the direction is DOWN, move DOWN
break;
case LEFT: moveLeft(); //If the direction is LEFT, move LEFT
break;
case RIGHT: moveRight(); //If the direction is RIGHT, move RIGHT
break;
default: break;
}
sleep_ms(33 - ((unsigned long)(get_absolute_time() / 1000) - begin_time)); //Sleep for the amount of time to achieve 30fps
}
}
}
The code has a VGA library which has been explained really nicely by Prof. Adams and the main file. Most of the code is self explanatory, however, the main components of the game are explained in the following subsections.
First and foremost, the snake is implemented in the form of a linked list. According to Wikipedia, a linked list is a linear collection of data elements whose order is not given by their physical placement in memory. Instead, each element points to the next. It is a data structure consisting of a collection of nodes which together represent a sequence. In its most basic form, each node contains: data, and a reference (in other words, a link) to the next node in the sequence. Each node in our snake linked list holds the x-coordinate of the node, the y-coordinate of the node and the pointer to the next node. Therefore, moving the snake is nothing more than shifting the data of the nodes of the linked list.
typedef struct Node{
short x, y;
struct Node *next;
}Node;
The function to generate food is pretty simple. First, I randomly generate x and y coordinates for the food. Then, I iterate through the linked list. If the food coordinates coincide with the snake's body, I regenerate the food coordinates.
void genFood(){
genAgain:
food[0] = randomRange(0, BOARD_WIDTH);
food[1] = randomRange(0, BOARD_HEIGHT);
Node *current = head;
current = current->next;
while(current != NULL){
if(food[0] == current->x && food[1] == current->y){
goto genAgain;
}
current = current->next;
}
fillRect(food[0] << 2, food[1] << 2, 4, 4, YELLOW);
}
Two functions which are responsible for motion are move()
and eatAndMove()
. These functions are responsible for the normal motion and the motion of the snake respectively. They're quite similar, except for the fact that eatAndMove()
increases the length of the snake by adding a new element at the end of the linked list. Both these functions work by shifting the data in the linked list.
void move(short x, short y){
fillRect(x << 2, y << 2, 4, 4, RED);
Node *current = head;
while(current != NULL){
fillRect(current->x << 2, current->y << 2, 4, 4, GREEN);
swap(&(current->x), &x);
swap(&(current->y), &y);
current = current->next;
if(current != NULL){
if(current->x == head->x && current->y == head->y){
alive = 0;
fillRect(head->x << 2, head->y << 2, 4, 4, RED);
setTextColor(WHITE);
setTextSize(4);
setCursor(100, 200);
writeString("Death awaits us all");
break;
}
}
}
fillRect(x << 2, y << 2, 4, 4, BLACK);
}
void eatAndMove(short x, short y){
fillRect(x << 2, y << 2, 4, 4, RED);
Node *current = head;
while(current->next != NULL){
fillRect(current->x << 2, current->y << 2, 4, 4, GREEN);
swap(&(current->x), &x);
swap(&(current->y), &y);
current = current->next;
}
current->next = (Node *)malloc(sizeof(Node));
fillRect(current->x << 2, current->y << 2, 4, 4, GREEN);
swap(&(current->x), &x);
swap(&(current->y), &y);
current = current->next;
current->x = x;
current->y = y;
current->next = NULL;
}
cmake_minimum_required(VERSION 3.13)
include(pico_sdk_import.cmake)
project(Snake-project)
pico_sdk_init()
add_executable(Snake)
pico_enable_stdio_usb(Snake 1)
pico_enable_stdio_uart(Snake 1)
pico_generate_pio_header(Snake ${CMAKE_CURRENT_LIST_DIR}/hsync.pio)
pico_generate_pio_header(Snake ${CMAKE_CURRENT_LIST_DIR}/vsync.pio)
pico_generate_pio_header(Snake ${CMAKE_CURRENT_LIST_DIR}/rgb.pio)
target_sources(Snake PRIVATE Snake.c vga_graphics.c)
target_link_libraries(Snake PRIVATE pico_stdlib hardware_pio hardware_dma hardware_adc hardware_irq pico_time pico_multicore)
pico_add_extra_outputs(Snake)