Сапер, использующий SFML с C++

Привет, я делаю игру Minesweeper, используя SFML 2.0.

Я только новичок в C ++ и начал использовать фреймворк уже 2 недели, поэтому можно с уверенностью сказать, что я, возможно, сделал много ошибок, особенно когда речь идет о типах данных или реализациях алгоритмов.

Прежде чем я начну, мне нужно объяснить эти две переменные настройки, чтобы избежать путаницы:
CELLS_OFFSET = где будет генерироваться сетка позиций ячеек. (можно использовать для центрирования ячеек)
CELLS_SIZE = Какова длина строки и столбца. (не значение масштаба спрайтов ячеек)

Я был бы очень признателен, если бы вы уделили время и просмотрели эти (своего рода беспорядочные) коды и сообщили о любых ошибках или неправильных методах^^

Настройки.hpp

const unsigned short CELLS_SIZE = 14;
const unsigned short CELLS_BOMBS = 24;

int8_t CELLS_DATA[CELLS_SIZE][CELLS_SIZE];  // The hidden cell values
int8_t CELLS_SDATA[CELLS_SIZE][CELLS_SIZE]; // The cells on top of the hidden cell values

const sf::Vector2i CELLS_OFFSET = sf::Vector2i(202, 16); // The position where the grid of cells will generate

Cell.hpp и Cell.cpp

// Cell.hpp
#include <SFML/Graphics.hpp>

#ifndef CELL_HPP
#define CELL_HPP

class Cell
{
    private:
        sf::Sprite sprite;
        sf::Texture texture;

    public:
        Cell();

        void change(int8_t index);
        void draw(sf::RenderWindow &window, sf::Vector2i position, sf::Vector2i offset);
};

#endif // CELL_HPP
// Cell.cpp
#include "Cell.hpp"

Cell::Cell()
{
    this->texture.loadFromFile("Assets/Cells.png");
    this->sprite.setTextureRect(sf::IntRect(0, 0, 16, 16));
    this->sprite.setTexture(texture);
}

void Cell::change(int8_t index)
{
    this->sprite.setTextureRect(sf::IntRect(16 * index, 0, 16, 16));
}

void Cell::draw(sf::RenderWindow &window, sf::Vector2i position, sf::Vector2i offset)
{
    this->sprite.setPosition(position.x * 32 + offset.x, position.y * 32 + offset.y);
    this->sprite.setScale(2, 2);
    window.draw(this->sprite);
}

Main.cpp

#include <SFML/Graphics.hpp>

#include "Cell.hpp"
#include "Settings.hpp"

