Snake Growth
Allowing our snake to eat apples, and grow longer each time it does
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.

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.

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 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
Pausing and Restarting
Giving our Snake game the ability to pause itself, and adding a clickable button for restarting the game