1
\$\begingroup\$

I implemented a simple snake clone in C++, using SDL2 for the graphics part. Gameplay-wise, its pretty much classic snake: The player is able to control the snake with "WASD", food gets spawned randomly and if you eat it, you gain speed and length. The game is lost once you collide with yourself. Any tips?

source.cpp

#include <SDL.h>

#include "Framework.h"
#include "Game.h"

int main(int argc, char* argv[])
{   
    Snake::SDL_Framework framework("Snake", 800, 800);
    Snake::Game game(framework);

    game.run();

    return 0;
}

Framework.h

#ifndef SDL_FRAMEWORK
#define SDL_FRAMEWORK

#include <SDL.h>

#include <cstdint>
#include <string>
#include <exception>

namespace Snake
{
    class SDL_Framework
    {
    public:

        SDL_Framework() : main_window{ nullptr }, renderer{ nullptr }, window_height{ 0 }, window_width{ 0 } {};
        SDL_Framework(const std::string& window_name, int32_t height, int32_t width);
        ~SDL_Framework();

        void clear();
        void update();
        int32_t get_height() const { return window_height; }
        int32_t get_width() const { return window_width; }

        SDL_Renderer* renderer;

    private:

        SDL_Window* main_window;
        int32_t window_height;
        int32_t window_width;

    };
}

#endif

Framework.cpp

#include "Framework.h"

static void process_error(const std::string& msg)
{
    throw std::runtime_error(msg);
}

namespace Snake
{
    SDL_Framework::SDL_Framework(const std::string& window_name, int32_t height, int32_t width)
    {
        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) < 0)
        {
            process_error(std::string("Error while trying to initialise SDL! SDL_Error: ") + SDL_GetError());
        }

        main_window = SDL_CreateWindow(window_name.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, height, width, SDL_WINDOW_SHOWN);
        if (main_window == nullptr)
        {
            process_error(std::string("Error while trying to create SDL_Window! SDL_Error: ") + SDL_GetError());
        }

        window_height = height;
        window_width = width;

        renderer = SDL_CreateRenderer(main_window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
        if (renderer == nullptr)
        {
            process_error(std::string("Error while trying to create renderer! SDL_Error: ") + SDL_GetError());
        }

        SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
    }

    SDL_Framework::~SDL_Framework()
    {
        SDL_Quit();
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(main_window);
    }

    void SDL_Framework::clear()
    {
        SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
        SDL_RenderClear(renderer);
    }

    void SDL_Framework::update()
    {
        SDL_RenderPresent(renderer);
    }
}

Game.h

#ifndef SNAKE_GAME
#define SNAKE_GAME

#include "Framework.h"
#include "Board.h"
#include "Player.h"

namespace Snake
{
    class Game
    {
    public:

        Game(const SDL_Framework& uframework);

        void run();
        void update();
        void render();

    private:

        SDL_Framework framework;
        Board board;
        Player player;

        int32_t frame;

    };
}

#endif

Game.cpp

#include "Game.h"

namespace Snake
{
    Game::Game(const SDL_Framework& uframework) : frame{0}
    {
        framework = uframework;

        int32_t cells_per_side = 25;
        int32_t cell_size = framework.get_height() / cells_per_side;

        board = Board(framework.get_height(), framework.get_width(), cell_size);
        board.generate_food();

        int32_t middle = cell_size * (framework.get_height() / cell_size / 2); // calculating the middle of the board in cell roster, assuming length == width
        player = Player(middle, middle);
    }

    void Game::run()
    {
        bool running = true;
        SDL_Event event;

        do {

            frame++;

            while (SDL_PollEvent(&event) != 0)
            {
                switch (event.type)
                {
                case SDL_QUIT:
                {
                    running = false;
                    break;
                }
                }
            }

            update();
            framework.clear();
            render();
            framework.update();

        } while (running);
    }

    void Game::update()
    {
        if (player.check_food_collision(board.get_food_pos()))
        {
            board.generate_food();
        }

        player.update(board.get_borders(), board.get_cell_size(), frame);
    }

    void Game::render()
    {
        board.render(framework.renderer);
        player.render(framework.renderer, board.get_cell_size());
    }
}

Board.h

#ifndef SNAKE_BOARD
#define SNAKE_BOARD

#include <SDL.h>

#include <utility>
#include <random>