int main()
{
    sf::RenderWindow window(sf::VideoMode(852, 480), "Minesweeper", sf::Style::Titlebar | sf::Style::Close);
    window.setVerticalSyncEnabled(true);

    // Define the cell class and randomly seed the bombs
    Cell cell; srand(time(0));

    // Fill Bomb Cells
    for (short x = 0; x < CELLS_BOMBS; x++)
    {
        int8_t &cell_location = CELLS_DATA[rand() % CELLS_SIZE][rand() % CELLS_SIZE];

        if (cell_location == 9)
            x--;
        else
            cell_location = 9;
    }

    // Fill Number Cells
    for (short x = 0; x < CELLS_SIZE; x++) for (short y = 0; y < CELLS_SIZE; y++)
    {
        int8_t &cell_location = CELLS_DATA[x][y];
        if (cell_location == 9) continue; // Skip if this cell is a bomb

        for (int8_t rx = -1; rx < 2; rx++) for (int8_t ry = -1; ry < 2; ry++) {
            // Skip if this cell is out of bounds
            if (x + rx > CELLS_SIZE - 1 || y + ry > CELLS_SIZE - 1 || x + rx < 0 || y + ry < 0 || (rx == 0 && ry == 0)) continue;

            // Increases the number if the neighbor cell has a bomb
            if (CELLS_DATA[x + rx][y + ry] == 9) cell_location++;
        }
    }

    // Game Loop
    while (window.isOpen())
    {
        sf::Vector2i cell_position = sf::Vector2i(std::floor((sf::Mouse::getPosition(window).x - CELLS_OFFSET.x) / 32.0), std::floor((sf::Mouse::getPosition(window).y - CELLS_OFFSET.y) / 32.0)); // I would appreciate if someone optimize this line
        bool cell_bounds = cell_position.x >= 0 && cell_position.x < CELLS_SIZE && cell_position.y >= 0 && cell_position.y < CELLS_SIZE; // Used to check if the cursor position is inside the grid of cells

        int8_t &cell_location = CELLS_DATA[cell_position.x][cell_position.y];
        int8_t &cell_slocation = CELLS_SDATA[cell_position.x][cell_position.y];

        sf::Event event; while (window.pollEvent(event))
        {
            // Cells Input
            if (event.type == sf::Event::MouseButtonPressed && cell_bounds) switch(event.mouseButton.button)
            {
                case sf::Mouse::Left: // Cell Opening
                    if (cell_slocation != 0) continue;
                    cell_slocation = -1;

                    // Flood Fill (BFS)
                    if (cell_location != 0) continue;
                    {
                        std::vector<sf::Vector2i> queue = {cell_position};

                        while (true)
                        {
                            // Get the first queue vector2 for checking it's neighbors
                            sf::Vector2i queue_first_position = sf::Vector2i(queue.at(0).x, queue.at(0).y);

                            // Loop through neigboring cells
                            for (int8_t rx = -1; rx < 2; rx++) for (int8_t ry = -1; ry < 2; ry++) {
                                // The cell must not be on out of bounds
                                if (queue_first_position.x + rx > CELLS_SIZE - 1 || queue_first_position.y + ry > CELLS_SIZE - 1 || queue_first_position.x + rx < 0 || queue_first_position.y + ry < 0 || (rx == 0 && ry == 0)) continue;

                                int8_t &neighbor_location = CELLS_DATA[queue_first_position.x + rx][queue_first_position.y + ry];
                                int8_t &neighbor_slocation = CELLS_SDATA[queue_first_position.x + rx][queue_first_position.y + ry];

                                // The cell must be already opened, and it is not a bomb
                                if (neighbor_slocation == -1 || neighbor_location == 9) continue;

                                // Open the cell
                                neighbor_slocation = -1;

                                // The cell must empty
                                if (neighbor_location != 0) continue;

                                // Add the cell's vector2 to queue
                                queue.push_back(sf::Vector2i(queue_first_position.x + rx, queue_first_position.y + ry));
                            }

                            // Remove the first element in the queue for checking if done filling
                            queue.erase(queue.begin());

                            if (queue.size() <= 0) break;
                        }
                    }

                    break;

                case sf::Mouse::Right: // Cell Flagging
                    if (cell_slocation == 1)
                        cell_slocation = 0;
                    else if (cell_slocation == 0)
                        cell_slocation = 1;
                    break;
            }

            if (event.type == sf::Event::Closed) window.close();
        }

        // Cells Render
        for (short x = 0; x < CELLS_SIZE; x++) for (short y = 0; y < CELLS_SIZE; y++)
        {
            switch(CELLS_SDATA[x][y])
            {
                case 0:  cell.change(10); break;        // Unopened Cell
                case 1:  cell.change(11); break;        // Flagged Cell
                default: cell.change(CELLS_DATA[x][y]); // Numbered Cell
            }

            cell.draw(window, sf::Vector2i(x, y), CELLS_OFFSET);
        }

        // Cells Highlight
        if (cell_bounds && ((cell_slocation == -1 && cell_location != 0) || (cell_slocation != -1)))
        {
            sf::RectangleShape highlight(sf::Vector2f(32, 32));
            highlight.setFillColor(sf::Color(255, 255, 255, 30));
            highlight.setPosition(cell_position.x * 32 + CELLS_OFFSET.x, cell_position.y * 32 + CELLS_OFFSET.y);
            window.draw(highlight);
        }

        window.display();
        window.clear();
    }

    return EXIT_SUCCESS;
}

