描述
我用C++和FLTK编写了蛇游戏。为了简化FLTK的使用,使用了Bjarne编写的构建库。Bellow定位了我编写的代码的主要部分,可以在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;
}
//------------------------------------------------------------------------------------游戏_窗口.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
//------------------------------------------------------------------------------Gamewindow.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(snake.direction()) << ")\n";
return 1;
case FL_Up: // Up-arrow
snake.set_direction(Snake::Direction::up);
cout << "Changed direction to the up (" << static_cast(snake.direction()) << ")\n";
return 1;
case FL_Right: // Right-arrow
snake.set_direction(Snake::Direction::right);
cout << "Changed direction to the right (" << static_cast(snake.direction()) << ")\n";
return 1;
case FL_Down: // Down-arrow
snake.set_direction(Snake::Direction::down);
cout << "Changed direction to the down (" << static_cast(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(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(pw).pause();
reference_to(pw).game();
}
// Callback function for new game
void Snake_window::cb_new_game(Address, Address pw)
{
reference_to(pw).new_game();
reference_to(pw).game();
}
// Callback function for quit
void Snake_window::cb_quit(Address, Address pw)
{
reference_to(pw).quit();
reference_to(pw).game();
}
// Callback function for game
void Snake_window::cb_game(Address, Address pw)
{
reference_to(pw).game();
}
// Callback function for help
void Snake_window::cb_help(Address, Address pw)
{
reference_to(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
//------------------------------------------------------------------------------游戏_图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 body;
Direction head; // Direction of head
};
//------------------------------------------------------------------------------
// Helper function
void random_move(Rectangle& rect, Point xy, int w, int h);
//------------------------------------------------------------------------------
} // End of Graph_lib namespace
//------------------------------------------------------------------------------游戏_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(head) - static_cast(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
//------------------------------------------------------------------------------问题
今后如何改进我的代码?任何技巧都会受到赞赏,但特别希望看到您对代码的结构、灵活性和可读性的看法。
学分
谢谢你的时间和努力。
发布于 2020-08-21 21:16:08
https://codereview.stackexchange.com/questions/248237
复制相似问题