Building the Snake Grid

Build the foundational grid structure that will power our Snake game's movement and collision systems.
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

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.

Screenshot of the initial state of our snake game

Creating a Cell Class

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

Creating a Grid Class

Next, 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 have
  • GRID_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:

Screenshot of the blank game window responding to grid size changes

Our grid isn’t visible yet, so let’s render it.

Rendering Cells

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:

Screenshot of the empty snake grid

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.

Determining Rendering Position

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 edge
  • y: How far the rectangle is from the top edge
  • w: The width of the rectangle
  • h: The height of the rectangle

For 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:

  • Our 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.
  • Our 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.
  • The w will be the width of the cell, from the CELL_SIZE variable in GameConfig.h.
  • The 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.

Configuring Cell Colors

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

// ...

Determining Background Color

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

Surface Blitting

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:

Screenshot of the empty snake grid

Rendering Apples and Snake Segments

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:

Screenshot of the initial state of our snake game

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:

Screenshot of the initial state of our snake game

Performance Note

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 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 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.

Summary

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:

  • Created a Cell class to manage individual grid positions
  • Created a CellState enum to track cell contents
  • Managed rendering positions using SDL_Rect structs
  • Created an alternating color pattern for visual clarity
  • Rendered the cells via surface blitting using SDL_FillRect()
  • Implemented a Grid class to coordinate multiple cells
  • Used configuration variables to make the grid system flexible and maintainable
  • Initialized the starting snake position and the first apple
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
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

    63.
    Building the Snake Grid

    Build the foundational grid structure that will power our Snake game's movement and collision systems.


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