Snake Growth

Allowing our snake to eat apples, and grow longer each time it does
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Digital art showing a retro snake game
Ryan McCombe
Ryan McCombe
Posted

Now that we have our basic movement system in place, it's time to make our Snake game truly playable. We'll implement apple consumption mechanics, handle snake growth, and create a dynamic apple spawning system.

By the end of this section, our snake will be able to move around the grid eating apples, and getting longer for every apple it consumes.

Screenshot of the snake consuming apples and growing longer

Adding APPLE_EATEN Events

When our snake consumes an apple, we need to trigger a series of game events:

  • Growing the snake
  • Spawning a new apple somewhere in the grid
  • Updating the score (in a future lesson)

We'll use SDL's event system to coordinate these actions. First, let's define a new user event type in our configuration:

// GameConfig.h
// ...

namespace UserEvents{
  // ...
  inline Uint32 APPLE_EATEN =
    SDL_RegisterEvents(1);
}
// ...

Now we'll modify our Cell class to dispatch this event when the snake moves into a cell containing an apple. The Advance() function already detects snake movement, so we'll add four additional lines of code to handle apple consumption:

// 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) {
      if (CellState == Apple) {
        SDL_Event Event{UserEvents::APPLE_EATEN};
        SDL_PushEvent(&Event);
      }
      CellState = Snake;
      SnakeDuration = Data->Length;
    } else if (CellState == Snake) {
      SnakeDuration--;
      if (SnakeDuration <= 0) {
        CellState = Empty;
      }
    }
  }
};

This approach keeps our code modular - the cell only needs to announce that an apple was eaten, and other components can react accordingly.

Reacting to APPLE_EATEN Events

Next, we need to update classes to react to these events. Every time an apple is eaten, our snake needs to get longer. Our snake’s length is stored in the Snake variable of our GameState object. Let’s first update HandleEvent() in GameState.h, and have it increment the snake’s length every time an apple is eaten:

// GameState.h
// ...

class GameState {
public:
  // ...
  void HandleEvent(SDL_Event& E) {
    using namespace UserEvents;
    if (E.type == SDL_KEYDOWN) {
      HandleKeyEvent(E.key);
    } else if (E.type == APPLE_EATEN) {
      ++Snake.Length;
    }
  }
  // ...
};

Additionally, individual snake segments are being managed by any Cell object that has a CellState of Snake. When an apple is eaten, we need to increment the SnakeDuration of these cells, meaning the cell will remain a snake segment for one additional turn.

Let’s update our HandleEvent() function to take care of this:

// Cell.h
// ...

class Cell {
public:
  void HandleEvent(SDL_Event& E) {
    using namespace UserEvents;
    if (E.type == ADVANCE) {
      Advance(E.user);
    } else if (E.type == APPLE_EATEN) {
      if (CellState == Snake) {
        ++SnakeDuration;
      }
    }
  }
};

Replacing Apples

Every time an apple is eaten, we need to place a new apple in a random, empty grid cell. Collectively, our Cell objects are managed by the Grid object, so the Grid class is the natural place to manage this.

First, however, let’s add a PlaceApple() public method to our Cell object. If the Cell is Empty, this method will update it to an Apple cell and return true, indicating the apple was successfully placed.

If the Cell is not empty, the PlaceApple() cell will do nothing except return false, indicating the action failed.

// Cell.h
// ...

class Cell {
public:
  // ...
  bool PlaceApple() {
    if (CellState != Empty) return false;

    CellState = Apple;
    return true;
  }
  // ...
};

Over in Grid.h, we’ll now implement the logic to place an apple in a random cell within the std::vector array called Cells. To access a random cell in this array, we’ll generate a random index.

Remember, index arrays start at 0 so, if our array contained 10 cells, their indices range from from 0 to 9. More generally, we want a random index from 0 to Cells.size() - 1

The Random::Int() function we added to Engine/Random.h can help us choose an integer between two values. Array indices use the size_t type, which is an alias for an integer:

// Grid.h
// ...
#include "Engine/Random.h" 

class Grid {
  // ...
private:
  std::vector<Cell> Cells;

  void PlaceRandomApple() {
    size_t RandomIndex{
      Random::Int(0, Cells.size() - 1)};
      
    // ...
  }
  // ...
};

We need to continuously call the PlaceApple() method on random cells until we find one that is empty - that is, until PlaceApple() returns true. We can implement this as a loop, that will break once Cells[RandomIndex].PlaceApple() succeeds:

// Grid.h
// ...

class Grid {
 // ...
private:
  void PlaceRandomApple() {
    while (true) {
      size_t RandomIndex{
        Random::Int(0, Cells.size() - 1)};
      if (Cells[RandomIndex].PlaceApple()) {
        break;
      }
    }
  }
  // ...
};

Finally, we need to call this PlaceRandomApple() function at the appropriate time. Our Grid class already has a HandleEvent() method, so we can invoke PlaceRandomApple() every time a UserEvents::APPLE_EATEN event is detected:

