Snake Movement and Navigation
Learn how to implement core snake game mechanics including movement, controls, and state management
In this lesson, we'll implement the core mechanics of our snake game. We'll create the game state management system, handle user input for controlling the snake's movement, and implement the snake's movement logic. By the end, you'll have a fully functional snake that responds to player controls.
We'll start by defining the fundamental data structures needed to track our snake's state, including its position, length, and direction. Then we'll expand our state system to advance the game through time.
Finally, we'll allow our game state to respond to user input, allowing the player to control the direction their snake moves.
Adding a SnakeData
Struct
Let's start by defining a simple struct that stores the current state of our snake. We'll store the row and column of the snake's head, how long the snake is, and the direction it is currently moving:
// SnakeData.h
#pragma once
enum MovementDirection { Up, Down, Left, Right };
struct SnakeData {
int HeadRow;
int HeadCol;
int Length;
MovementDirection Direction;
};
Adding a GameState
Class
When we're working on a game, it is common to have an object that is designed to manage the overall state of our game world. We'll create a GameState
class for this. Game states typically don't need to be rendered, but they usually need to be notified of events and ticks, so we'll add our HandleEvent()
and Tick()
functions:
// GameState.h
#pragma once
#include <SDL.h>
class GameState {
public:
void HandleEvent(SDL_Event& E) {}
void Tick(Uint32 DeltaTime) {}
};
This GameState
class will also be responsible for managing the state of our snake, so we'll store a SnakeData
object. As we covered before, the snake will initially have the following characteristics:
- Its head will be on the middle row, ie
CONFIG::GRID_ROWS / 2
- Its head will be on column
3
- Its length will be
2
- It will be moving
Right
Let's initialize our SnakeData
accordingly:
// GameState.h
// ...
#include "SnakeData.h"
class GameState {
// ...
private:
SnakeData Snake{
Config::GRID_ROWS / 2, 3, 2, Right};
};
Over in main.cpp
, let's initialize our GameState
struct and call HandleEvent()
and Tick()
at the appropriate times within our game loop:
// main.cpp
#include <SDL.h>
#include <SDL_image.h>
#include <SDL_ttf.h>
#include "Engine/Window.h"
#include "GameState.h"
#include "GameUI.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
CheckSDLError("Initializing SDL");
IMG_Init(IMG_INIT_PNG);
CheckSDLError("Initializing SDL_image");
TTF_Init();
CheckSDLError("Initializing SDL_ttf");
Window GameWindow;
GameUI UI;
GameState State;
Uint32 PreviousTick{SDL_GetTicks()};
Uint32 CurrentTick;
Uint32 DeltaTime;
SDL_Event Event;
while (true) {
CurrentTick = SDL_GetTicks();
DeltaTime = CurrentTick - PreviousTick;
// Events
while (SDL_PollEvent(&Event)) {
UI.HandleEvent(Event);
State.HandleEvent(Event);
if (Event.type == SDL_QUIT) {
SDL_Quit();
IMG_Quit();
return 0;
}
}
// Tick
State.Tick(DeltaTime);
UI.Tick(DeltaTime);
// Render
GameWindow.Render();
UI.Render(GameWindow.GetSurface());
// Swap
GameWindow.Update();
PreviousTick = CurrentTick;
}
return 0;
}
Advancing the Game State
In most Snake implementations, our game advances on a specific interval. For example, every 200 milliseconds, our snake's position changes. Let's start by defining this advance interval as a variable within our GameConfig.h
file:
// GameConfig.h
// ...
namespace Config{
// ...
inline constexpr int ADVANCE_INTERVAL{200};
// ...
}
// ...
We'll also define a user event that we can use with SDL's event mechanism. We can dispatch this user event any time our game advances, so any component that needs to react to this can do so. We covered user events in more detail in a dedicated lesson earlier in the course:
Creating Custom Events
Learn how to create and manage your own game-specific events using SDL's event system.
Let's start by registering our event within our GameConfig.h
. We'll have a few of these custom event types as we progress through our course, so let's put them in a UserEvents
namespace:
// GameConfig.h
// ...
namespace UserEvents{
inline Uint32 ADVANCE{SDL_RegisterEvents(1)};
}
// ...
In our GameState.h
, we'll keep track of how much time has passed by accumulating the time deltas reported to our Tick()
function invocations. When we've accumulated enough milliseconds to satisfy our ADVANCE_INTERVAL
configuration, we'll reset our accumulated time and advance our game state.
We'll put this advance logic in a private UpdateSnake()
function which we'll implement next:
// GameState.h
#pragma once
#include <SDL.h>
#include "GameConfig.h"
#include "SnakeData.h"
class GameState {
public:
void HandleEvent(SDL_Event& E) {}
void Tick(Uint32 DeltaTime) {
ElapsedTime += DeltaTime;
if (ElapsedTime >= Config::ADVANCE_INTERVAL) {
ElapsedTime = 0;
UpdateSnake();
}
}
private:
void UpdateSnake() {}
Uint32 ElapsedTime{0};
// ...
};
Our new UpdateSnake()
function will update our SnakeData
to reflect its new position. Where our snake moves next will be based on a new NextDirection
variable that our GameState
manages.
For now, the NextDirection
will always be Right
, but we'll later update our class to allow players to change this direction based on keyboard input.
Finally, our UpdateSnake()
function will dispatch one of our new UserEvents::ADVANCE
events, notifying other components that the game state has advanced. We'll include a pointer to the SnakeData
in that event which other components can access as needed to determine how they need to react:
// GameState.h
// ...
class GameState {
// ...
private:
void UpdateSnake() {
Snake.Direction = NextDirection;
switch (NextDirection) {
case Up:
Snake.HeadRow--;
break;
case Down:
Snake.HeadRow++;
break;
case Left:
Snake.HeadCol--;
break;
case Right:
Snake.HeadCol++;
break;
}
SDL_Event Event{UserEvents::ADVANCE};
Event.user.data1 = &Snake;
SDL_PushEvent(&Event);
}
// Direction the snake will move in when the
// game state next advances
MovementDirection NextDirection{Right};
// ...
};
Rendering the Moving Snake
Over in Cell.h
, let's react to our game state advancing, and therefore our snake moving. Within our HandleEvent()
function, we'll check if the event has the UserEvents::ADVANCE
type and, if it does, we'll pass it off to a new private Advance()
method:
// Cell.h
// ...
class Cell {
public:
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
if (E.type == ADVANCE) {
Advance(E.user);
}
}
// ...
private:
void Advance(SDL_UserEvent& E) {
// TODO - Update Cell
}
// ...
};
Within our Advance()
method, we first need to understand if the snake advanced into this cell. To do this, we can first retrieve the SnakeData
pointer that our GameState
attaches to all UserEvents::ADVANCE
events:
// Cell.h
// ...
#include "SnakeData.h"
// ...
class Cell {
// ...
private:
void Advance(SDL_UserEvent& E) {
SnakeData* Data{static_cast<SnakeData*>(
E.data1)};
// TODO - Update Cell
}
// ...
};
By comparing the HeadRow
and HeadCol
of the SnakeData
to the Row
and Column
of the cell reacting to the event, we can understand if the snake has advanced into this cell. If it has, we'll update the CellState
to Snake
:
// Cell.h
// ...
class Cell {
// ...
private:
void Advance(SDL_UserEvent& E) {
SnakeData* Data{static_cast<SnakeData*>(
E.data1)};
bool isThisCell{
Data->HeadRow == Row &&
Data->HeadCol == Column
};
if (isThisCell) {
CellState = Snake;
}
}
};
If we run our game, we should now notice our snake continuously growing to the right, expanding by one cell every time our Config::ADVANCE_INTERVAL
elapses:

We don't want our snake growing to the right - rather, we want it moving to the right. To do this, we'll need to remove snake segments from the tail.
Managing Snake Segments
When a snake moves in our game, we need to both add new segments at the head and remove old segments from the tail. We'll implement this using a duration system where each cell keeps track of how long it should remain part of the snake.
Let's add a SnakeDuration
counter to our Cell
class. We'll initialize this to 0
for now, but we'll update its value later:
// Cell.h
// ...
class Cell {
// ...
private:
// ...
// How many more advances this cell
// remains part of the snake
int SnakeDuration{0};
};
This counter works as follows:
- When a cell becomes the snake's head, we set its duration to the snake's total length
- Every game advance decrements the duration of all snake cells
- When a cell's duration reaches zero, it reverts to an empty cell
This system handles the snake's movement:
- New head segments get the full snake length as their duration
- Tail segments naturally disappear when their duration expires
- The snake maintains its length as it moves
Let's implement this logic in our Advance()
function:
// Cell.h
// ...
class Cell {
// ...
private:
// ...
void Advance(SDL_UserEvent& E) {
SnakeData* Data{static_cast<SnakeData*>(
E.data1)};
bool isThisCell{
Data->HeadRow == Row &&
Data->HeadCol == Column
};
if (isThisCell) {
CellState = Snake;
} else if (CellState == Snake) {
--SnakeDuration;
if (SnakeDuration == 0) {
CellState = Empty;
}
}
}
};
Finally, we need to set SnakeDuration
to appropriate values any time a cell becomes a Snake
segment. In Advance()
, if the snake advances into our cell, we know how long the cell needs to remain a snake segment by looking at the overall length of the snake:
// Cell.h
// ...
class Cell {
// ...
private:
// ...
void Advance(SDL_UserEvent& E) {
SnakeData* Data{static_cast<SnakeData*>(
E.data1)};
bool isThisCell{
Data->HeadRow == Row &&
Data->HeadCol == Column
};
if (isThisCell) {
CellState = Snake;
SnakeDuration = Data->Length;
} else if (CellState == Snake) {
--SnakeDuration;
if (SnakeDuration == 0) {
CellState = Empty;
}
}
}
};
When our game is initialized, we start with a two-segment snake. We need to initialize the SnakeDuration
for those cells too, with the snake's head segment having an initial duration of 2
and the tail segment having a duration of 1
. All other cells should have a duration of 0
:
// Cell.h
// ...
class Cell {
// ...
private:
// ...
void Initialize() {
CellState = Empty;
SnakeDuration = 0;
int InitialRow{Config::GRID_ROWS / 2};
if (Row == InitialRow && Column == 2) {
CellState = Snake;
SnakeDuration = 1;
} else if (Row == InitialRow && Column == 3) {
CellState = Snake;
SnakeDuration = 2;
} else if (Row == InitialRow && Column == 11) {
CellState = Apple;
}
}
};
Running our program, our snake should now move right:

Turning the Snake
As the final step in this section, let's allow the player to change the snake's direction using the arrow keys or the WASD keys. Our GameState
is already receiving keyboard events - we just need to react to them appropriately.
Let's update HandleEvent()
to check for SDL_KEYDOWN
events, and forward them to a new HandleKeyEvent()
private method:
// GameState.h
// ...
class GameState {
public:
// ...
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_KEYDOWN) {
HandleKeyEvent(E.key);
}
}
private:
// ...
void HandleKeyEvent(SDL_KeyboardEvent& E) {}
};
Our HandleKeyEvent()
will check for arrow or WASD keypresses, and then update the NextDirection
variable as appropriate.
We don't want our snake to turn 180 degrees in a single step. For example, if the snake is currently moving Right
it won't be able to change its direction to Left
in a single advance. It can only turn Up
or Down
, or continue moving Right
.
We'll implement these restrictions using a set of if
statements:
// GameState.h
// ...
class GameState {
// ...
private:
void HandleKeyEvent(SDL_KeyboardEvent& E) {
switch (E.keysym.sym) {
case SDLK_UP:
case SDLK_w:
if (Snake.Direction != Down) {
NextDirection = Up;
}
break;
case SDLK_DOWN:
case SDLK_s:
if (Snake.Direction != Up) {
NextDirection = Down;
}
break;
case SDLK_LEFT:
case SDLK_a:
if (Snake.Direction != Right) {
NextDirection = Left;
}
break;
case SDLK_RIGHT:
case SDLK_d:
if (Snake.Direction != Left) {
NextDirection = Right;
}
break;
}
}
// ...
};
If we run our game, we should now be able to move our snake around:

Our snake's movement will feel quite jolted as we're only updating its visual position by a large step every time the game state advances, rather than by a small step on every frame.
We may prefer this jolted movement for the more retro feel, but later in the chapter we'll demonstrate how to make the snake's movement feel smoother by updating it on every frame.
Complete Code
Complete versions of the files we changed in this part are available below
Files not listed above have not been changed since the previous section.
Summary
In this lesson, we built the core movement mechanics for our snake game. We implemented a state-based game system that manages the snake's position, handles user input, and updates the game at regular intervals. The key steps we took include:
- Game state management using a dedicated
GameState
class - Event-based movement control using SDL keyboard events
- Snake segment tracking using a duration-based system
- Frame-independent game updates using
SDL_GetTicks()
- Custom event handling for game state changes
Snake Growth
Allowing our snake to eat apples, and grow longer each time it does