Что касается кода, я продолжал использовать этот тип данных int8_t для хранения значений ячеек. Интересно, это хорошая практика или нет?

Предварительный просмотр
введите описание изображения здесь

1 ответ
1

Поздравляем с созданием рабочей игры! Как вы уже подозревали, действительно есть кое-что, что можно улучшить:

Переосмыслите структуру своего кода

У вас есть представление об организации кода в разных файлах и использовании classэ. Тем не менее, подавляющее большинство кода находится в main(). Вы действительно должны попытаться добавить больше структуры в свой код. Как правило, старайтесь не делать функции длиннее 20 строк кода и разделять их, если они это делают. Код и функциональность, которые принадлежат друг другу, должны быть помещены в отдельный класс. Вы сделали это для Cellно вы могли бы также создать class Board который представляет всю доску.

Другой трюк при написании функции состоит в том, чтобы написать несколько строк, описывающих на высоком уровне то, что делает функция, а затем создать дополнительные функции, реализующие детали низкого уровня, которые, в свою очередь, при необходимости вызывают функции даже более низкого уровня. Рассмотреть возможность main(): это должно выглядеть так:

int main()
{
    // 1. Initialize
    // 2. Run game loop
    // 3. Cleanup
}

Теперь превратите это в реальную функцию и для каждого шага используйте только несколько строк кода или делегируйте функции (члену), если это будет больше. Так, например:

int main()
{
    // 1. Initialize
    sf::RenderWindow window(sf::VideoMode(852, 480), "Minesweeper", sf::Style::Titlebar | sf::Style::Close);
    window.setVerticalSyncEnabled(true);
    Board board;

    // 2. Run game loop
    while (window.isOpen())
    {
         handleEvents(window, board);
         renderGame(window, board);
    }

    // 3. Cleanup
    return EXIT_SUCCESS;
}

Чтобы приведенный выше код работал, class Board должен быть создан, который заполняет ячейки бомбы и числа в своем конструкторе, и функция handleEvents() должен быть создан, который считывает события окна и соответствующим образом обновляет доску. Эта функция может выглядеть так:

void handleEvents(sf::RenderWindow &window, Board &board)
{
    sf::Event event;

    while (window.pollEvent(event))
    {
        switch(event.type)
        {
        case sf::MouseButtonPressed:
            handleMouseButtonPressed(window, board, event);
            break;

        case sf::Event::Closed:
            window.close();
        }
    }
}

В свою очередь, функция handleMouseButtonPressed() должен быть создан, который проверяет, какая кнопка мыши нажата, смотрит на координаты (которые также являются частью event.mouseButtonне надо звонить sf::Mouse::getPosition()) и обновляет доску.

Обратите внимание, что эти небольшие функции легко читать и понимать. У них есть имена, которые описывают то, что они делают, поэтому меньше нужно писать комментарии, чтобы объяснить, что делает ваш код.

Избегайте длинных строк кода

Некоторые из ваших строк очень длинные, в частности та, которая вычисляет, какая ячейка находится под указателем мыши. Попробуйте уменьшить длину строк, добавив разрывы строк, разбив один оператор на несколько операторов или переместив сложные части в отдельные функции. Например:

sf::Vector2i get_cell_position(int x, int y)
{
    x = (x - CELLS_OFFSET.x) / 32;
    y = (y - CELLS_OFFSET.y) / 32;
    return {x, y};
}
...
sf::Vector2i cell_position = get_cell_position(event.mouseButton.x, event.mouseButton.y);

