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);
}
}
std::pair<int32_t, int32_t>
with struct or wrapping intousing
. 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\$