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
Step 1: Include the SDK headers
Section titled “Step 1: Include the SDK headers”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.
Step 2: Add app metadata
Section titled “Step 2: Add app metadata”APP_NAME("Snake")APP_DESCRIPTION("A simple implementation of the game Snake.")APP_AUTHOR("The6P4C, modified by SnailMath")APP_VERSION("1.0.0")Step 3: Define colors and grid size
Section titled “Step 3: Define colors and grid size”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 3The 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.
Step 4: Define directions
Section titled “Step 4: Define directions”The demo stores direction as a number:
#define DIRECTION_NORTH 0#define DIRECTION_EAST 1#define DIRECTION_SOUTH 2#define DIRECTION_WEST 3This makes direction checks simple later:
if (direction == DIRECTION_NORTH) { deltaY = -1;}Step 5: Store the snake
Section titled “Step 5: Store the snake”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 headsnakeX[1],snakeY[1]is the next blocksnakeX[2],snakeY[2]is the tail
The demo also stores the fruit position:
int fruitX;int fruitY;Step 6: Draw one block
Section titled “Step 6: Draw one block”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.
Step 7: Draw the game
Section titled “Step 7: Draw the game”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.
Step 8: Move the snake
Section titled “Step 8: Move the snake”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.
Step 9: Shift the body
Section titled “Step 9: Shift the body”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.
Step 10: Move the fruit
Section titled “Step 10: Move the fruit”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.
Step 11: Check collisions
Section titled “Step 11: Check collisions”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.
Step 12: Set up the first game state
Section titled “Step 12: Set up the first game state”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;Step 13: Handle input and timer events
Section titled “Step 13: Handle input and timer events”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.
Final Code
Section titled “Final Code”#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 listint 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;}