Description
I've written the snake game using C++ and FLTK. For simplifying the use of FLTK, a built-up library written by Bjarne Stroustrup was used. Bellow located main parts of the code written by me, a whole project can be found on GitHub: https://github.com/WumpusHunter/Snake-game.
Source.cpp
/*
Snake game
Revision history:
Written by Oleg Kovalevskiy in August 2020
*/
//------------------------------------------------------------------------------------
#include "Game_window.h"
using namespace Graph_lib;
//------------------------------------------------------------------------------------
int main()
try {
// Window with top-left angle at (100, 100), of size 600 * 400, labeled "Snake game"
Snake_window win{ Point{ 100, 100 }, 600, 400, "Snake game" };
return gui_main();
}
catch (const exception& e) {
cerr << "Error message: " << e.what() << '\n';
return 1;
}
catch (...) {
cerr << "Unknown error\n";
return 1;
}
//------------------------------------------------------------------------------------
Game_window.h
// Snake game's window
//------------------------------------------------------------------------------
#pragma once
#include "GraphicsLib/Window.h"
#include "GraphicsLib/GUI.h"
#include "GraphicsLib/Graph.h"
#include "Game_graph.h"
//------------------------------------------------------------------------------
namespace Graph_lib {
//------------------------------------------------------------------------------
// Invariant: w > 0, h > 0
class Snake_window : public Window { // Game window
public:
// Construction
Snake_window(Point xy, int w, int h, const string& lab);
private:
// Callback functions
int handle(int event) override;
static void cb_game_loop(Address pw);
static void cb_pause(Address, Address pw);
static void cb_new_game(Address, Address pw);
static void cb_quit(Address, Address pw);
static void cb_game(Address, Address pw);
static void cb_help(Address, Address pw);
// Action functions
void start();
void game_loop();
bool is_pause();
void pause();
void new_game();
void quit();
void game();
void help();
int current_score();
void put_score();
void show_graphics();
void hide_graphics();
private:
// Graphics
Grid field;
Snake snake;
Rectangle fruit;
// GUI
Menu game_menu;
Button game_button;
Button help_button;
Text_box help_box;
Out_box score_box;
Out_box max_score_box;
};
//------------------------------------------------------------------------------
} // End of Graph_lib namespace
//------------------------------------------------------------------------------
Game_window.cpp
// Snake game's window
//------------------------------------------------------------------------------
#include "Game_window.h"
//------------------------------------------------------------------------------
namespace Graph_lib {
//------------------------------------------------------------------------------
// Min possible size of window
constexpr int min_w = 400; // Window's min width
constexpr int min_h = 300; // Window's min height
// Size of cells
constexpr int cell_w = 50; // Cell's width
constexpr int cell_h = 50; // Cell's height
// Default parameters of snake
constexpr int snake_sz = 3; // Snake's length
// Default location of graphics
Point snake_xy = { 0, 0 }; // Snake's location
Point fruit_xy = { 0, 0 }; // Fruit's location
// Size of widgets
constexpr int widget_h = 25; // Widgets' height
constexpr int out_box_w = 30; // Output boxes' width
constexpr int button_w = 100; // Buttons' width
// Indexes of game menu's buttons
constexpr int new_game_ind = 0; // New game button's index
constexpr int pause_ind = 1; // Pause button's index
constexpr int quit_ind = 2; // Quit button's index
// Constructs window with top-left angle at xy, of size w * h (if
// it's not less than min, which is 400 * 300), labeled with lab
Snake_window::Snake_window(Point xy, int w, int h, const string& lab)
: Window{ xy, w >= min_w ? w - w % cell_w : min_w, h >= min_h ? h - h % cell_h : min_h, lab },
field{ Point{ 0, cell_h }, cell_w, cell_h, x_max() / cell_w, (y_max() - cell_h) / cell_h },
snake{ Point{ snake_sz * cell_w, y_max() / 2 }, cell_w, cell_h, snake_sz },
fruit{ Point{ x_max() - cell_w * 2, y_max() / 2 }, cell_w, cell_h },
game_menu{ Point{ 0, 0 }, button_w, widget_h, Menu::Kind::horizontal, "Game" },
game_button{ Point{ 0, 0 }, button_w, widget_h, "&Game", cb_game },
help_button{ Point{ button_w, 0 }, button_w, widget_h, "&Help", cb_help },
help_box{ Point{ 0, cell_h }, x_max(), y_max() - cell_h, "" },
score_box{ Point{ cell_w * 2, widget_h }, out_box_w, widget_h, "Current score: " },
max_score_box{ Point{ cell_w * 4 + out_box_w, widget_h }, out_box_w, widget_h, "Max score: " }
{
if (w <= 0 || h <= 0) // Error handling
throw invalid_argument("Bad Snake_window: non-positive size");
// Keep default location of graphics
snake_xy = snake.point(0);
fruit_xy = fruit.point(0);
// Attach graphics to window
attach(field);
attach(snake);
attach(fruit);
// Attach widgets to window
game_menu.attach(new Button{ Point{ 0, 0 }, 0, 0, "&New game", cb_new_game });
game_menu.attach(new Button{ Point{ 0, 0 }, 0, 0, "&Pause", cb_pause });
game_menu.attach(new Button{ Point{ 0, 0 }, 0, 0, "&Quit", cb_quit });
attach(game_menu);
attach(game_button);
attach(help_button);
attach(help_box);
attach(score_box);
attach(max_score_box);
// Default value for graphics
show_graphics();
put_on_top(snake);
// Default value for widgets
game_menu.hide();
help_box.put(" SNAKE GAME\n"
" Snake is a video game concept where the player maneuvers a line\n"
"that grows in length, with the line itself being a primary obstacle.\n"
"The concept originated in the 1976 arcade game Blockade.\n"
" GAMEPLAY\n"
" The player controls an object on a bordered plane. As it moves for-\n"
"ward, it leaves a trail behind, resembling a moving snake. The snake\n"
"has a specific length, so there is a moving tail a fixed number of units\n"
"away from the head. The player loses when the snake runs into the\n"
"screen border or itself.\n"
" A sole player attempts to eat items by running into them with the he-\n"
"ad of the snake. Each item eaten makes the snake longer, so con-\n"
"trolling is progressively more difficult.\n"
" CONTROL\n"
" The snake moves forward automatically, everything you need to do\n"
"is to choose the direction of moving. To choose the direction of mov-\n"
"ing use arrow-buttons, that is,\n"
"1) Left-arrow - to move in the left direction;\n"
"2) Up-arrow - to move in the up direction;\n"
"3) Right-arrow - to move in the right direction;\n"
"4) Down-arrow - to move in the down direction.\n"
"Remember: you can't rotate the snake's head to the opposite direc-\n"
"tion, for instance, from the left to the right, or from the up to the\n"
"down.\n"
" ADDITIONAL NOTES\n"
" Good luck on the game, try to eat as much as you can!\n");
help_box.hide();
score_box.put(0);
max_score_box.put(0);
}
// Handles passed to window event, for instance, pressed key
int Snake_window::handle(int event)
{
switch (event) {
case FL_FOCUS: case FL_UNFOCUS: // Focuses are skipped (required by system)
return 1;
case FL_KEYBOARD: { // Keys, pressed using keyboard
switch (Fl::event_key()) {
// Arrow-keys used to change snake's direction
case FL_Left: // Left-arrow
snake.set_direction(Snake::Direction::left);
cout << "Changed direction to the left (" << static_cast<int>(snake.direction()) << ")\n";
return 1;
case FL_Up: // Up-arrow
snake.set_direction(Snake::Direction::up);
cout << "Changed direction to the up (" << static_cast<int>(snake.direction()) << ")\n";
return 1;
case FL_Right: // Right-arrow
snake.set_direction(Snake::Direction::right);
cout << "Changed direction to the right (" << static_cast<int>(snake.direction()) << ")\n";
return 1;
case FL_Down: // Down-arrow
snake.set_direction(Snake::Direction::down);
cout << "Changed direction to the down (" << static_cast<int>(snake.direction()) << ")\n";
return 1;
}
}
}
return Window::handle(event); // Everything else is handled by base window
}
// Callback function for game_loop
void Snake_window::cb_game_loop(Address pw)
{
constexpr double delay = 0.25; // Delay of game's loop
reference_to<Snake_window>(pw).game_loop(); // Call of action function
Fl::repeat_timeout(delay, cb_game_loop, pw); // Execute delay of game's loop
}
// Callback function for pause
void Snake_window::cb_pause(Address, Address pw)
{
reference_to<Snake_window>(pw).pause();
reference_to<Snake_window>(pw).game();
}
// Callback function for new game
void Snake_window::cb_new_game(Address, Address pw)
{
reference_to<Snake_window>(pw).new_game();
reference_to<Snake_window>(pw).game();
}
// Callback function for quit
void Snake_window::cb_quit(Address, Address pw)
{
reference_to<Snake_window>(pw).quit();
reference_to<Snake_window>(pw).game();
}
// Callback function for game
void Snake_window::cb_game(Address, Address pw)
{
reference_to<Snake_window>(pw).game();
}
// Callback function for help
void Snake_window::cb_help(Address, Address pw)
{
reference_to<Snake_window>(pw).help();
}
// Starts game's loop
void Snake_window::start()
{
constexpr double delay = 1.0; // Delay before first timeout
Fl::add_timeout(delay, cb_game_loop, this); // Start game's loop and delay proccess
cout << "Started the game\n";
}
// Starts all proccesses of game's loop
void Snake_window::game_loop()
{
// Snake's bumping (obstacle is snake's body or field's borders)
if (snake.is_body_except_head(snake.body_head())) { // Snake's body as obstacle
cout << "Bumped into the snake's body\n";
// Pause after losed game
return Fl::add_timeout(0.0, [](Address pw) { cb_pause(nullptr, pw); }, this);;
}
if (!is_grid(field, snake.body_head())) { // Grid's border as obstacle
cout << "Bumped into the grid's border\n";
// Pause after losed game
return Fl::add_timeout(0.0, [](Address pw) { cb_pause(nullptr, pw); }, this);
}
// Snake's eating
if (snake.point(0) == fruit.point(0)) {
snake.grow_length();
put_score(); // Update score after eating
cout << "Ate the fruit; the length becomes equal to " << snake.length() << '\n';
// Randomly change location of fruit to everywhere, except snake's body
while (snake.is_body(fruit))
random_move(fruit, field.point(0), field.width() - fruit.width(), field.height() - fruit.height());
}
else snake.move_forward(); // Snake's moving
cout << "Moved to (" << snake.point(0).x << ", " << snake.point(0).y << ")\n";
redraw(); // Redraw window after made changes
}
// Determines either game is paused or not
bool Snake_window::is_pause()
{
return Fl::has_timeout(cb_game_loop, this) ? false : true;
}
// Pauses game if it's playing, or starts if it's already
// paused, that is, pause prevents snake's moves
void Snake_window::pause()
{
if (!is_pause()) {
Fl::remove_timeout(cb_game_loop, this); // Stop timeout
cout << "Paused the game\n";
}
else start(); // Start timeout
}
// Starts new game, that is, returns everything to initial state
void Snake_window::new_game()
{
if (!is_pause()) pause(); // Pause game
snake.shrink_length(current_score()); // Shrink length to default length
// Return graphics to default location
snake.set_direction(Snake::Direction::up);
snake.set_direction(Snake::Direction::right);
for (int i = 0; i < snake_sz; ++i)
snake.move_forward();
snake.move(-snake.point(0).x, -snake.point(0).y); // Top-left angle of window
snake.move(snake_xy.x, snake_xy.y);
fruit.move(-fruit.point(0).x, -fruit.point(0).y); // Top-left angle of window
fruit.move(fruit_xy.x, fruit_xy.y);
cout << "Started the new game; shrank the length to " << snake.length() << '\n';
put_score(); // Update score after shrinking
redraw(); // Redraw window after made changes
}
// Quits game, that is, closes window
void Snake_window::quit()
{
Window::hide(); // Hide window to close it
cout << "Quited the game\n";
}
// Hides game button and shows game menu, if game button is pressed,
// or shows game button and hides game menu, if game menu is pressed
void Snake_window::game()
{
// Hide game button and show game menu
if (game_button.visible()) { // Game button is pressed
game_button.hide();
game_menu.show();
help_button.move(game_menu.selection.size() * game_menu.width - help_button.width, 0);
cout << "Hid the game button and showed the game menu\n";
}
// Hide game menu and show game button
else { // Game menu is pressed
game_menu.hide();
game_button.show();
help_button.move(help_button.width - game_menu.selection.size() * game_menu.width, 0);
cout << "Hid the game menu and showed the game button\n";
}
}
// Shows help box if it's invisible, or hides it if it's visible
void Snake_window::help()
{
// Show help box
if (!help_box.visible()) { // Help box is invisible
if (!is_pause()) pause(); // Pause game
game_menu.selection[pause_ind].deactivate();
hide_graphics();
help_box.show();
cout << "Showed the help box\n";
}
// Hide help box
else { // Help box is visible
game_menu.selection[pause_ind].activate();
help_box.hide();
show_graphics();
cout << "Hid the help box\n";
}
}
// Determines current score
int Snake_window::current_score()
{
return snake.length() - snake_sz;
}
// Writes current score and max score into score boxes, if required
void Snake_window::put_score()
{
int score = current_score();
score_box.put(score); // Write current score
if (score > max_score_box.get_int()) { // New record
max_score_box.put(score); // Write max score
cout << "Updated the max score to " << score << '\n';
}
cout << "Updated the current score to " << score << '\n';
}
// Shows game's graphics, that is, makes field, snake, and fruit visible
void Snake_window::show_graphics()
{
// Modify color parameters of graphics
field.set_color(Color::black);
field.set_fill_color(Color::dark_green);
snake.set_color(Color::black);
snake.set_fill_color(Color::dark_yellow);
snake.head_set_fill_color(Color::yellow);
fruit.set_color(Color::black);
fruit.set_fill_color(Color::red);
cout << "Showed the graphics\n";
}
// Hides game's graphics, that is, makes field, snake, and fruit invisible
void Snake_window::hide_graphics()
{
// Modify color parameters of graphics
field.set_color(Color::invisible);
field.set_fill_color(Color::invisible);
snake.set_color(Color::invisible);
snake.set_fill_color(Color::invisible);
snake.head_set_fill_color(Color::invisible);
fruit.set_color(Color::invisible);
fruit.set_fill_color(Color::invisible);
cout << "Hid the graphics\n";
}
//------------------------------------------------------------------------------
} // End of Graph_lib namespace
//------------------------------------------------------------------------------
Game_graph.h
// Snake game's graphics
//------------------------------------------------------------------------------
#pragma once
#include "GraphicsLib/Graph.h"
//------------------------------------------------------------------------------
namespace Graph_lib {
//------------------------------------------------------------------------------
// Invariant: cell_w > 0, cell_h > 0, sz > 0
class Snake : public Shape {
public:
enum class Direction { // Possible directions of head
left, up, right, down
};
// Construction
Snake(Point xy, int cell_w, int cell_h, int sz);
// Drawing
void draw_lines() const override;
void move(int dx, int dy) override;
void move_forward();
void grow_length();
void shrink_length(int num);
// Modification of parameters
void set_color(Color c);
void set_fill_color(Color c);
void set_style(Line_style ls);
void set_direction(Direction d);
void head_set_fill_color(Color c);
// Access to parameters
const Rectangle& body_head() const;
Direction direction() const { return head; }
int length() const { return body.size(); }
bool is_body(const Rectangle& cell) const;
bool is_body_except_head(const Rectangle& cell) const;
private:
Vector_ref<Rectangle> body;
Direction head; // Direction of head
};
//------------------------------------------------------------------------------
// Helper function
void random_move(Rectangle& rect, Point xy, int w, int h);
//------------------------------------------------------------------------------
} // End of Graph_lib namespace
//------------------------------------------------------------------------------
Game_graph.cpp
// Snake game's graphics
//------------------------------------------------------------------------------
#include "Game_graph.h"
#include "RandomNumber/Generator.h"
//------------------------------------------------------------------------------
namespace Graph_lib {
//------------------------------------------------------------------------------
// Indexes of snake's body
constexpr int head_ind = 0;
// Constructs snake with top left-angle of its head at xy, of sz
// cells, and with size of each cell equal to cell_w * cell_h
Snake::Snake(Point xy, int cell_w, int cell_h, int sz)
: body{}, head{ Direction::right }
{
if (sz <= 0) // Error handling
throw invalid_argument("Bad Snake: non-positive length");
// Fill of body
for (int i = 0; i < sz; ++i) // Horizontal line
body.push_back(new Rectangle{ Point{ xy.x - i * cell_w, xy.y }, cell_w, cell_h });
add(xy); // Top-left angle of snake's head
}
// Draws snake and fills it with color if required
void Snake::draw_lines() const
{
// Draw each cell of body
for (int i = 0; i < body.size(); ++i)
body[i].draw();
}
// Moves snake by dx at x-coordinate and dy at y-coordinate
void Snake::move(int dx, int dy)
{
Shape::move(dx, dy);
// Move each cell of body
for (int i = 0; i < body.size(); ++i)
body[i].move(dx, dy);
}
// Moves snake forward, that is, moves each cell from tail to head
// to its next neighbor, and moves head one cell in its direction
void Snake::move_forward()
{
// Move each cell from tail to head to its next neighbour
for (int i = body.size() - 1; i > 0; --i) {
body[i].move(-body[i].point(0).x, -body[i].point(0).y); // Move to initial point
body[i].move(body[i - 1].point(0).x, body[i - 1].point(0).y); // Move to neigbhour's point
}
// Move head one cell in its direction
switch (head) {
case Direction::left: // Left-side
body[head_ind].move(-body[head_ind].width(), 0);
break;
case Direction::up: // Up-side
body[head_ind].move(0, -body[head_ind].height());
break;
case Direction::right: // Right-side
body[head_ind].move(body[head_ind].width(), 0);
break;
case Direction::down: // Down-side
body[head_ind].move(0, body[head_ind].height());
break;
}
set_point(0, body[head_ind].point(0)); // Update location of snake's head
}
// Grows snake in length, that is, adds one cell to its tail
void Snake::grow_length()
{
const Point tail = body[body.size() - 1].point(0); // Tail's coordinate
move_forward();
// Add new cell into body at previous tail's location
body.push_back(new Rectangle{ tail, body[head_ind].width(), body[head_ind].height() });
// Set same parameters for new tail as for all body
body[body.size() - 1].set_color(color());
body[body.size() - 1].set_fill_color(fill_color());
body[body.size() - 1].set_style(style());
}
// Shrinks snake in length, that is, removes num cells from its body, starting with tail
void Snake::shrink_length(int num)
{
if (num >= body.size()) // Error handling
throw invalid_argument("Bad Snake: can't shrink to non-positive length");
constexpr bool own = true; // Cells are owned by body
// Remove num cells from snake's body
for (int i = 0; i < num; ++i)
body.pop_back(own);
}
// Sets c as color of snake's lines
void Snake::set_color(Color c)
{
Shape::set_color(c);
// Set c as color of lines to each cell of body
for (int i = 0; i < body.size(); ++i)
body[i].set_color(c);
}
// Sets c as fill color of snake's body
void Snake::set_fill_color(Color c)
{
Shape::set_fill_color(c);
// Set c as fill color to each cell of body
for (int i = 0; i < body.size(); ++i)
body[i].set_fill_color(c);
}
// Sets c as fill color of snake's head
void Snake::head_set_fill_color(Color c)
{
if (body.begin() == body.end()) // Error handling
throw out_of_range("Bad Snake: can't set fill color to head of empty snake");
body[head_ind].set_fill_color(c);
}
// Sets ls as line style of snake's body
void Snake::set_style(Line_style ls)
{
Shape::set_style(ls);
// Set ls as line style to each cell of body
for (int i = 0; i < body.size(); ++i)
body[i].set_style(ls);
}
// Sets d as direction of snake's head
void Snake::set_direction(Direction d)
{
constexpr int opposite_diff = 2; // Module of opposite direction's difference
// Difference of directions
const int diff = abs(static_cast<int>(head) - static_cast<int>(d));
if (diff != opposite_diff) // Set direction if it's not opposite
head = d;
}
// Gets snake's head
const Rectangle& Snake::body_head() const
{
if (body.cbegin() == body.cend()) // Error handling
throw out_of_range("Bad Snake: can't get head of empty snake");
return body[head_ind];
}
// Determines either cell is one of snake's body's cells
bool Snake::is_body(const Rectangle& cell) const
{
// Search for cell in snake's body, located same as cell, and compare parameters
return find_if(body.cbegin(), body.cend(), [&cell](const Rectangle* rect)
{ return rect->point(0) == cell.point(0); }) != body.cend()
&& body[0].width() == cell.width() && body[0].height() == cell.height();
}
// Determines either cell is one of snake's body's cells, except its head
bool Snake::is_body_except_head(const Rectangle& cell) const
{
// Search for cell in snake's body, located same as cell, except snake's head, and compare parameters
return body.cbegin() != body.cend() ? find_if(next(body.cbegin()), body.cend(),
[&cell](const Rectangle* rect) { return rect->point(0) == cell.point(0); }) != body.cend()
&& body[0].width() == cell.width() && body[0].height() == cell.height() : false;
}
//------------------------------------------------------------------------------
// Moves rect randomly in range [xy.x; xy.x + w] for x-coordinate and [xy.y; xy.y + h] for
// y-coordinate, with xy as original point, w as width of range and h as height of range
void random_move(Rectangle& rect, Point xy, int w, int h)
{
if (w < 0 || h < 0) // Error handling
throw invalid_argument("Bad random_move: invalid range for coordinates");
// Move to original location, that is, xy
rect.move(-(rect.point(0).x - xy.x), -(rect.point(0).y - xy.y));
rect.move(rect.width() * randint(0, w / rect.width()), // Random x-coordinate
rect.height() * randint(0, h / rect.height())); // Random y-coordinate
}
//------------------------------------------------------------------------------
} // End of Graph_lib namespace
//------------------------------------------------------------------------------
Question
How can I improve my code in the future? Any tips are appreciated, but especially hope to see your thoughts on the structuring of the code, its flexibility, and readability.
Credits
Thanks for your time and efforts.
-
\$\begingroup\$ Great game! i was struggling with fltk callbacks without a button and this helped me out so thank you! \$\endgroup\$Roscopikotrain– Roscopikotrain2020年11月12日 12:51:15 +00:00Commented Nov 12, 2020 at 12:51
1 Answer 1
Don't catch
errors you can't handle
You should catch exceptions if you can do something useful with them. However, just printing an error message and then quitting immediately is not useful. If you don't catch an exception, that is what will happen by default anyway.
Don't specify the window position
You should let the window manager decide the initial position of your window. It knows better where the user would like the window, and can use heuristics like where the mouse cursor is currently, where there is still unused space on the screen, and so on.
Make game menu buttons member variables
Why are the three buttons added to game_menu
created with new
, when other buttons are just member variables of Snake_window
? Looking at your code it seems Window::attach()
also has an overload that takes a reference to a Button
, so that should just work, and will be more consistent.
Move the help text out of the constructor, use a raw string literal
The constructor of Snake_window()
contains mostly logic for adding widgets to the window, but there's a huge blob of help text in the middle of it. It might make sense to move the text itself out of this function, and put it in a static variable. You can also use a raw string literal so you don't have to write quote characters and escape newlines anymore:
static const char *help_text =
R"( SNAKE GAME
Snake is a video game concept where the player maneuvers a line
that grows in length, with the line itself being a primary obstacle.
The concept originated in the 1976 arcade game Blockade.
...
ADDITIONAL NOTES
Good luck on the game, try to eat as much as you can!
)";
...
Snake_window::Snake_window(...)
: ...
{
...
help_box.put(help_text);
...
}
Remove debug statements
In Snake_window::handle()
you are printing something everytime the snake changes direction. It looks like you used this for debugging? You should remove this in production code. There are other examples throughout your code that prints to cout
that should be removed.
Give a name to reference_to<Snake_window>(pw)
It's a bit unfortunate that FLTK doesn't support callbacks to non-static member functions. So now you have to write reference_to<Snake_window>(pw)
to get the class instance. But it's a bit long and cryptic. Consider giving it a name, like self
, which should be reasonably self-explanatory:
void Snake_window::cb_pause(Address, Address pw)
{
auto self = reference_to<Snake_window>(pw);
self.pause();
self.game();
}
The body of the snake
This is where it went horribly wrong. Let's look at how the body is declared:
Vector_ref<Rectangle> body;
I see that Vector_ref
is kind of a wrapper around std::vector<T *>
. But why do you need to store the Rectangle
s by pointer or reference? Looking at your GitHub repository, it seems Rectangle
derives from Shape
, but you deleted the copy constructor and copy assignment operator. I don't see a reason for that. If you want to prevent someone from copying a bare Shape
, it is better to make the copy operations protected
, like so:
class Shape {
...
protected:
Shape(const Shape &other) = default;
Shape &operator=(const Shape &other) = default;
...
};
Once you have that, you should be able to create a vector of Rectangle
s like so:
std::vector<Rectangle> body;
But there are other issues, which I'll discuss below:
Use a std::deque<>
to store the body positions
You are using a vector, and whenever you remove the tail piece and add a new head piece, you have to shift all the positions in the body. That's quite an expensive operation. Your own for
-loop is very inefficient, because you move each point twice. If you use a std::vector
, you could use pop_back()
and emplace()
like so:
void Snake::move_forward() {
body.pop_back();
body.emplace(body.begin(), { /* new head constructor arguments */ });
}
But then std::vector
will just shift all element for you. What you ideally want is to keep all the body positions as they are, and then remove the tail and add a new head in O(1) time. That can be done by using either a std::list
, but if you want something that works more like a std::vector
, a std::deque
is ideal. Your code would then look like:
void Snake::move_forward() {
body.pop_back();
body.emplace_front({ /* new head constructor arguments */ });
}
And again:
Avoid moving points unnecessarily
I see this pattern being used in several places:
fruit.move(-fruit.point(0).x, -fruit.point(0).y); // Top-left angle of window
fruit.move(fruit_xy.x, fruit_xy.y);
Basically what you want is setting the fruit position to fruit_xy
. Why not create a member function of Rectangle
that allows direct setting of the desired position, so you can write the following:
fruit.set_xy(fruit_xy);
Simplifying growing the body
Instead of having a separate function to grow the body, which first moves the snake (which removes its old tail), and then add the old tail back, consider changing Snake::move_forward()
to optionally not remove the tail. I would do this by adding a member variable to Snake
that indicates how many elements the body needs to grow with:
class Snake {
...
public:
void grow(size_t length) { to_grow += length; }
private:
size_t to_grow;
};
And then in Snake::move_forward()
, do something like this:
void Snake::move_forward() {
if (to_grow)
to_grow--;
else
body.pop_back();
body.emplace_front({ /* new head constructor arguments */ });
}
Use assert()
to check for things that shouldn't be possible
I see several member functions of Snake
that check whether body.begin() == body.end()
. That's only true if the length of the body is zero. But the constructor of Snake
already throws an error if you specify a length that is less than 1. So this check if in principle unnecessary. But, it's good practice to encode your assumptions using assert()
statements, so these assumptions can be checked in debug builds, but won't slow down release builds, like so:
#include <cassert>
...
const Rectangle &Snake::body_head() const {
assert(head_ind >= 0 && head_ind < body.size());
return body[head_ind];
}
Although it would be simpler to either use body.front()
to get the head element, and write:
const Rectangle &Snake::body_head() const {
assert(!body.empty());
return body.front();
}
Although personally, in this particular case, if it is clear that the Snake always has a non-zero body length, I wouldn't write those assert()
statements at all; they just clutter the code, and tools like Valgrind can catch out-of-bounds errors as well.
Regardless, I would use an assert in the constructor of Snake
to check the length parameter instead of throwing an exception.
Asserts should generally be used to check for assumptions about your own code. But use if (...)
plus some kind of error reporting (like throwing an exception) when the condition is something that depends on user input.
-
1\$\begingroup\$ Thank you a lot for your deep analysis! I'll take your notes into account, and will make changes in the code to make it better. \$\endgroup\$WumpusHunter– WumpusHunter2020年08月22日 04:22:25 +00:00Commented Aug 22, 2020 at 4:22