#include "Cell.h"

namespace Snake
{
    class Board
    {
    public:

        Board() = default;
        Board(int32_t ulength, int32_t uwidth, int32_t cell_size);

        void generate_food();
        void render(SDL_Renderer* renderer) const;
        std::pair<int32_t, int32_t> get_food_pos() const { return food.get_pos(); }
        std::pair<int32_t, int32_t> get_borders() { return { length, width }; }
        int32_t get_cell_size() const { return cell_size; }

    private:

        Cell food;
        int32_t length;
        int32_t width;
        int32_t cell_size;
        std::mt19937 gen;
        std::uniform_int_distribution<int32_t> distr;

    };
}

#endif

Board.cpp

#include "Board.h"

namespace Snake
{
    Board::Board(int32_t ulength, int32_t uwidth, int32_t ucell_size) : length{ ulength }, width{ uwidth }, cell_size{ ucell_size }
    {
        gen = std::mt19937(std::random_device{}());
        distr = std::uniform_int_distribution<int32_t>(0, (length / cell_size) - 1);
    }

    void Board::generate_food()
    {
        int32_t food_x = distr(gen) * cell_size;
        int32_t food_y = distr(gen) * cell_size;

        food.update_pos(food_x, food_y);
    }

    void Board::render(SDL_Renderer* renderer) const
    {
        SDL_SetRenderDrawColor(framework.renderer, 0, 0, 0, 255);

        // render cell grid
        for (int32_t distance = cell_size; distance < length; distance += cell_size)
        {
            SDL_RenderDrawLine(renderer, distance, 0, distance, length);
            SDL_RenderDrawLine(renderer, 0, distance, width, distance);
        }

        // render food
        food.render(renderer, cell_size, SDL_Color{ 0, 0, 0, 255 });
    }
}

Player.h

#ifndef SNAKE_PLAYER
#define SNAKE_PLAYER

#include <SDL.h>

#include <vector>

#include "Cell.h"

namespace Snake
{
    class Player
    {
        enum Direction
        {
            UP, DOWN, RIGHT, LEFT
        };

    public:

        Player() = default;
        Player(int32_t x, int32_t y) :
            length{ 1 },
            growth_remaining{ 2 },
            growth_per_cell{ 2 },
            cur_direction{LEFT},
            speed{ 30 },
            speed_gain{2},
            max_speed{ 4 }
        {
            head.update_pos(x, y);
        }

        void update(std::pair<int32_t, int32_t> borders, int32_t cell_size, int32_t frame);
        void render(SDL_Renderer* renderer, int32_t cell_size) const;
        bool check_food_collision(std::pair<int32_t, int32_t> food_pos);

    private:

        void update_position(std::pair<int32_t, int32_t> borders, int32_t cell_size, int32_t frame);
        void update_direction();
        bool check_self_collision();
        void reset();

        int32_t length;
        int32_t growth_remaining;
        int32_t growth_per_cell;
        Direction cur_direction;
        int32_t speed; // lower is faster, updates player every {speed} frames
        int32_t speed_gain;
        int32_t max_speed;

        Cell head;
        std::vector<Cell> body;

        std::pair<int32_t, int32_t> get_next_pos(int32_t cell_size, std::pair<int32_t, int32_t> cur_pos);

    };
}

#endif

Player.cpp

#include "Player.h"

namespace Snake
{
    void Player::update(std::pair<int32_t, int32_t> borders, int32_t cell_size, int32_t frame)
    {
        update_direction();
        update_position(borders, cell_size, frame);
        
        if (check_self_collision())
        {
            reset();
        }
    }

    void Player::update_position(std::pair<int32_t, int32_t> borders, int32_t cell_size, int32_t frame)
    {
        if (frame % speed) return; // only update when neccessary

        if (growth_remaining)
        {
            body.insert(std::begin(body), Cell(head.get_pos().first, head.get_pos().second));

            growth_remaining--;
        }
        else
        {
            if (!body.empty())
            {
                for (uint32_t i = body.size() - 1; i > 0; --i)
                {
                    body[i].update_pos(body[i-1].get_pos().first, body[i - 1].get_pos().second);
                }
                body[0].update_pos(head.get_pos().first, head.get_pos().second);
            }
        }

        auto new_pos = get_next_pos(cell_size, head.get_pos());

        // implement player moving over border
        if (new_pos.first > borders.second) { new_pos.first = 0; }
        if (new_pos.first < 0) { new_pos.first = borders.second - cell_size; }
        if (new_pos.second > borders.first) { new_pos.second = 0; }
        if (new_pos.second < 0) { new_pos.second = borders.first - cell_size; }

        head.update_pos(new_pos.first, new_pos.second);
    }