// Grid.h
// ...

class Grid {
public:
  // ...
  void HandleEvent(SDL_Event& E) {
    for (auto& Cell : Cells) {
      Cell.HandleEvent(E);
    }
    if (E.type == UserEvents::APPLE_EATEN) {
      PlaceRandomApple();
    }
  }
  // ...
};

With these changes, our snake can now move around the world and eat apples. Every time an apple is eaten, a new one spawns elsewhere in the grid, and our snake gets one segment larger.

Screenshot of the snake consuming apples and growing longer

Infinite Loop Note

The implementation of the PlaceRandomApple() function is problematic if the player manages to complete the game. In that state, no empty cells are remaining. As such, Cells[RandomIndex].PlaceApple() will always return false, and our loop will never end.

We’ll fix this later in the chapter when we handle victory and loss states.

Performance Note

This approach where we try a random option, and try again if the option isn’t suitable, is a very common way of solving programming problems. However, we should be aware that it can have performance considerations.

As our snake gets larger, it can take more attempts to find an empty cell through randomness. This isn’t a problem for our simple game, but with more expensive procedures, it may be worth building a more robust solution to solve these types of problems.

For example, we could maintain a second list of Cell pointers, containing just the empty cells. We could then update that list every time our snake advances, removing the cell the snake’s head enters and adding the cell the snake’s tail vacates.

This approach smooths the work out over many frames ensuring that when the key event happens (eg, an apple is eaten) there is not a sudden drop in performance whilst the CPU spends significant time finding an appropriate reaction.

Complete Code

Complete versions of the files we changed in this part are available below

#pragma once
#define CHECK_ERRORS

#include <iostream>
#include <SDL.h>
#include <string>

namespace UserEvents{
  inline Uint32 ADVANCE =
    SDL_RegisterEvents(1);
  inline Uint32 APPLE_EATEN =
    SDL_RegisterEvents(1);
}

namespace Config{
  // Game Settings
  inline const std::string GAME_NAME{
    "Snake"};
  inline constexpr int ADVANCE_INTERVAL{200};

  inline constexpr int GRID_COLUMNS{16};
  static_assert(
    GRID_COLUMNS >= 12,
    "Grid must be at least 12 columns wide");

  inline constexpr int GRID_ROWS{5};
  static_assert(
    GRID_ROWS >= 5,
    "Grid must be at least 5 rows tall");

  // Size and Positioning
  inline constexpr int PADDING{5};
  inline constexpr int CELL_SIZE{36};

  inline constexpr int GRID_HEIGHT{
    CELL_SIZE * GRID_ROWS
  };
  inline constexpr int GRID_WIDTH{
    CELL_SIZE * GRID_COLUMNS};

  inline constexpr int WINDOW_HEIGHT{
    GRID_HEIGHT + PADDING * 2};
  inline constexpr int WINDOW_WIDTH{
    GRID_WIDTH + PADDING * 2};

  // Colors
  inline constexpr SDL_Color BACKGROUND_COLOR{
    85, 138, 52, 255};
  inline constexpr SDL_Color CELL_COLOR_A{
    171, 214, 82, 255};
  inline constexpr SDL_Color CELL_COLOR_B{
    161, 208, 74, 255};
  inline constexpr SDL_Color SNAKE_COLOR{
    67, 117, 234, 255};
  inline constexpr SDL_Color FONT_COLOR{
    255, 255, 255, 255};

  // Asset Paths
  inline const std::string APPLE_IMAGE{
    "apple.png"};
  inline const std::string FONT{
    "Rubik-SemiBold.ttf"};
}

inline void CheckSDLError(
  const std::string& Msg){
#ifdef CHECK_ERRORS
  const char* error = SDL_GetError();
  if (*error != '\0') {
    std::cerr << Msg << " Error: "
      << error << '\n';
    SDL_ClearError();
  }
#endif
}
#pragma once
#include <SDL.h>
#include "GameConfig.h"
#include "SnakeData.h"
#include "Assets.h"

enum CellState { Snake, Apple, Empty };

class Cell {
public:
  Cell(int Row, int Column, Assets& Assets)
  : Row(Row),
    Column(Column),
    Assets{Assets}
  {
    Initialize();
  }

  void HandleEvent(SDL_Event& E) {
    using namespace UserEvents;
    if (E.type == ADVANCE) {
      Advance(E.user);
    } else if (E.type == APPLE_EATEN) {
      if (CellState == Snake) {
        ++SnakeDuration;
      }
    }
  }

  void Tick(Uint32 DeltaTime) {}

