Skip to content

Snake

In this tutorial, you’ll build a small Snake game.

Snake is a good beginner game because the screen can be treated as a grid. The snake is a list of grid cells, the fruit is one grid cell, and every tick moves the snake by one cell.

You will learn how to:

  • Store game objects as grid positions
  • Draw blocks into VRAM
  • Move the snake one step at a time
  • Use key events to change direction
  • Detect fruit, wall, and self collisions

Start with the headers used by the demo:

#include <appdef.h>
#include <sdk/os/debug.h>
#include <sdk/os/input.h>
#include <sdk/os/lcd.h>
#include <sdk/calc/calc.h>

debug.h prints the score, input.h reads keys and timer events, and lcd.h gives access to vram.

APP_NAME("Snake")
APP_DESCRIPTION("A simple implementation of the game Snake.")
APP_AUTHOR("The6P4C, modified by SnailMath")
APP_VERSION("1.0.0")

The demo uses 20 pixel blocks:

#define COLOR_BACKGROUND RGB_TO_RGB565(0, 0, 0)
#define COLOR_SNAKE RGB_TO_RGB565(0x1F, 0, 0)
#define COLOR_FRUIT RGB_TO_RGB565(0, 0x3F, 0)
#define BLOCK_SIZE 20
#define MAX_SNAKE_LENGTH 50
#define FRUIT_SQUARES_BONUS 3

The game does not move pixel by pixel. It moves block by block. With BLOCK_SIZE set to 20, one move means 20 pixels on screen.

The demo stores direction as a number:

#define DIRECTION_NORTH 0
#define DIRECTION_EAST 1
#define DIRECTION_SOUTH 2
#define DIRECTION_WEST 3

This makes direction checks simple later:

if (direction == DIRECTION_NORTH) {
deltaY = -1;
}

The snake is stored as two arrays:

int snakeX[MAX_SNAKE_LENGTH];
int snakeY[MAX_SNAKE_LENGTH];

The head is always at index 0.

For example, if the snake has length 3:

  • snakeX[0], snakeY[0] is the head
  • snakeX[1], snakeY[1] is the next block
  • snakeX[2], snakeY[2] is the tail

The demo also stores the fruit position:

int fruitX;
int fruitY;

drawBlock() converts a grid position into real pixels:

void drawBlock(unsigned int blockX, unsigned int blockY, uint16_t color) {
for (auto x = blockX * BLOCK_SIZE; x < (blockX + 1) * BLOCK_SIZE; ++x) {
for (auto y = blockY * BLOCK_SIZE; y < (blockY + 1) * BLOCK_SIZE; ++y) {
vram[x + (y + 24) * width] = color;
}
}
}

The + 24 leaves space at the top of the screen for the score.

The draw() function redraws everything:

  • The score
  • The background
  • Every snake block
  • The fruit

The core part is:

for (int i = 0; i < snakeLength; ++i) {
drawBlock(snakeX[i], snakeY[i], COLOR_SNAKE);
}
drawBlock(fruitX, fruitY, COLOR_FRUIT);
LCD_Refresh();

Redrawing the whole grid is simple and reliable for a first version.

The first part of moveSnake() turns the current direction into a movement:

int deltaX = 0;
int deltaY = 0;
if (direction == DIRECTION_NORTH) {
deltaY = -1;
} else if (direction == DIRECTION_EAST) {
deltaX = 1;
}

Then it calculates the next head position:

int newX = snakeX[0] + deltaX;
int newY = snakeY[0] + deltaY;

If that position is outside the grid, the function returns false, which ends the game.

To move the snake, copy every block into the next slot:

for (int i = snakeLength - 1; i >= 0; --i) {
snakeX[i + 1] = snakeX[i];
snakeY[i + 1] = snakeY[i];
}
snakeX[0] = newX;
snakeY[0] = newY;

This makes room for the new head.

The fruit position comes from two LFSR values. This gives simple pseudo-random positions without needing a random library.

The demo retries until the fruit is not placed on the snake.

The important idea is that moveFruit() changes fruitX and fruitY, then checks the snake arrays to make sure the fruit is not inside the snake.

checkSnake() does two jobs:

if (snakeX[0] == fruitX && snakeY[0] == fruitY) {
numBlocksToAdd += FRUIT_SQUARES_BONUS;
moveFruit();
}