    void Player::update_direction()
{
        const Uint8* key_state = SDL_GetKeyboardState(nullptr);

        if (key_state[SDL_SCANCODE_W])
        {
            cur_direction = UP;
        }
        else if (key_state[SDL_SCANCODE_S])
        {
            cur_direction = DOWN;
        }
        else if (key_state[SDL_SCANCODE_A])
        {
            cur_direction = LEFT;
        }
        else if (key_state[SDL_SCANCODE_D])
        {
            cur_direction = RIGHT;
        }
    }

    bool Player::check_self_collision()
    {
        for (auto& bodypart : body)
        {
            if (head.get_pos() == bodypart.get_pos())
            {
                return true;
            }
        }

        return false;
    }

    void Player::reset()
    {
        body.clear();
        growth_remaining = 2;
        speed = 30;
    }

    void Player::render(SDL_Renderer* renderer, int32_t cell_size) const
    {
        head.render(renderer, cell_size, SDL_Color{255, 100, 0, 255});

        for (auto& cell : body)
        {
            cell.render(renderer, cell_size);
        }
    }

    bool Player::check_food_collision(std::pair<int32_t, int32_t> food_pos)
    {
        if (head.get_pos() == food_pos)
        {
            growth_remaining += growth_per_cell;

            speed -= speed_gain; // increase speed
            if (speed < max_speed) { speed = max_speed; }

            return true;
        }
        
        return false;
    }


    std::pair<int32_t, int32_t> Player::get_next_pos(int32_t cell_size, std::pair<int32_t, int32_t> cur_pos)
    {
        std::pair<int32_t, int32_t> next_pos(cur_pos);

        switch (cur_direction)
        {
            case UP:
            {
                next_pos.second -= cell_size;
                break;
            }
            case DOWN:
            {
                next_pos.second += cell_size;
                break;
            }
            case RIGHT:
            {
                next_pos.first += cell_size;
                break;
            }
            case LEFT:
            {
                next_pos.first -= cell_size;
                break;
            }
        }

        return next_pos;
    }
}

Cell.h

#ifndef SNAKE_CELL
#define SNAKE_CELL

#include <SDL.h>

#include <cstdint>
#include <utility>

namespace Snake
{
    class Cell
    {
    public:

        Cell() = default;
        Cell(int32_t x, int32_t y) : 
            pos{ x, y }
        {}

        void render(SDL_Renderer* renderer, int32_t cell_size, SDL_Color color = SDL_Color{ 255, 30, 20, 255 }) const;
        void update_pos(int32_t x, int32_t y) { pos.first = x; pos.second = y; }
        std::pair<int32_t, int32_t> get_pos() const { return pos; }

    private:

        std::pair<int32_t, int32_t> pos;

    };
}

#endif

Cell.cpp

#include "Cell.h"

namespace Snake
{
    void Cell::render(SDL_Renderer* renderer, int32_t cell_size, SDL_Color color) const
    {
        // set draw color to red
        SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);

        SDL_Rect rect = { pos.first + 1, pos.second + 1, cell_size - 1, cell_size - 1 };
        SDL_RenderFillRect(renderer, &rect);
    }
}

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Well done. Only a few minor things come to mind. I would suggest replacing std::pair<int32_t, int32_t> with struct or wrapping into using. Also, it is a good habit to use forward declarations to reduce compile times. Though it is useful only in huge projects. In a huge project, I would suggest having file with forward declarations and move as much includes from header to source file. \$\endgroup\$
    – xevepisis
    Feb 16, 2021 at 9:26
  • \$\begingroup\$ @xevepisis Thank you for the feedback! \$\endgroup\$
    – Tom Gebel
    Feb 16, 2021 at 15:28

1 Answer 1

2
\$\begingroup\$

double deletion issue:

Snake::SDL_Framework framework("Snake", 800, 800);
Snake::Game game(framework);

...

Game::Game(const SDL_Framework& uframework) : frame{0}
{
    framework = uframework;

...

SDL_Framework::~SDL_Framework()
{
    SDL_Quit();
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(main_window);
}

We have one SDL_Framework object in main(). Then we copy it to a member variable in the Game constructor.

So now we have two SDL_Framework objects. That means two destructor calls... and we're calling SDL_DestroyRenderer and SDL_DestroyWindow twice with the same handles! We might get away with it right now because the SDL_Quit call is first, otherwise we'd probably be trying to free the same memory twice.

What we really need is to make SDL_Framework non-copyable and store only a reference or pointer to it in Game. Alternatively we could make SDL_Framework moveable only, and move it into Game instead.


window size bug:

SDL_CreateWindow(window_name.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, height, width, SDL_WINDOW_SHOWN);

The height and width parameters are the wrong way around in this call. (It's conventional to use (x, y) for coordinates instead of (y, x)).


naming nitpick:

void SDL_Framework::update()
{
    SDL_RenderPresent(renderer);
}

update() is very vague and potentially confusing given its use elsewhere. present_buffer() or swap_buffers() or something similar might be a better name.


access-control nitpick:

class Game
{
public:

    Game(const SDL_Framework& uframework);

    void run();
    void update();
    void render();

We only call the run() function from outside the class, so update() and render() could be private.


coordinate class:

std::pair<int32_t, int32_t>

As the comments point out, it's definitely worth using a simple struct so we know which coordinate is which:

template<class T>
struct Vec2 { T x, y; };

frame-rate (in)dependence:

    int32_t speed; // lower is faster, updates player every {speed} frames

    ...

    if (frame % speed) return; // only update when neccessary

This might work if we had a fixed frame-rate. But I don't see anything in the main loop limiting the frame-rate. So on a fast computer, the snake will be much quicker than on a slower one.

We should use the facilities in the <chrono> standard header to make an actual timer for this, e.g. in Player::update_position():

auto now = std::chrono::high_resolution_clock::now();
if (now > last_update + time_between_updates) { last_update = now; ... do update ... }

Where time_between_updates works similarly to the speed variable, but represents an actual time.


unnecessary class:

class Cell
{
public:

    Cell() = default;
    Cell(int32_t x, int32_t y) : 
        pos{ x, y }
    {}

    void render(SDL_Renderer* renderer, int32_t cell_size, SDL_Color color = SDL_Color{ 255, 30, 20, 255 }) const;
    void update_pos(int32_t x, int32_t y) { pos.first = x; pos.second = y; }
    std::pair<int32_t, int32_t> get_pos() const { return pos; }

private:

    std::pair<int32_t, int32_t> pos;

};

I'm not sure we really need this class. It's just a coordinate pair.

The rendering could be a free function, or inlined where it's used.


player update improvements:

        body.insert(std::begin(body), Cell(head.get_pos().first, head.get_pos().second));

Inserting at the front of a vector is quite slow, because we have to move every other element backwards to make room. Perhaps a deque would be a better? (Though iteration may be slightly slower).

        if (!body.empty())
        {
            for (uint32_t i = body.size() - 1; i > 0; --i)
            {
                body[i].update_pos(body[i-1].get_pos().first, body[i - 1].get_pos().second);
            }
            body[0].update_pos(head.get_pos().first, head.get_pos().second);
        }

Every cell simply follows the next cell, right? In that case, can't we just erase the last cell in the vector, and insert a new cell at the front?

(Perhaps we could use some sort of fancy circular index system, where we just shift over the head index and change the last (now first) coordinate... but that's probably a bit overcomplicated).


\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the feedback! About the framerate-independence: SDL_RENDERER_PRESENTVSYNC in the SDL_CreateRenderer() call in Framework.cpp should cap to 60fps. \$\endgroup\$
    – Tom Gebel
    Feb 16, 2021 at 15:35
  • 1
    \$\begingroup\$ Not 60fps, but whatever your monitor's refresh rate happens to be (and depending on graphics card drivers settings). So anywhere from 50Hz on old monitors, to 240Hz on fancy gaming monitors. \$\endgroup\$
    – user673679
    Feb 16, 2021 at 16:12
  • \$\begingroup\$ Of course, you are right, my monitor's refresh rate just happens to be 60Hz. \$\endgroup\$
    – Tom Gebel
    Feb 16, 2021 at 16:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.