  void Render(SDL_Surface* Surface) {
    SDL_FillRect(Surface, &BackgroundRect,
      SDL_MapRGB(
        Surface->format,
        BackgroundColor.r,
        BackgroundColor.g,
        BackgroundColor.b
      )
    );

    if (CellState == Apple) {
      Assets.Apple.Render(Surface, &BackgroundRect);
    } else if (CellState == Snake) {
      SDL_FillRect(Surface, &BackgroundRect,
        SDL_MapRGB(
          Surface->format,
          Config::SNAKE_COLOR.r,
          Config::SNAKE_COLOR.g,
          Config::SNAKE_COLOR.b
        )
      );
    }
  }

  bool PlaceApple() {
    if (CellState != Empty) return false;

    CellState = Apple;
    return true;
  }

private:
  void Initialize() {
    CellState = Empty;
    SnakeDuration = 0;

    int MiddleRow{Config::GRID_ROWS / 2};
    if (Row == MiddleRow && Column == 2) {
      CellState = Snake;
      SnakeDuration = 1;
    } else if (Row == MiddleRow && Column == 3) {
      CellState = Snake;
      SnakeDuration = 2;
    } else if (Row == MiddleRow && Column == 11) {
      CellState = Apple;
    }

    SnakeRect = BackgroundRect;
  }

  void Advance(SDL_UserEvent& E) {
    SnakeData* Data{static_cast<SnakeData*>(
      E.data1)};

    bool isThisCell{
      Data->HeadRow == Row &&
      Data->HeadCol == Column
    };

    if (isThisCell) {
      if (CellState == Apple) {
        SDL_Event Event{UserEvents::APPLE_EATEN};
        SDL_PushEvent(&Event);
      }
      CellState = Snake;
      SnakeDuration = Data->Length;
    } else if (CellState == Snake) {
      SnakeDuration--;
      if (SnakeDuration <= 0) {
        CellState = Empty;
      }
    }
  }

  int Row;
  int Column;

  CellState CellState;
  int SnakeDuration;

  SDL_Rect BackgroundRect{
    Column * Config::CELL_SIZE + Config::PADDING,
    Row * Config::CELL_SIZE + Config::PADDING,
    Config::CELL_SIZE, Config::CELL_SIZE};

  SDL_Color BackgroundColor{
    (Row + Column) % 2 == 0
      ? Config::CELL_COLOR_A
      : Config::CELL_COLOR_B};

  SDL_Rect SnakeRect;
  Assets& Assets;

  float FillPercent{0};
  MovementDirection FillDirection{Right};
};
#pragma once
#include <SDL.h>
#include "GameConfig.h"
#include "SnakeData.h"

class GameState {
public:
  void HandleEvent(SDL_Event& E) {
    using namespace UserEvents;
    using namespace Config;
    if (E.type == SDL_KEYDOWN) {
      HandleKeyEvent(E.key);
    } else if (E.type == APPLE_EATEN) {
      ++Snake.Length;
    }
  }

  void Tick(Uint32 DeltaTime) {
    ElapsedTime += DeltaTime;
    if (ElapsedTime >=
      Config::ADVANCE_INTERVAL) {
      ElapsedTime = 0;
      UpdateSnake();
    }
  }

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;
    }
  }

  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);
  }

  Uint32 ElapsedTime{0};
  SnakeData Snake{
    Config::GRID_ROWS / 2, 3, 2, Right};
  MovementDirection NextDirection{Right};
};
#pragma once
#include <SDL.h>
#include <vector>
#include "Cell.h"
#include "GameConfig.h"
#include "Engine/Random.h"

class Grid {
public:
  Grid(Assets& Assets) {
    using namespace Config;
    Cells.reserve(GRID_ROWS * GRID_COLUMNS);
    for (int R{0}; R < GRID_ROWS; ++R) {
      for (int C{0}; C < GRID_COLUMNS; ++C) {
        Cells.emplace_back(R, C, Assets);
      }
    }
  }

  void HandleEvent(SDL_Event& E) {
    for (auto& Cell : Cells) {
      Cell.HandleEvent(E);
    }
    if (E.type == UserEvents::APPLE_EATEN) {
      PlaceRandomApple();
    }
  }

  void Tick(Uint32 DeltaTime) {
    for (auto& Cell : Cells) {
      Cell.Tick(DeltaTime);
    }
  }

  void Render(SDL_Surface* surface) {
    for (auto& Cell : Cells) {
      Cell.Render(surface);
    }
  }

private:
  std::vector<Cell> Cells;

  void PlaceRandomApple() {
    using Random::Int;
    while (true) {
      size_t RandomIndex{Int(0, Cells.size() - 1)};
      if (Cells[RandomIndex].PlaceApple()) {
        break;
      }
    }
  }
};

Files not listed above have not been changed since the previous section.

Summary

In this lesson, we implemented more of the core gameplay mechanics for our Snake game. We added the following:

  • A custom event type for apple consumption
  • Reactions to this event type to handle snake growth
  • A system for randomly placing new apples
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Posted
Lesson Contents

Snake Growth

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

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Project: Snake

    65.
    Snake Growth

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


sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 84 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved