Привет, я делаю игру 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 ответ
Поздравляем с созданием рабочей игры! Как вы уже подозревали, действительно есть кое-что, что можно улучшить:
Переосмыслите структуру своего кода
У вас есть представление об организации кода в разных файлах и использовании 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 гарантируют, что при первом щелчке по доске под мышью не окажется бомбы. Причина в том, что не имеет смысла ставить флаг в качестве первого хода, и это будет воспринято как несправедливость, поскольку первый сделанный вами ход может привести к вашему проигрышу без какой-либо ошибки со стороны игрока.
Есть несколько способов реализовать это; если первый щелчок приходится на бомбу, вы можете удалить бомбу или разместить ее в другом месте и исправить количество соседей. Кроме того, вы можете отложить размещение бомб до тех пор, пока игрок не щелкнет где-нибудь.