C ++ 및 FLTK를 사용한 스네이크 게임

Aug 21 2020

기술

C ++ 및 FLTK를 사용하여 스네이크 게임을 작성했습니다. FLTK 사용을 단순화하기 위해 Bjarne Stroustrup이 작성한 내장 라이브러리가 사용되었습니다. 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;
}

//------------------------------------------------------------------------------------

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

//------------------------------------------------------------------------------

질문

앞으로 코드를 개선하려면 어떻게해야합니까? 어떤 팁이라도 감사하지만 특히 코드 구조, 유연성 및 가독성에 대한 귀하의 생각을 확인하시기 바랍니다.

크레딧

시간과 노력에 감사드립니다.

답변

4 G.Sliepen Aug 22 2020 at 04:16

catch처리 할 수 없는 오류는하지 마십시오

유용한 일을 할 수 있다면 예외를 포착해야합니다. 그러나 오류 메시지를 인쇄 한 다음 즉시 종료하는 것은 유용하지 않습니다. 예외를 포착하지 않으면 어쨌든 기본적으로 발생합니다.

창 위치를 지정하지 마십시오

창 관리자가 창의 초기 위치를 결정하도록해야합니다. 사용자가 창을 원하는 위치를 더 잘 알고 있으며 현재 마우스 커서가있는 위치, 화면에 아직 사용되지 않은 공간이있는 위치 등과 같은 휴리스틱 스를 사용할 수 있습니다.

게임 메뉴 버튼 멤버 변수 만들기

다른 버튼이의 멤버 변수 일 때 세 개의 버튼이으로 game_menu생성 된 이유는 무엇 입니까? 코드를 살펴보면에 대한 참조를받는 오버로드가있는 것 같으므로 작동해야하며 더 일관성이 있습니다.newSnake_windowWindow::attach()Button

생성자에서 도움말 텍스트를 이동하고 원시 문자열 리터럴을 사용합니다.

의 생성자는 Snake_window()대부분 창에 위젯을 추가하기위한 논리를 포함하지만 그 중간에 거대한 도움말 텍스트 덩어리가 있습니다. 이 함수에서 텍스트 자체를 이동하여 정적 변수에 넣는 것이 합리적 일 수 있습니다. 또한 원시 문자열 리터럴을 사용할 수 있으므로 더 이상 따옴표 문자를 작성하고 줄 바꿈을 이스케이프 할 필요가 없습니다.

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

디버그 문 제거

에서 Snake_window::handle()당신이 뭔가 매번를 인쇄하는 뱀 방향을 변경합니다. 이것을 디버깅에 사용한 것 같습니까? 프로덕션 코드에서이를 제거해야합니다. cout제거해야하는 코드를 인쇄하는 다른 예제 가 있습니다.

이름을 reference_to<Snake_window>(pw)

FLTK가 비 정적 멤버 함수에 대한 콜백을 지원하지 않는 것은 조금 안타깝습니다. 이제 reference_to<Snake_window>(pw)클래스 인스턴스를 얻기 위해 작성 해야합니다. 그러나 그것은 약간 길고 비밀 스럽습니다. self합리적으로 자명 한 이름을 지정 하는 것이 좋습니다.

void Snake_window::cb_pause(Address, Address pw)
{
    auto self = reference_to<Snake_window>(pw);
    self.pause();
    self.game();
}

뱀의 몸

이것은 끔찍하게 잘못된 곳입니다. 본문이 어떻게 선언되는지 살펴 보겠습니다.

Vector_ref<Rectangle> body;

나는 그것이 Vector_ref일종의 래퍼 라는 것을 봅니다 std::vector<T *>. 하지만 왜 Rectangle포인터 또는 참조로 s 를 저장해야 합니까? GitHub 저장소를 보면에서 Rectangle파생 된 것처럼 보이지만 Shape복사 생성자와 복사 할당 연산자를 삭제했습니다. 그 이유를 모르겠습니다. 누군가가 bare를 복사하지 못하도록 Shape하려면 다음 protected과 같이 복사 작업을 수행하는 것이 좋습니다 .

class Shape {
    ...
protected:
    Shape(const Shape &other) = default;
    Shape &operator=(const Shape &other) = default;
    ...
};

일단 가지고 있으면 다음 Rectangle과 같이 s 벡터를 만들 수 있습니다 .

std::vector<Rectangle> body;

그러나 아래에서 논의 할 다른 문제가 있습니다.