Кроме того, некоторые строки кода содержат несколько операторов, например вложенные for-петли. Общее правило в руководства по стилю кодирования заключается в том, чтобы никогда не иметь более одного оператора в одной строке. Это делает строки короче, но также позволяет избежать неожиданностей, если в строке есть другой оператор, но он не виден, потому что он был слишком далеко вправо.

Максимально используйте библиотеки, которые вы используете

Вышеупомянутое может быть еще более упрощено, если вы будете больше использовать sf::Vector2i: последний также позволяет вам выполнять простые арифметические действия, поэтому вы можете написать:

sf::Vector2i get_cell_position(sf::Vector2i mouse_position)
{
    return (mouse_position - CELLS_OFFSET) / 32;
}
...
sf::Vector2i mouse_position = {event.mouseButton.x, event.mouseButton.y};
sf::Vector2i cell_position = get_cell_position(mouse_position);

Избегать жесткое кодирование а также магические числа

В Settings.hpp вы постарались дать имена некоторым константам, таким как размер доски и количество бомб. Однако есть много констант, имена которых не указаны в вашем коде, например размер ячейки (32), значение, указывающее, что ячейка является бомбой (9) и т. д. Попробуйте найти все эти числа и составить для них константы. Преимущество заключается в том, что кому-то другому, читающему код, будет легче узнать, что представляет собой число, а также позволяет вам изменить константу в одном месте, не просматривая весь код для поиска и замены этих констант.

Даже если вы назовете все константы, подумайте, должны ли некоторые вещи быть постоянными. Это может иметь смысл для размера ячейки, поскольку вы хотите, чтобы она соответствовала размеру изображения, которое у вас есть для нее, но как насчет размера окна, размера доски или количества бомб?

Попробуйте посмотреть, сможете ли вы сделать больше вещей вариативными. Для этого могут потребоваться более обширные изменения в вашем коде; например CELLS_DATA[][] а также CELLS_SDATA[][] массивы больше не компилируются, если CELLS_SIZE не является константой. Возможно, вам придется использовать std::vector или что-то подобное для хранения ячеек.

Создать struct для состояния клетки

У тебя есть class Cell который обрабатывает отрисовку ячейки, но есть также состояние, в котором находится ячейка. В настоящее время оно хранится в двух массивах как int8_tс. Однако рассмотрите возможность создания struct который содержит всю информацию о состоянии одной ячейки. Также рассмотрите возможность создания enum который кодирует, является ли ячейка скрытой, видимой или помеченной:

enum class CellState {
    HIDDEN,
    FLAGGED,
    VISIBLE,
};

struct CellData
{
    int8_t neighbours;
    CellState state;
};

Тогда вам нужен только один массив:

CellData cells[CELLS_SIZE][CELLS_SIZE];

Именование вещей

Некоторые вещи в вашем коде имеют названия, которые сбивают с толку. Например, CELLS_SIZE звучит как размер клетки, но на самом деле это размер доски. Так BOARD_SIZE было бы лучше имя. CELLS_BOMBS следует переименовать в NUMBER_OF_BOMBS, N_BOMBS или же BOMB_COUNT.

Добавляя бомбы на доску, вы используете x как счетчик циклов. Однако, x обычно ассоциируется с координатой x. я хотел бы использовать i вместо этого это хорошо известное имя общего индекса цикла.

Убедитесь, что имена ясны, лаконичны и недвусмысленны.

Первый щелчок должен быть безопасным

Многие версии Minesweeper гарантируют, что при первом щелчке по доске под мышью не окажется бомбы. Причина в том, что не имеет смысла ставить флаг в качестве первого хода, и это будет воспринято как несправедливость, поскольку первый сделанный вами ход может привести к вашему проигрышу без какой-либо ошибки со стороны игрока.

Есть несколько способов реализовать это; если первый щелчок приходится на бомбу, вы можете удалить бомбу или разместить ее в другом месте и исправить количество соседей. Кроме того, вы можете отложить размещение бомб до тех пор, пока игрок не щелкнет где-нибудь.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *