Snake Movement and Navigation

Learn how to implement core snake game mechanics including movement, controls, and state management

Ryan McCombe
Updated

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:

  1. When a cell becomes the snake's head, we set its duration to the snake's total length
  2. Every game advance decrements the duration of all snake cells
  3. 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
Next Lesson
Lesson 61 of 129

Snake Growth

Allowing our snake to eat apples, and grow longer each time it does

Have a question about this lesson?
Purchase the course to ask your own questions