Animating Snake Movement
Animate the snake's movement across cells for a smooth, dynamic visual effect.
Right now, our snake moves instantly between cells. We'll enhance this by implementing a sliding animation, giving the illusion of the snake smoothly traversing the grid. This involves dynamically adjusting the portion of each cell occupied by the snake.
To do this, we'll be using our Tick() mechanism to provide frame-by-frame adjustments to our snake's visuals.
This is the final part of our project, so we'll also finish things off with a list of suggestions on how we can further develop the game and put our skills to the test!

Currently, when our cell has a CellState of Snake, we fill the entire cell with our snake color. This is done in our Render() function, where we're using the BackgroundRect rectangle to define the bounds of both our background color and our snake segment:
src/Snake/Cell.cpp
// ...
void Cell::Render(SDL_Surface* Surface) {
SDL_FillSurfaceRect(
Surface,
&BackgroundRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
BackgroundColor.r,
BackgroundColor.g,
BackgroundColor.b
)
);
if (State == CellState::Apple) {
AssetList.Apple.Render(
Surface, &BackgroundRect);
} else if (State == CellState::Snake) {
SDL_FillSurfaceRect(
Surface,
&BackgroundRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
SnakeColor.r,
SnakeColor.g,
SnakeColor.b
)
);
}
}
// ...Instead, we want to create the visual effect of our snake sliding across the cell. From a high level, this involves two steps:
- Creating a different
SDL_Rectmember to store what part of our cell should be filled by the snake color - Updating the values of this
SDL_Recton every frame (ie, on everyTick()invocation) to create the sliding animation effect
Let's work through this step by step.
Starting Point
This lesson will build upon the code from the end of the previous lesson. We'll be focused on updating the Cell class in this lesson, which we've provided below for reference
Files
Adding SnakeRect
First, let's define the SDL_Rect for our snake blitting. We'll call it SnakeRect:
src/Snake/Cell.h
// ...
class Cell {
// ...
private:
// ...
SDL_Rect SnakeRect;
};We'll initialize our SnakeRect in the Reset() method to have the same dimensions as our BackgroundRect:
src/Snake/Cell.cpp
// ...
void Cell::Reset() {
SnakeRect = BackgroundRect;
// ...
}
// ...Let's update our Render() function to use this SnakeRect for our snake, rather than the BackgroundRect it is currently using:
src/Snake/Cell.cpp
// ...
void Cell::Render(SDL_Surface* Surface) {
SDL_FillSurfaceRect(
Surface,
&BackgroundRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
BackgroundColor.r,
BackgroundColor.g,
BackgroundColor.b
)
);
if (State == CellState::Apple) {
AssetList.Apple.Render(Surface, &BackgroundRect);
} else if (State == CellState::Snake) {
SDL_FillSurfaceRect(
Surface,
&SnakeRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
SnakeColor.r,
SnakeColor.g,
SnakeColor.b
)
);
}
}
// ...Adding FillPercent and FillDirection
To control the effect of the snake sliding across our cell, we'll need two new member variables:
- A
FillPercentvariable, controlling how far the snake has slid across our cell - A
FillDirectionvariable, controlling which direction the snake needs to slide
Think of FillPercent as how much of the cell the snake occupies, ranging from 0.0 (empty) to 1.0 (completely full). FillDirection indicates where the snake enters the cell (Up, Down, Left, or Right).
Here are some examples of how they work together:
- FillPercent = 0.0: The cell is entirely the background color. The snake hasn't entered yet.
- FillPercent = 1.0: The cell is entirely the snake color. The snake completely fills the cell.
- FillPercent = 0.5, FillDirection = Right: The snake is entering from the left. The left half of the cell is the snake color, and the right half is the background color.
- FillPercent = 0.25, FillDirection = Down: The snake is entering from the top. The top quarter of the cell is the snake color, and the bottom three-quarters are the background color.
Using FillPercent
Let's add the FillPercent floating point member to our class:
src/Snake/Cell.h
// ...
class Cell {
// ...
private:
float FillPercent{0.0f};
// ...
};When our game starts or restarts, we'll initialize FillPercent to 0.0f in the Reset() method, except for the two cells that contain our initial snake segments, which will be fully filled:
src/Snake/Cell.cpp
// ...
void Cell::Reset() {
State = CellState::Empty;
SnakeRect = BackgroundRect;
SnakeColor = Config::SNAKE_COLOR;
SnakeDuration = 0;
FillPercent = 0.0f;
int MiddleRow{Config::GRID_ROWS / 2};
if (Row == MiddleRow && Column == 2) {
State = CellState::Snake;
SnakeDuration = 1;
FillPercent = 1.0f;
} else if (Row == MiddleRow && Column == 3) {
State = CellState::Snake;
SnakeDuration = 2;
FillPercent = 1.0f;
} else if (Row == MiddleRow && Column == 11) {
State = CellState::Apple;
}
}
// ...Currently, our Render() function checks if State == CellState::Snake to decide if it needs to render the snake in our cell. We'll update this to check if FillPercent > 0 instead:
src/Snake/Cell.cpp
// ...
void Cell::Render(SDL_Surface* Surface) {
SDL_FillSurfaceRect(
Surface, &BackgroundRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
BackgroundColor.r,
BackgroundColor.g,
BackgroundColor.b
)
);
if (State == CellState::Apple) {
AssetList.Apple.Render(Surface, &BackgroundRect);
} else if (FillPercent > 0) { // <h>
SDL_FillSurfaceRect(Surface, &SnakeRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
SnakeColor.r,
SnakeColor.g,
SnakeColor.b
)
);
}
}
// ...Using FillDirection
We'll also need a FillDirection value representing which direction we need to fill our cell. For example, if the snake is entering the cell from the left, the cell should be filled with the snake color from left to right.
Let's add FillDirection to our class, using the MovementDirection type we have already declared:
src/Snake/Cell.h
// ...
class Cell {
// ...
private:
MovementDirection FillDirection{Right};
// ...
};We'll initialize it to Right for all cells in Reset(), as our snake always starts by moving right:
src/Snake/Cell.cpp
// ...
void Cell::Reset() {
FillDirection = Right;
// ...
}
// ...However, when a snake visits a cell, we'll update that cell's FillDirection to keep track of which direction the snake entered the cell from. We'll also set FillPercent to 0.0f when the snake head first enters, and we'll animate this value from 0.0f to 1.0f in the next section:
src/Snake/Cell.cpp
// ...
void Cell::Advance(const SDL_UserEvent& E) {
auto* Data{static_cast<SnakeData*>(E.data1)};
bool isThisCell{
Data->HeadRow == Row &&
Data->HeadCol == Column
};
if (isThisCell) {
if (State == CellState::Snake) {
SDL_Event Event{};
Event.type = UserEvents::GAME_LOST;
SDL_PushEvent(&Event);
return;
}
if (State == CellState::Apple) {
SDL_Event Event{ .type = UserEvents::APPLE_EATEN };
SDL_PushEvent(&Event);
}
State = CellState::Snake;
SnakeDuration = Data->Length;
FillDirection = Data->Direction;
FillPercent = 0.0f;
} else if (State == CellState::Snake) {
if (SnakeDuration == Data->Length) {
FillDirection = Data->Direction;
}
--SnakeDuration;
if (SnakeDuration <= 0) {
State = CellState::Empty;
}
}
}Animating the Snake's Head
To give our snake's head the illusion of sliding into the cell, we'll update our SnakeRect variable to gradually increase how much of the cell is filled with our snake color. Our FillPercent should start at 0.0f when the snake first enters the cell and, by the time the snake is ready to advance to the next cell, FillPercent should be at 1.0f.
As we want our animation to update every frame, we'll use the Tick() function to implement this. We know our cell has animation work to do if its State is Snake, but its FillPercent hasn't yet reached 1.0f. In that case, we'll call a new private GrowHead() function:
src/Snake/Cell.h
// ...
class Cell {
// ...
private:
void GrowHead(float DeltaTime);
// ...
};src/Snake/Cell.cpp
// ...
void Cell::Tick(Uint64 DeltaTime) {
if (State == CellState::Snake && FillPercent < 1.0f) {
GrowHead(float(DeltaTime));
}
}
void Cell::GrowHead(float DeltaTime) {
// TODO: Grow Head
}
// ...Within successive invocations of GrowHead(), we want to increase our FillPercent such that it reaches 1.0f by the time our snake is ready to advance to the next cell. For example, if our Config::ADVANCE_INTERVAL is set to 200, our snake advances every 200 milliseconds, so we want our fill percent to increase from 0.0f to 1.0f over that interval.
The DeltaTime represents the milliseconds passed since the last frame. We can calculate how much FillPercent should increase by dividing DeltaTime by the ADVANCE_INTERVAL. We also ensure FillPercent doesn't exceed 1.0f.
// ...
void Cell::GrowHead(float DeltaTime) {
using namespace Config;
FillPercent += DeltaTime / ADVANCE_INTERVAL;
if (FillPercent > 1.0f) FillPercent = 1.0f;
// TODO: Update SnakeRect
}We now use this FillPercent value in combination with our FillDirection to modify the size or position of our SnakeRect.
The logic is as follows:
- Right: Scale width by
FillPercent. - Down: Scale height by
FillPercent. - Left: Move
xfrom right to left. - Up: Move
yfrom bottom to top.
src/Snake/Cell.cpp
// ...
void Cell::GrowHead(float DeltaTime) {
using namespace Config;
FillPercent += DeltaTime / ADVANCE_INTERVAL;
if (FillPercent > 1.0f) FillPercent = 1.0f;
SnakeRect = BackgroundRect;
if (FillDirection == Right) {
SnakeRect.w = int(CELL_SIZE * FillPercent);
} else if (FillDirection == Down) {
SnakeRect.h = int(CELL_SIZE * FillPercent);
} else if (FillDirection == Left) {
SnakeRect.x = int(BackgroundRect.x +
CELL_SIZE * (1.0f - FillPercent));
} else if (FillDirection == Up) {
SnakeRect.y = int(BackgroundRect.y +
CELL_SIZE * (1.0f - FillPercent));
}
}Running our game, we should now see our snake's head animating smoothly. However, our other snake segments remain on screen even after the snake has left those cells. Let's apply similar logic in reverse to animate the snake's tail leaving cells it no longer occupies.
Animating the Snake's Tail
We can animate the snake's tail leaving a cell in much the same way we animated the head entering. First, we need to update the FillDirection variable with the direction that the snake's head leaves our cell. We can infer this by checking if the SnakeDuration equals the snake's Length, which means the head just passed through.
src/Snake/Cell.cpp
// ...
void Cell::Advance(const SDL_UserEvent& E) {
auto* Data{static_cast<SnakeData*>(E.data1)};
// ...
if (isThisCell) {
// ...
} else if (State == CellState::Snake) {
if (SnakeDuration == Data->Length) {
FillDirection = Data->Direction;
}
--SnakeDuration;
if (SnakeDuration <= 0) {
State = CellState::Empty;
}
}
}Updating Tick()
In our Tick() function, we know that we need to animate the snake out of our cell if its State is not Snake, but it is still rendering some part of the snake segment (i.e., FillPercent > 0). We'll create a new ShrinkTail() private method to handle this:
src/Snake/Cell.h
// ...
class Cell {
// ...
private:
void ShrinkTail(float DeltaTime);
// ...
};src/Snake/Cell.cpp
// ...
void Cell::Tick(Uint64 DeltaTime) {
if (State == CellState::Snake && FillPercent < 1.0f) {
GrowHead(float(DeltaTime));
} else if (
State != CellState::Snake && FillPercent > 0.0f
) {
ShrinkTail(float(DeltaTime));
}
}
void Cell::ShrinkTail(float DeltaTime) {
// TODO: Shrink Tail
}
// ...The logic for ShrinkTail() is the inverse of GrowHead(). We decrease FillPercent and adjust SnakeRect in the opposite direction:
- Decrease
FillPercentbased onDeltaTime. - Adjust
SnakeRectbased on the newFillPercentandFillDirection, effectively shrinking the rendered segment.
src/Snake/Cell.cpp
// ...
void Cell::ShrinkTail(float DeltaTime) {
using namespace Config;
FillPercent -= DeltaTime / ADVANCE_INTERVAL;
if (FillPercent < 0.0f) FillPercent = 0.0f;
SnakeRect = BackgroundRect;
if (FillDirection == Right) {
SnakeRect.x = BackgroundRect.x +
int(CELL_SIZE * (1.0f - FillPercent));
} else if (FillDirection == Left) {
SnakeRect.w = int(CELL_SIZE * FillPercent);
} else if (FillDirection == Up) {
SnakeRect.h = int(CELL_SIZE * FillPercent);
} else if (FillDirection == Down) {
SnakeRect.y = BackgroundRect.y +
int(CELL_SIZE * (1.0f - FillPercent));
}
}Complete Code
The final version of Cell.h and Cell.cpp after all changes is below. Other files remain unchanged from the previous lesson's final state.
Files
Improvement Ideas
Congratulations on building your working Snake game! You've learned a lot about SDL3, game loops, event handling, and animation. However, the learning process doesn't stop here.
An important part of becoming a better programmer is to experiment, iterate, and improve upon your creations. By taking on self-directed challenges, you solidify your understanding and develop problem-solving skills.
Now that you have a functional base, consider exploring some of the following improvements. These will not only enhance your game but also deepen your understanding of C++ and game development principles. Think of these as opportunities to apply what you've learned and push your abilities further.
User-Defined Game Settings
Currently, many of the game's core parameters, like grid size (GRID_ROWS, GRID_COLUMNS) and the snake's movement speed (ADVANCE_INTERVAL), are hardcoded as constants. A more flexible and user-friendly approach would be to allow the player to customize these settings.
Consider adding a simple menu system, perhaps accessible at the start of the game or through a pause screen. This menu could present options to:
- Adjust Grid Size: Let the player choose between different grid dimensions (e.g., small, medium, large).
- Change Snake Speed: Offer options to control how quickly the snake moves (e.g., slow, normal, fast).
- Customize Colors: Allow the player to personalize the snake's color, the background, or the cell colors.
Implementing these features will require you to:
- Create UI elements to represent the settings.
- Handle user input to change the settings.
- Store the selected settings (perhaps in a separate
GameSettingsclass). - Modify your game logic to use these settings instead of hardcoded values.
Input Queue
Currently, every time our game advances, only the most recent keyboard input is used to determine how the snake turns (GameState::NextDirection). This can make our game effectively ignore some inputs if the user provides an additional input before the next Advance().
This behavior is sometimes fine, but many games use an input queue to allow all inputs provided in quick succession to be acted upon.
This involves creating a data structure (like a std::vector or std::queue) that stores a sequence of player inputs. Then, in your game's update loop, you would process all of the inputs from the queue in a way that feels better for the type of game you're making.
Reducing Event Traffic
To keep things simple, our project routed pretty much every event through to every relevant object in our game. This results in a lot of excess traffic, where the object's HandleEvent() functions are invoked with an event the object has no interest in.
Every function call that results in no effect has an unnecessary performance cost. An obvious way to reduce this traffic is having each parent check if an event is relevant to its children before forwarding it:
void HandleEvent(const SDL_Event& E){
if (isRelevantToChildren(E.type) {
for (auto& Child : Children) {
Child.HandleEvent(E);
}
}
};This situation is one example of a problem that can be solved by concepts like function pointers, delegates, and observers that we covered earlier in the course.
Refactoring
Now that you have a complete game, it's a good time to revisit your code and look for areas to improve its structure, readability, and maintainability.
If a class or system doesn't make sense or is difficult to follow, that's a good indicator that it could use some refactoring.
Adding const
To keep our lessons as simple as possible, we restrict the syntax as much as possible to keep the focus on the core concepts. However, there are some more advanced C++ features that you may want to add if you're more comfortable.
One such example is the const keyword, which we covered in more detail in our .
Adding static and inline
More keywords we should consider deploying are static and inline. We cover these keywords in more detail here in our .
Friends and private Constructors
Finally, we should consider how our objects can be constructed. We cover the friend keyword in more detail .
Summary
This lesson focused on implementing a visual improvement: animating the snake's movement across the grid. We replaced the instantaneous cell-to-cell jumps with a smooth sliding effect. This was done with the following components:
FillPercent(0.0fto1.0f) controls how much of a cell is filled by the snake.FillDirectionindicates the direction of snake movement within a cell.SnakeRectis dynamically resized and repositioned in each frame to create the animation.GrowHead()handles the animation of the snake entering a cell.ShrinkTail()handles the animation of the snake leaving a cell.
Video Displays
Learn how to handle multiple monitors in SDL3, including creating windows on specific displays.