a std::deque<>를 사용하여 신체 위치 저장

벡터를 사용하고 있으며, 꼬리 부분을 제거하고 새 머리 부분을 추가 할 때마다 몸의 모든 위치를 이동해야합니다. 꽤 비싼 작업입니다. 자신의 for루프는 각 지점을 두 번 이동하기 때문에 매우 비효율적입니다. 를 사용하는 경우 다음 과 같이 std::vector사용할 수 있습니다 .pop_back()emplace()

void Snake::move_forward() {
    body.pop_back();
    body.emplace(body.begin(), { /* new head constructor arguments */ });
}

그러나 그러면 std::vector모든 요소가 변경됩니다. 이상적으로 원하는 것은 모든 신체 위치를 그대로 유지 한 다음 꼬리를 제거하고 O (1) 시간에 새 머리를 추가하는 것입니다. a 중 하나를 사용하여 수행 할 수 std::list있지만 a와 같이 작동하는 것을 원한다면 std::vectora std::deque가 이상적입니다. 코드는 다음과 같습니다.

void Snake::move_forward() {
    body.pop_back();
    body.emplace_front({ /* new head constructor arguments */ });
}

다시 한번:

불필요하게 포인트를 이동하지 마십시오.

이 패턴이 여러 곳에서 사용되는 것을 봅니다.

fruit.move(-fruit.point(0).x, -fruit.point(0).y); // Top-left angle of window
fruit.move(fruit_xy.x, fruit_xy.y);

기본적으로 원하는 것은 과일 위치를로 설정하는 것 fruit_xy입니다. Rectangle원하는 위치를 직접 설정할 수 있는 멤버 함수를 만들면 다음과 같이 작성할 수 있습니다.

fruit.set_xy(fruit_xy);

신체 성장 단순화

먼저 뱀을 움직 인 다음 (오래된 꼬리를 제거하는) 몸을 키우는 별도의 기능을 갖는 대신, 이전 꼬리를 다시 추가하는 대신 Snake::move_forward()선택적으로 꼬리를 제거하지 않도록 변경하는 것이 좋습니다. Snake본문이 성장해야하는 요소 수 를 나타내는 멤버 변수를에 추가하여이를 수행합니다 .

class Snake {
    ...
public:
    void grow(size_t length) { to_grow += length; }
private:
    size_t to_grow;
};

그런 다음에서 다음 Snake::move_forward()과 같이하십시오.

void Snake::move_forward() {
    if (to_grow)
        to_grow--;
    else
        body.pop_back();

    body.emplace_front({ /* new head constructor arguments */ });
}

assert()불가능한 일을 확인하는 데 사용

Snake그 여부를 확인하는 몇 가지 멤버 함수를 봅니다 body.begin() == body.end(). 몸의 길이가 0 인 경우에만 해당됩니다. 그러나 생성자는 Snake1보다 작은 길이를 지정하면 이미 오류가 발생합니다. 따라서 원칙적으로 불필요한 지 확인합니다. 그러나 assert()문 을 사용하여 가정을 인코딩하는 것이 좋습니다 . 그래야 이러한 가정을 디버그 빌드에서 확인할 수 있지만 다음과 같이 릴리스 빌드 속도가 느려지지는 않습니다.

#include <cassert>
...
const Rectangle &Snake::body_head() const {
    assert(head_ind >= 0 && head_ind < body.size());
    return body[head_ind];
}

body.front()헤드 요소를 가져 오는 데 사용 하는 것이 더 간단하지만 다음 과 같이 작성합니다.

const Rectangle &Snake::body_head() const {
    assert(!body.empty());
    return body.front();
}

개인적으로,이 특별한 경우에 뱀의 몸길이가 항상 0이 아닌 것이 분명하다면, 나는 그 assert()진술을 전혀 쓰지 않을 것입니다 . 코드를 복잡하게 만들고 Valgrind 와 같은 도구는 경계를 벗어난 오류도 포착 할 수 있습니다.

그럼에도 불구 Snake하고 예외를 던지는 대신 길이 매개 변수를 확인하기 위해 생성자에서 assert를 사용합니다 .

일반적으로 Assert는 자신의 코드에 대한 가정을 확인하는 데 사용되어야합니다. 그러나 if (...)조건이 사용자 입력에 따라 달라지는 경우에는 plus 일부 종류의 오류보고 (예 : 예외 발생)를 사용하십시오.