In this lesson, we'll build the foundation of our Snake game by implementing a grid system. We'll create Cell
and Grid
classes to manage the game state, handle rendering, and prepare for the snake's movement.
By the end, you'll have a fully functional grid that displays the initial snake position and first apple.
Cell
ClassOur Cell
class will include the standard suite of methods - HandleEvent()
, Tick()
, and Render()
, which we’ll use later.
Additionally, our Cell
objects will need to know where they are within the grid. We’ll provide this Row
and Column
as constructor arguments, and store them in member variables.
We’ll also need to render apple images in our cells in the future, so we’ll give our Cell
objects a reference to our Assets
object.
// Cell.h
#pragma once
#include <SDL.h>
#include "Assets.h"
class Cell {
public:
Cell(int Row, int Column, Assets& Assets)
: Row(Row),
Column(Column),
Assets{Assets}
{}
void HandleEvent(SDL_Event& E) {}
void Tick(Uint32 DeltaTime) {}
void Render(SDL_Surface* Surface) {}
private:
int Row;
int Column;
Assets& Assets;
};
Grid
ClassNext, we’ll create a Grid
class to manage all of our Cell
objects. First, let’s update our GameConfig.h
file with some new configuration variables to support this.
GRID_COLUMNS
controls how many columns of cells we’ll haveGRID_ROWS
controls how many rows of cells we’ll have. We’ll use a short grid of only 5
rows in our example to improve lesson presentation, but in most cases, we’ll want the row and column number of our grid to be similar, such that our game’s play area is approximately square.CELL_SIZE
controls the visual size of each cell. We’ll use square cells, so we’ll use this variable for both the width and height of the cell.PADDING
controls the size of the visual gap between elements. For now, this padding will apply between the edges of our window and the edges of our grid, but we’ll use it for additional layout calculations later.We’ll also use these configuration variables to calculate the visual dimensions GRID_HEIGHT
and GRID_WIDTH
, and we’ll also update our WINDOW_HEIGHT
and WINDOW_WIDTH
to adapt the window size to the size of our grid:
// GameConfig.h
// ...
namespace Config{
// Game Settings
// ...
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 CELL_SIZE{36};
inline constexpr int GRID_HEIGHT{
CELL_SIZE * GRID_ROWS};
inline constexpr int GRID_WIDTH{
CELL_SIZE * GRID_COLUMNS};
inline constexpr int PADDING{5};
inline constexpr int WINDOW_HEIGHT{
GRID_HEIGHT + PADDING * 2};
inline constexpr int WINDOW_WIDTH{
GRID_WIDTH + PADDING * 2};
// ...
}
// ...
We’ll create a Grid.h
file for our Grid
class, which will store all our Cell
objects in a std::vector
:
// Grid.h
#pragma once
#include <vector>
#include "Cell.h"
class Grid {
private:
std::vector<Cell> Cells;
};
When constructing our Grid
, we’ll receive a reference to our Assets
object. We’ll also fill our Cells
vector with all the cells our grid needs, based on the GRID_ROWS
and GRID_COLUMNS
configuration variables. We’ll also pass the row and column to the constructor for each Cell
, as well as a reference to our Assets
:
// Grid.h
// ...
#include "Cell.h"
#include "GameConfig.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);
}
}
}
};
Our Grid
class will also need our usual HandleEvent()
, Tick()
and Render()
methods. Every invocation of these functions will be forwarded to all the Cell
objects within the Grid
:
// Grid.h
// ...
class Grid {
public:
// ...
void HandleEvent(SDL_Event& E) {
for (auto& Cell : Cells) {
Cell.HandleEvent(E);
}
}
void Tick(Uint32 DeltaTime) {
for (auto& Cell : Cells) {
Cell.Tick(DeltaTime);
}
}
void Render(SDL_Surface* Surface) {
for (auto& Cell : Cells) {
Cell.Render(Surface);
}
}
// ...
};
Finally, let’s update our GameUI
to construct a Grid
, passing along the Assets
reference. Our GameUI
will also forward all HandleEvent()
, Tick()
and Render()
calls to our Grid
:
// GameUI.h
// ...
#include "Grid.h"
class GameUI {
public:
GameUI(): Grid{Assets} {}
void HandleEvent(SDL_Event& E) {
Grid.HandleEvent(E);
}
void Tick(Uint32 DeltaTime) {
Grid.Tick(DeltaTime);
}
void Render(SDL_Surface* Surface) {
Grid.Render(Surface);
}
private:
Grid Grid;
Assets Assets;
};
At this point, our program should compile, and our window’s size should adapt based on our configuration values:
Our grid isn’t visible yet, so let’s render it.
To make our grid visible, we’ll need to render all of our cells. The visual position of each cell should correspond with the Row
and Column
member variables we initialized it with.
We’d also like players to be able to tell where one cell ends and the next begins, so we’ll render them such that adjacent cells have a slightly different color. By the end of this section, we’ll want our Grid
to look like this:
Currently, the Render()
function of our Cell
objects is already being called on every frame. We just need to implement the logic in that function to render our cell onto the Surface
that is provided as an argument. In this case, that Surface
is the window surface.
As you may remember from earlier in the course, we can use the SDL_FillRect()
function to render a rectangle onto a surface. It looks like this:
void Render(SDL_Surface* Surface) {
SDL_FillRect(Surface, &Rect, Color);
}
In this example, &Rect
is a pointer to an SDL_Rect
defining where we want the rectangle to be, and Color
is an SDL_Color
defining the color we want to use. Let’s figure out what this rectangle and color should be for each cell.
An SDL_Rect
is a simple struct with 4 variables, defining a rectangle. The variables are, in order:
x
: How far the rectangle is from the left edgey
: How far the rectangle is from the top edgew
: The width of the rectangleh
: The height of the rectangleFor each Cell
object, we need to construct an SDL_Rect
to specify where that cell should be rendered within the SDL_Surface
provided to its Render()
function. In this program, the Surface
we’re providing is the window surface, so the SDL_Rect
defines where the cell should appear within our game’s window.
Each cell has Row
and Column
member variables, so to get the required position based on those variables, our SDL_Rect
will be as follows:
x
will be based on multiplying which Column
we’re in by the width of each column (from the CELL_SIZE
variable in GameConfig.h
). We’ll also add the PADDING
value from GameConfig.h
so there’s a gap between the left edge of the window and the left edge of the grid.y
will be based on multiplying which Row
we’re in by the height of each row (from the CELL_SIZE
variable in GameConfig.h
). We’ll also add the PADDING
value from GameConfig.h
so there’s a gap between the top edge of the window and the top edge of the grid.w
will be the width of the cell, from the CELL_SIZE
variable in GameConfig.h
.h
will be the height of the cell, from the CELL_SIZE
variable in GameConfig.h
.We don’t want to perform these calculations in our Render()
function, as this gets called every frame. Instead, let’s store it as a BackgroundRect
member variable, and perform these calculations a single time when our Cell
is initialized:
// Cell.h
// ...
#include "GameConfig.h"
class Cell {
// ...
private:
// ...
int Row;
int Column;
SDL_Rect BackgroundRect{
Column * Config::CELL_SIZE + Config::PADDING,
Row * Config::CELL_SIZE + Config::PADDING,
Config::CELL_SIZE,
Config::CELL_SIZE
};
};
Note that because we’re using Row
and Column
in this initialization, we need to ensure BackgroundRect
is initialized after these variables.
Let’s configure the alternating colors used by our grid by adding CELL_COLOR_A
and CELL_COLOR_B
variables to our GameConfig.h
file:
// GameConfig.h
// ...
namespace Config{
// ...
// Colors
// ...
inline constexpr SDL_Color CELL_COLOR_A{
171, 214, 82, 255};
inline constexpr SDL_Color CELL_COLOR_B{
161, 208, 74, 255};
// ...
}
// ...
To create the alternating color pattern, we can split out cells into "even" and "odd" cells. We can do this by adding the row and column together and using the modulo operator %
to determine if the sum is divisible by 2. We covered the modulus operator and similar techniques in our beginner course:
We’ll then give the even and odd cells different colors. Again, we don’t want to perform this calculation on every frame, so we’ll do it a single time when our Cell
is first created, and store the result as a member variable called BackgroundColor
. Again, because the BackgroundColor
initialization depends on the Row
and Column
values, we need to ensure Row
and Column
are initialized first:
// Cell.h
// ...
#include "GameConfig.h"
class Cell {
// ...
private:
// ...
int Row;
int Column;
// ...
SDL_Color BackgroundColor{
(Row + Column) % 2 == 0
? Config::CELL_COLOR_A
: Config::CELL_COLOR_B
};
};
Now we can implement our cell's Render()
method to draw onto the window surface. We'll use SDL's SDL_FillRect()
function to render our cell at the position specified by BackgroundRect
using the color stored in BackgroundColor
.
The SDL_FillRect()
function expects colors in a format specific to the surface we're drawing on. We convert our SDL_Color
to this format using SDL_MapRGB()
, which takes three parameters for red, green, and blue color components:
// Cell.h
// ...
class Cell {
public:
// ...
void Render(SDL_Surface* Surface) {
SDL_FillRect(Surface, &BackgroundRect,
SDL_MapRGB(
Surface->format,
BackgroundColor.r,
BackgroundColor.g,
BackgroundColor.b
)
);
}
// ...
};
When we run our program now, we should see a checkerboard pattern of alternating colors filling our window:
In addition to rendering a solid background color, our Cell
objects need the ability to render snake segments and apple images. Let’s add a CellState
type and data member to keep track of whether our Cell
contains a snake segment, apple, or if it is empty:
// Cell.h
// ...
enum CellState { Snake, Apple, Empty };
class Cell {
// ...
private:
CellState CellState;
};
Most of our cells will initially be empty, but we also want our game to start with a snake and an apple on the grid. We’ll start with a two-segment snake in columns 2 and 3 of the middle row, and the first apple in column 11 of that same row:
We could write this code in our constructor, but later, we’ll want the ability to restart our game and reset our cells back to this initial state. Therefore, we’ll create a dedicated private Initialize()
function for this which we can reuse as needed. For now, we’ll just call it from our constructor:
// Cell.h
// ...
class Cell {
public:
Cell(int Row, int Column, Assets& Assets)
: Row(Row),
Column(Column),
Assets{Assets}
{
Initialize();
}
private:
void Initialize() {
CellState = Empty;
int MiddleRow{Config::GRID_ROWS / 2};
if (Row == MiddleRow && Column == 2) {
CellState = Snake;
} else if (Row == MiddleRow && Column == 3) {
CellState = Snake;
} else if (Row == MiddleRow && Column == 11) {
CellState = Apple;
}
}
// ...
};
Let’s update our Render()
method so we can see these apples and snake segments. We’ll first update GameConfig.h
to define a color for our snake segments:
// GameConfig.h
// ...
namespace Config{
// ...
inline constexpr SDL_Color SNAKE_COLOR{
67, 117, 234, 255};
// ...
}
// ...
In Cell.h
, after rendering our background color, we’ll check if our CellState
is either Apple
or Snake
. We’ll then render the apple or snake on top of our background as appropriate.
// Cell.h
// ...
class Cell {
public:
// ...
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
)
);
}
}
};
Our program should now look like this:
All three of these blitting operations (the background, the apple, and the snake) are using the same SDL_Rect
- the BackgroundRect
.
In the Apple
case, this is reasonable - the apple image is a semitransparent png, so the background will remain visible through the transparent parts of the apple.
However, this is not true of the Snake
case. The SDL_FillRect()
call in that condition will update every pixel in the rectangle. As such, the blitting of the background color was wasteful - all those pixels were being overwritten by the snake.
However, we’ll later update this function such that the snake is only being rendered to part of the cell, leaving part of the background visible. So, we’ll keep this inefficient code in place for now.
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 Config{
// Game Settings
inline const std::string GAME_NAME{"Snake"};
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 FOOTER_HEIGHT{0};
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 <vector>
#include "Cell.h"
#include "GameConfig.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);
}
}
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;
};
#pragma once
#include <SDL.h>
#include "GameConfig.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) {}
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
)
);
}
}
private:
void Initialize() {
CellState = Empty;
int MiddleRow{Config::GRID_ROWS / 2};
if (Row == MiddleRow && Column == 2) {
CellState = Snake;
} else if (Row == MiddleRow && Column == 3) {
CellState = Snake;
} else if (Row == MiddleRow && Column == 11) {
CellState = Apple;
}
}
int Row;
int Column;
CellState CellState;
Assets& Assets;
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
};
};
#pragma once
#include <SDL.h>
#include "Grid.h"
#include "Assets.h"
class GameUI {
public:
GameUI() : Grid{Assets} {}
void HandleEvent(SDL_Event& E) {
Grid.HandleEvent(E);
}
void Tick(Uint32 DeltaTime) {
Grid.Tick(DeltaTime);
}
void Render(SDL_Surface* Surface) {
Grid.Render(Surface);
}
private:
Grid Grid;
Assets Assets;
};
Files not listed above have not been changed since the previous section.
In this lesson, we established the foundational grid architecture for our Snake game. We implemented a cell-based system that manages game state and rendering through two key classes: Cell
and Grid
.
Key steps from this lesson:
Cell
class to manage individual grid positionsCellState
enum to track cell contentsSDL_Rect
structsSDL_FillRect()
Grid
class to coordinate multiple cellsBuild the foundational grid structure that will power our Snake game's movement and collision systems.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games