Snake
Click the game, then press Space to start. Use the arrow keys or W, A, S, and D to steer. Eat the orange squares, and do not hit a wall or your own body. If the screen turns red, press Space two times to play again.
This game needs a physical keyboard.
The full program
/* Snake. Use the arrow keys or W, A, S, D. Space starts a new game. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdlib.h>
#define CELL_SIZE 20
#define GRID_WIDTH 32
#define GRID_HEIGHT 24
#define WINDOW_WIDTH (GRID_WIDTH * CELL_SIZE)
#define WINDOW_HEIGHT (GRID_HEIGHT * CELL_SIZE)
#define MAX_LENGTH (GRID_WIDTH * GRID_HEIGHT)
#define START_LENGTH 4
#define MAX_DT 0.05f /* seconds */
#define START_DELAY 0.16f /* seconds for each move */
#define MIN_DELAY 0.06f
#define SPEED_UP 0.003f
typedef struct {
int x;
int y;
} Cell;
typedef enum {
STATE_WAITING,
STATE_PLAYING,
STATE_GAME_OVER
} GameState;
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static Uint64 last_ticks = 0;
static GameState state = STATE_WAITING;
static Cell snake[MAX_LENGTH]; /* snake[0] is the head. */
static int snake_length = 0;
static Cell direction = { 1, 0 };
static Cell next_direction = { 1, 0 };
static Cell food;
static float move_timer = 0.0f;
static float move_delay = START_DELAY;
static bool same_cell(Cell a, Cell b)
{
return a.x == b.x && a.y == b.y;
}
/* Test the first cells of the snake. The tail can be left out of the test. */
static bool snake_covers(Cell cell, int length)
{
for (int i = 0; i < length; i++) {
if (same_cell(snake[i], cell)) {
return true;
}
}
return false;
}
/* Try random cells until one is free. The grid is rarely full. */
static void place_food(void)
{
if (snake_length == MAX_LENGTH) {
return;
}
do {
food.x = rand() % GRID_WIDTH;
food.y = rand() % GRID_HEIGHT;
} while (snake_covers(food, snake_length));
}
static void new_game(void)
{
snake_length = START_LENGTH;
for (int i = 0; i < snake_length; i++) {
snake[i].x = GRID_WIDTH / 2 - i;
snake[i].y = GRID_HEIGHT / 2;
}
direction = (Cell){ 1, 0 };
next_direction = direction;
move_timer = 0.0f;
move_delay = START_DELAY;
place_food();
state = STATE_WAITING;
}
/* A turn takes effect at the next move. A snake cannot turn straight back. */
static void turn(int x, int y)
{
if (x != -direction.x || y != -direction.y) {
next_direction = (Cell){ x, y };
}
}
/* Move the snake by one cell. */
static void step_snake(void)
{
Cell head;
bool eating;
direction = next_direction;
head.x = snake[0].x + direction.x;
head.y = snake[0].y + direction.y;
eating = same_cell(head, food);
if (head.x < 0 || head.x >= GRID_WIDTH || head.y < 0 || head.y >= GRID_HEIGHT) {
state = STATE_GAME_OVER;
return;
}
/* The tail moves away in this step, unless the snake is eating. */
if (snake_covers(head, eating ? snake_length : snake_length - 1)) {
state = STATE_GAME_OVER;
return;
}
if (eating) {
snake_length++;
}
/* Each cell takes the place of the one in front of it. */
SDL_memmove(&snake[1], &snake[0], (size_t)(snake_length - 1) * sizeof(Cell));
snake[0] = head;
if (eating) {
place_food();
move_delay = SDL_max(move_delay - SPEED_UP, MIN_DELAY);
}
}
static void update(float dt)
{
if (state != STATE_PLAYING) {
return;
}
move_timer += dt;
while (move_timer >= move_delay && state == STATE_PLAYING) {
move_timer -= move_delay;
step_snake();
}
}
/* Handle one key press. Any turn starts a waiting game. */
static void press_key(SDL_Scancode key)
{
if (key == SDL_SCANCODE_SPACE) {
if (state == STATE_GAME_OVER) {
new_game();
} else if (state == STATE_WAITING) {
state = STATE_PLAYING;
}
return;
}
if (key == SDL_SCANCODE_UP || key == SDL_SCANCODE_W) {
turn(0, -1);
} else if (key == SDL_SCANCODE_DOWN || key == SDL_SCANCODE_S) {
turn(0, 1);
} else if (key == SDL_SCANCODE_LEFT || key == SDL_SCANCODE_A) {
turn(-1, 0);
} else if (key == SDL_SCANCODE_RIGHT || key == SDL_SCANCODE_D) {
turn(1, 0);
}
}
static void fill_cell(Cell cell)
{
SDL_FRect rect = {
(float)(cell.x * CELL_SIZE) + 1.0f,
(float)(cell.y * CELL_SIZE) + 1.0f,
(float)CELL_SIZE - 2.0f,
(float)CELL_SIZE - 2.0f
};
SDL_RenderFillRect(renderer, &rect);
}
static void draw(void)
{
if (state == STATE_GAME_OVER) {
SDL_SetRenderDrawColor(renderer, 100, 32, 36, SDL_ALPHA_OPAQUE);
} else {
SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
}
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
fill_cell(food);
SDL_SetRenderDrawColor(renderer, 80, 200, 160, SDL_ALPHA_OPAQUE);
for (int i = 1; i < snake_length; i++) {
fill_cell(snake[i]);
}
SDL_SetRenderDrawColor(renderer, 235, 236, 240, SDL_ALPHA_OPAQUE);
fill_cell(snake[0]);
SDL_RenderPresent(renderer);
}
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO)) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Snake", WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer)) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(renderer, 1);
new_game();
last_ticks = SDL_GetTicksNS();
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT) {
return SDL_APP_SUCCESS;
}
if (event->type == SDL_EVENT_KEY_DOWN && !event->key.repeat) {
press_key(event->key.scancode);
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
Uint64 now = SDL_GetTicksNS();
float dt = (float)(now - last_ticks) / (float)SDL_NS_PER_SECOND;
last_ticks = now;
if (dt > MAX_DT) {
dt = MAX_DT;
}
update(dt);
draw();
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
}/* Snake. Use the arrow keys or W, A, S, D. Space starts a new game. */
#include <SDL.h>
#include <stdbool.h>
#include <stdlib.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#define CELL_SIZE 20
#define GRID_WIDTH 32
#define GRID_HEIGHT 24
#define WINDOW_WIDTH (GRID_WIDTH * CELL_SIZE)
#define WINDOW_HEIGHT (GRID_HEIGHT * CELL_SIZE)
#define MAX_LENGTH (GRID_WIDTH * GRID_HEIGHT)
#define START_LENGTH 4
#define MAX_DT 0.05f /* seconds */
#define START_DELAY 0.16f /* seconds for each move */
#define MIN_DELAY 0.06f
#define SPEED_UP 0.003f
typedef struct {
int x;
int y;
} Cell;
typedef enum {
STATE_WAITING,
STATE_PLAYING,
STATE_GAME_OVER
} GameState;
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static Uint64 last_ticks = 0;
static GameState state = STATE_WAITING;
static Cell snake[MAX_LENGTH]; /* snake[0] is the head. */
static int snake_length = 0;
static Cell direction = { 1, 0 };
static Cell next_direction = { 1, 0 };
static Cell food;
static float move_timer = 0.0f;
static float move_delay = START_DELAY;
static bool running = true;
static bool same_cell(Cell a, Cell b)
{
return a.x == b.x && a.y == b.y;
}
/* Test the first cells of the snake. The tail can be left out of the test. */
static bool snake_covers(Cell cell, int length)
{
for (int i = 0; i < length; i++) {
if (same_cell(snake[i], cell)) {
return true;
}
}
return false;
}
/* Try random cells until one is free. The grid is rarely full. */
static void place_food(void)
{
if (snake_length == MAX_LENGTH) {
return;
}
do {
food.x = rand() % GRID_WIDTH;
food.y = rand() % GRID_HEIGHT;
} while (snake_covers(food, snake_length));
}
static void new_game(void)
{
snake_length = START_LENGTH;
for (int i = 0; i < snake_length; i++) {
snake[i].x = GRID_WIDTH / 2 - i;
snake[i].y = GRID_HEIGHT / 2;
}
direction = (Cell){ 1, 0 };
next_direction = direction;
move_timer = 0.0f;
move_delay = START_DELAY;
place_food();
state = STATE_WAITING;
}
/* A turn takes effect at the next move. A snake cannot turn straight back. */
static void turn(int x, int y)
{
if (x != -direction.x || y != -direction.y) {
next_direction = (Cell){ x, y };
}
}
/* Move the snake by one cell. */
static void step_snake(void)
{
Cell head;
bool eating;
direction = next_direction;
head.x = snake[0].x + direction.x;
head.y = snake[0].y + direction.y;
eating = same_cell(head, food);
if (head.x < 0 || head.x >= GRID_WIDTH || head.y < 0 || head.y >= GRID_HEIGHT) {
state = STATE_GAME_OVER;
return;
}
/* The tail moves away in this step, unless the snake is eating. */
if (snake_covers(head, eating ? snake_length : snake_length - 1)) {
state = STATE_GAME_OVER;
return;
}
if (eating) {
snake_length++;
}
/* Each cell takes the place of the one in front of it. */
SDL_memmove(&snake[1], &snake[0], (size_t)(snake_length - 1) * sizeof(Cell));
snake[0] = head;
if (eating) {
place_food();
move_delay = SDL_max(move_delay - SPEED_UP, MIN_DELAY);
}
}
static void update(float dt)
{
if (state != STATE_PLAYING) {
return;
}
move_timer += dt;
while (move_timer >= move_delay && state == STATE_PLAYING) {
move_timer -= move_delay;
step_snake();
}
}
/* Handle one key press. Any turn starts a waiting game. */
static void press_key(SDL_Scancode key)
{
if (key == SDL_SCANCODE_SPACE) {
if (state == STATE_GAME_OVER) {
new_game();
} else if (state == STATE_WAITING) {
state = STATE_PLAYING;
}
return;
}
if (key == SDL_SCANCODE_UP || key == SDL_SCANCODE_W) {
turn(0, -1);
} else if (key == SDL_SCANCODE_DOWN || key == SDL_SCANCODE_S) {
turn(0, 1);
} else if (key == SDL_SCANCODE_LEFT || key == SDL_SCANCODE_A) {
turn(-1, 0);
} else if (key == SDL_SCANCODE_RIGHT || key == SDL_SCANCODE_D) {
turn(1, 0);
}
}
static void fill_cell(Cell cell)
{
SDL_FRect rect = {
(float)(cell.x * CELL_SIZE) + 1.0f,
(float)(cell.y * CELL_SIZE) + 1.0f,
(float)CELL_SIZE - 2.0f,
(float)CELL_SIZE - 2.0f
};
SDL_RenderFillRectF(renderer, &rect);
}
static void draw(void)
{
if (state == STATE_GAME_OVER) {
SDL_SetRenderDrawColor(renderer, 100, 32, 36, SDL_ALPHA_OPAQUE);
} else {
SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
}
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
fill_cell(food);
SDL_SetRenderDrawColor(renderer, 80, 200, 160, SDL_ALPHA_OPAQUE);
for (int i = 1; i < snake_length; i++) {
fill_cell(snake[i]);
}
SDL_SetRenderDrawColor(renderer, 235, 236, 240, SDL_ALPHA_OPAQUE);
fill_cell(snake[0]);
SDL_RenderPresent(renderer);
}
static void frame(void)
{
SDL_Event event;
Uint64 now = SDL_GetPerformanceCounter();
float dt = (float)(now - last_ticks) / (float)SDL_GetPerformanceFrequency();
last_ticks = now;
if (dt > MAX_DT) {
dt = MAX_DT;
}
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
} else if (event.type == SDL_KEYDOWN && !event.key.repeat) {
press_key(event.key.keysym.scancode);
}
}
update(dt);
draw();
}
int main(int argc, char *argv[])
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return 1;
}
if (SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer) != 0) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_SetWindowTitle(window, "Snake");
SDL_RenderSetVSync(renderer, 1);
new_game();
last_ticks = SDL_GetPerformanceCounter();
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(frame, 0, 1);
#else
while (running) {
frame();
}
#endif
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}A game on a grid
Pong moves objects by fractions of a pixel. Snake is different, because everything sits on a grid of whole cells. So the positions are int values, and a position is a cell number and not a pixel.
typedef struct {
int x;
int y;
} Cell;Only fill_cell knows about pixels. It multiplies a cell number by CELL_SIZE. The game rules know nothing about the size of the screen. To change the size, you change one number.
Step at a fixed rate
The snake does not move in each frame. It moves one cell at a time, on a timer:
move_timer += dt;
while (move_timer >= move_delay && state == STATE_PLAYING) {
move_timer -= move_delay;
step_snake();
}This is the fixed step pattern from the timer in Sprites and animation. The game gets faster as move_delay gets smaller.
The body is an array
The head is at snake[0], and the tail is at the end. To move, each cell takes the place of the cell in front of it:
SDL_memmove(&snake[1], &snake[0], (size_t)(snake_length - 1) * sizeof(Cell));
snake[0] = head;SDL_memmove copies a block of memory, and it handles a source and a destination that overlap. SDL_memcpy is not safe here. After this call, the old head is at index 1, and the last cell falls off the end.
To grow, the program adds 1 to snake_length before the move. So the last cell stays, and the snake is one cell longer.
A small rule with a big effect
if (snake_covers(head, eating ? snake_length : snake_length - 1)) {The tail moves away in this step, so the head can move into the cell that the tail is leaving. The test leaves out the tail, unless the snake is eating, because then the tail stays. Without this rule, the snake dies in a move that looks safe.
Turn only at a step
A key press does not change direction. It stores next_direction, and the next step uses it.
if (x != -direction.x || y != -direction.y) {
next_direction = (Cell){ x, y };
}The test stops a turn straight back into the body. Compare against direction, the direction of the last real move. If you compare against next_direction, then two quick key presses between steps can reverse the snake and end the game.
Place the food
do {
food.x = rand() % GRID_WIDTH;
food.y = rand() % GRID_HEIGHT;
} while (snake_covers(food, snake_length));The program tries random cells until it finds a free one. This is simple, and it is fast while the grid has space. A snake that fills the whole grid has no free cell, so the function tests for that first. Without the test, this loop runs forever.
Try this
- Show the score as text, with the code from Draw text.
- Make the walls wrap around, so that the snake comes back on the other side.
- Store two turns, so that a very quick pair of key presses both work.
- Replace
SDL_memmovewith a ring buffer (an array with a moving start index). The cost of a move no longer grows with the length.
What you learned
- A grid game holds whole cell numbers, and only the drawing code knows about pixels.
- A timer moves the game at a fixed rate, apart from the frame rate.
SDL_memmoveshifts an array safely.- An input can wait in a variable for the next step of the game.