If the head touches the fruit, the snake grows and a new fruit is placed.

The function also checks whether the head touches another part of the snake. If it does, the function returns false.

At the start of main(), calculate the grid size from the screen size:

numBlocksX = width / BLOCK_SIZE;
numBlocksY = (height - 24) / BLOCK_SIZE;

Then place a 3 block snake in the middle:

snakeLength = 3;
for (int i = 0; i < snakeLength; ++i) {
snakeX[i] = numBlocksX / 2 - i;
snakeY[i] = numBlocksY / 2;
}
direction = DIRECTION_EAST;

The main loop draws the game, then waits for input:

GetInput(&event, 0xFFFFFFFF, 0x10);

On EVENT_TIMER, the snake moves:

case EVENT_TIMER:
if (!moveSnake()) {
running = false;
}
if (!checkSnake()) {
running = false;
}
break;

On EVENT_KEY, arrow keys change direction. The code prevents reversing directly into yourself:

case KEYCODE_UP:
if (direction != DIRECTION_SOUTH) {
direction = DIRECTION_NORTH;
}
break;

At this point, all pieces of the game are ready. The final code below puts them together.

#include <appdef.h>
#include <sdk/os/debug.h>
#include <sdk/os/input.h>
#include <sdk/os/lcd.h>
#include <sdk/calc/calc.h>
APP_NAME("Snake")
APP_DESCRIPTION("A simple implementation of the game Snake.")
APP_AUTHOR("The6P4C, modified by SnailMath")
APP_VERSION("1.0.0")
#define COLOR_BACKGROUND RGB_TO_RGB565(0, 0, 0)
#define COLOR_SNAKE RGB_TO_RGB565(0x1F, 0, 0)
#define COLOR_FRUIT RGB_TO_RGB565(0, 0x3F, 0)
#define BLOCK_SIZE 20
#define MAX_SNAKE_LENGTH 50
#define FRUIT_SQUARES_BONUS 3
#define DIRECTION_NORTH 0
#define DIRECTION_EAST 1
#define DIRECTION_SOUTH 2
#define DIRECTION_WEST 3
unsigned int numBlocksX, numBlocksY;
// the snake's head is at position 0 in our list
int snakeLength;
int snakeX[MAX_SNAKE_LENGTH];
int snakeY[MAX_SNAKE_LENGTH];
int numBlocksToAdd;
int direction;
uint32_t fruitXLFSR;
uint32_t fruitYLFSR;
int fruitX;
int fruitY;
void drawBlock(unsigned int blockX, unsigned int blockY, uint16_t color) {
for (auto x = blockX * BLOCK_SIZE; x < (blockX + 1) * BLOCK_SIZE; ++x) {
for (auto y = blockY * BLOCK_SIZE; y < (blockY + 1) * BLOCK_SIZE; ++y) {
vram[x + (y + 24) * width] = color;
}
}
}
void draw() {
Debug_SetCursorPosition(0, 0);
Debug_PrintString("Score: ", false);
char num[] = "0000";
num[0] = (snakeLength / 1000) % 10 + '0';
num[1] = (snakeLength / 100) % 10 + '0';
num[2] = (snakeLength / 10) % 10 + '0';
num[3] = snakeLength % 10 + '0';
Debug_PrintString(num, false);
// Draw the background
for (unsigned int blockY = 0; blockY < numBlocksY; ++blockY) {
for (unsigned int blockX = 0; blockX < numBlocksX; ++blockX) {
drawBlock(blockX, blockY, COLOR_BACKGROUND);
}
}
// Draw the snake
for (int i = 0; i < snakeLength; ++i) {
drawBlock(snakeX[i], snakeY[i], COLOR_SNAKE);
}
// Draw the fruit
drawBlock(fruitX, fruitY, COLOR_FRUIT);
LCD_Refresh();
}
bool moveSnake() {
int deltaX = 0;
int deltaY = 0;
if (direction == DIRECTION_NORTH) {
deltaY = -1;
} else if (direction == DIRECTION_EAST) {
deltaX = 1;
} else if (direction == DIRECTION_SOUTH) {
deltaY = 1;
} else if (direction == DIRECTION_WEST) {
deltaX = -1;
}
int newX = snakeX[0] + deltaX;
int newY = snakeY[0] + deltaY;
// if we've gone outside the grid
if (newX < 0 || static_cast<unsigned int>(newX) >= numBlocksX || newY < 0 || static_cast<unsigned int>(newY) >= numBlocksY) {
return false;
}
// Move the current blocks of the snake back one to make room for the head
for (int i = snakeLength - 1; i >= 0; --i) {
snakeX[i + 1] = snakeX[i];
snakeY[i + 1] = snakeY[i];
}
snakeX[0] = newX;
snakeY[0] = newY;
if (numBlocksToAdd > 0) {
--numBlocksToAdd;
++snakeLength;
}
return true;
}
void moveFruit() {
bool foundPosition = false;
while (!foundPosition) {
// Emulate modulo - SH4 doesn't let us do mod by an arbitary value
uint32_t xCopy = fruitXLFSR & 0xFF;
while (xCopy >= numBlocksX) {
xCopy -= numBlocksX;
}
fruitX = xCopy;
uint32_t yCopy = fruitYLFSR & 0xFF;
while (yCopy >= numBlocksY) {
yCopy -= numBlocksY;
}
fruitY = yCopy;
// calculate lfsr
// poly: x^32 + x^22 + x^2 + x
uint32_t fruitXLFSRBit = ((fruitXLFSR >> 0) ^ (fruitXLFSR >> 10) ^ (fruitXLFSR >> 30) ^ (fruitXLFSR >> 31)) & 1;
fruitXLFSR = (fruitXLFSR >> 1) | (fruitXLFSRBit << 31);
uint32_t fruitYLFSRBit = ((fruitYLFSR >> 0) ^ (fruitYLFSR >> 10) ^ (fruitYLFSR >> 30) ^ (fruitYLFSR >> 31)) & 1;
fruitYLFSR = (fruitYLFSR >> 1) | (fruitYLFSRBit << 31);
foundPosition = true;
// check we haven't placed the fruit on the snake
for (int i = 0; i < snakeLength; ++i) {
if (fruitX == snakeX[i] && fruitY == snakeY[i]) {
foundPosition = false;
break;
}
}
}
}
bool checkSnake() {
bool collidedWithSelf = false;
for (int i = 1; i < snakeLength; ++i) {
if (snakeX[i] == snakeX[0] && snakeY[i] == snakeY[0]) {
collidedWithSelf = true;
break;
}
}
if (snakeX[0] == fruitX && snakeY[0] == fruitY) {
numBlocksToAdd += FRUIT_SQUARES_BONUS;
moveFruit();
}
return !collidedWithSelf;
}
int main() {
numBlocksX = width / BLOCK_SIZE;
numBlocksY = (height - 24) / BLOCK_SIZE;
snakeLength = 3;
for (int i = 0; i < snakeLength; ++i) {
snakeX[i] = numBlocksX / 2 - i;
snakeY[i] = numBlocksY / 2;
}
direction = DIRECTION_EAST;
fruitXLFSR = 0x7312FAE1;
fruitYLFSR = 0xAF05432A;
moveFruit();
struct Input_Event event;
bool running = true;
while (running) {
draw();
GetInput(&event, 0xFFFFFFFF, 0x10);
Debug_PrintNumberHex_Dword(event.type, 12, 0);
switch (event.type) {
case EVENT_TIMER: //Defined in sdk/os/input.hpp scince 03/07/2021
if (!moveSnake()) {
running = false;
}
if (!checkSnake()) {
running = false;
}
break;
case EVENT_KEY:
switch (event.data.key.keyCode) {
case KEYCODE_UP:
if (direction != DIRECTION_SOUTH) {
direction = DIRECTION_NORTH;
}
break;
case KEYCODE_RIGHT:
if (direction != DIRECTION_WEST) {
direction = DIRECTION_EAST;
}
break;
case KEYCODE_DOWN:
if (direction != DIRECTION_NORTH) {
direction = DIRECTION_SOUTH;
}
break;
case KEYCODE_LEFT:
if (direction != DIRECTION_EAST) {
direction = DIRECTION_WEST;
}
break;
case KEYCODE_POWER_CLEAR:
running = false;
break;
default:
break;
}
break;
default:
break;
}
}
Debug_WaitKey();
return 0;
}