Игра «Сапер» (англ. «Minesweeper») — это простая, но в то же время очень интересная игра-головоломка. В начале игры открывается поле, разделенное на ровные клетки, на котором спрятаны мины. Когда игрок кликает мышкой по произвольной клетке на поле, то в этой клетке появляется цифра, показывающая сколько мин спрятано по соседству. Анализируя эти цифры, можно понять, где спрятаны следующие мины. Затем на клетке (в которой игрок предполагает, что спрятана мина) ставится флажок для наглядного обозначения мины.
Таким образом, задача игрока — найти все мины, спрятанные на игровом поле, и при этом постараться не подорваться на них. А наша задача — разработать игру «Сапер» средствами графической библиотеки SFML и языка программирования С++.
Модель игрового поля
Начнем с графики. Для обозначения элементов игрового поля нам потребуются картинки закрытой клетки, открытой пустой клетки, флажка, мины и цифр от 1 до 8. Т.к. отдельными изображениями пользоваться не очень удобно, то будет лучше, если мы соединим их все в единую текстуру, а уже из нее по мере необходимости вырежем нужный элемент:
Размеры одного квадратного элемента текстуры составляют 32×32 пикселя. Размеры игрового поля — 10×10 квадратов.
Теперь перейдем непосредственно к написанию кода. Как вы уже наверняка знаете, минимальный каркас SFML-приложения имеет примерно следующий вид:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <SFML/Graphics.hpp> #include <time.h> using namespace sf; int main() { RenderWindow app(VideoMode(400, 400), "Minesweeper!"); while (app.isOpen()) { Event e; while (app.pollEvent(e)) { if (e.type == Event::Closed) app.close(); } } return 0; } |
Модель игрового поля составляют 2 двумерных массива:
int gridLogic[12][12]
— представляет логическую часть игрового поля;
int gridView[12][12]
— отображает графическую составляющую игрового поля.
В коде это выглядит следующим образом:
1 2 3 4 5 6 7 8 |
// Генератор случайных чисел srand(time(0)); RenderWindow app(VideoMode(400, 400), "Minesweeper!"); // Ширина клетки int w = 32; int gridLogic[12][12]; int gridView[12][12]; |
Далее нужно создать спрайт из заготовленной текстуры и сформировать первоначальный вид игрового поля. Для этого, мы всем элементам массива gridView[i][j]
присваиваем значение (10
) соответствующего порядкового номера квадратика текстуры:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
… … int gridView[12][12]; // Загрузка текстуры и создание спрайта Texture t; t.loadFromFile("images/tiles.jpg"); Sprite s(t); for (int i = 1; i <= 10; i++) for (int j = 1; j <= 10; j++) { gridView[i][j] = 10; } |
Добавим функцию отрисовки игрового поля:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
while (app.isOpen()) { … … // Устанавливаем белый фон app.clear(Color::White); for(int i = 1; i <= 10; i++) for (int j = 1; j <= 10; j++) { // Вырезаем из спрайта нужный нам квадратик текстуры s.setTextureRect(IntRect(gridView[i][j] * w, 0, w, w)); // Устанавливаем его в заданную позицию... s.setPosition(i*w, j*w); // ... и отрисовываем app.draw(s); } // Отображаем всю композицию на экране app.display(); } return 0; |
И в результате получим вот такую заготовку:
Примечание: Если у вас ничего не отображается, то попробуйте прописать полный путь до файла текстуры. Например, вместо:
1 |
t.loadFromFile("images/tiles.jpg"); |
укажите:
1 |
t.loadFromFile("C:\\images\\tiles.jpg"); |
При этом у вас на компьютере должна быть папка C:\images
, в которой должен находиться файл tiles.jpg
.
Расстановка и подсчет мин
Для расстановки мин на игровом поле воспользуемся генератором случайных чисел с заданным диапазоном значений от 0
до 4
. Если выпадает ноль, то ставим мину (9
), в противном случае — пустая клетка (0
). Индексы 9
и 0
совпадают с порядковыми номерами соответствующих изображений в текстуре:
1 2 3 4 5 6 7 8 9 |
Sprite s(t); for (int i = 1; i <= 10; i++) for (int j = 1; j <= 10; j++) { gridView[i][j] = 10; if (rand() % 5 == 0) gridLogic[i][j] = 9; else gridLogic[i][j] = 0; } |
Дальше реализуем подсчет мин, окружающих каждую клетку. Для этого в двойном цикле for поочередно пройдемся по каждой клетке. Если в текущей клетке уже стоит мина, то просто переходим к новому шагу итерации. Если же в текущей клетке мины нет, то с помощью девяти условий if поочередно проверяем все примыкающие к ней клетки. Если там есть мины, то на каждом сработавшем if увеличиваем счетчик количества мин на 1
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
Sprite s(t); for (int i = 1; i <= 10; i++) for (int j = 1; j <= 10; j++) { … … } // Подсчет мин вокруг каждой клетки for (int i = 1; i <= 10; i++) for (int j = 1; j <= 10; j++) { int n = 0; if (gridLogic[i][j] == 9) continue; if (gridLogic[i + 1][j] == 9) n++; if (gridLogic[i][j + 1] == 9) n++; if (gridLogic[i - 1][j] == 9) n++; if (gridLogic[i][j-1] == 9) n++; if (gridLogic[i + 1][j + 1] == 9) n++; if (gridLogic[i - 1][j - 1] == 9) n++; if (gridLogic[i - 1][j + 1] == 9) n++; if (gridLogic[i + 1][j - 1] == 9) n++; gridLogic[i][j] = n; } |
Посмотрим, что у нас получилось. Для этого добавим строку gridView[i][j] = gridLogic[i][j]
, тем самым отобразив всё поле на экране:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
while (app.isOpen()) { … for (int i = 1; i <= 10; i++) for (int j = 1; j <= 10; j++) { gridView[i][j] = gridLogic[i][j]; … } app.display(); } return 0; } |
Результат выполнения программы:
Примечание: Учитывайте, что расстановка мин генерируется случайным образом. Поэтому ваш результат запуска программы наверняка будет отличаться от того, что изображено на вышеприведенной картинке.
Обработка нажатий кнопок мыши
Теперь добавим функции обработки нажатия левой клавиши мыши (далее — ЛКМ) и установки флажка по щелчку правой клавиши мыши (далее — ПКМ). Если по нажатию ЛКМ мы открыли клетку, а там находится мина, то следом открываем всё поле — игра окончена:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
while (app.isOpen()) { // Получаем координаты курсора мышки относительно окна нашего приложения Vector2i pos = Mouse::getPosition(app); int x = pos.x / w; int y = pos.y / w; Event e; while (app.pollEvent(e)) { if (e.type == Event::Closed) app.close(); // Определяем, была ли нажата кнопка мыши? if (e.type == Event::MouseButtonPressed) // Если была нажата левая кнопка мыши, то открываем клетку if (e.key.code == Mouse::Left) gridView[x][y] = gridLogic[x][y]; // Если была нажата правая кнопка мыши, то отображаем флажок else if (e.key.code == Mouse::Right) gridView[x][y] = 11; } |
Настало время запустить приложение «Сапер» и посмотреть на готовый игровой процесс:
GitHub / Создание игры «Сапер» на С++/SFML — Исходный код
Заключение
Вот такое небольшое и довольно-таки простое приложение можно написать, используя C++ и SFML. Конечно, наша игра далека от идеала и не лишена недостатков. Например, алгоритм игры не исключает ситуации, когда игрок может проиграть уже на первом ходу. Также отсутствуют такие элементы игрового процесса, как: таймер обратного отсчета, кнопки перезапуска и паузы игры, подсчет уже отмеченных игроком мин и т.д. Возможно, мы рассмотрим реализацию подобных механизмов в следующих статьях, но ничего не мешает и вам самим попробовать добавить данный функционал. Как говорится, дорога в тысячу миль начинается с первого шага. Этот первый шаг мы сделали вместе, а дальше — дело за вами.
Дерзайте 🙂
Примечание: Статья написана по мотивам одноименного видеоролика за авторством FamTrinli.
Решил даже не заглядывать в код, воспользовался из урока лишь текстурой. По сравнению с игрой из урока: добавил обратный отсчет, вывод текста о победе/проигрыше и кнопку рестарт.
main.cpp
А можно текстурку repos? Как она вообще выглядит, хотелось бы посмотреть, если можно, конечно
Вы имеете ввиду текстуру Reset? Пока сайт не дает возможности нормально загрузить изображение в комментарии, поэтому загрузил проект на Github -> Finchi’s Project
Спасибо за урок! Сбылась мечта переписать эту игрушку. Получилось её немного добработать и прописать дополнительный функционал: окошко об окончании игры с количеством отмеченных мин, возможно снимать флажок по повторному нажатию на поле правой кнопкой и тд. Свой код прилагаю 🙂
https://pastebin.com/FmxbAW3i
Надеюсь на новые уроки по SFML и C++. Очень интересно и мотивирующе — сразу видно результат изучения языка на практике.
Спасибо, в детстве все мечтали сами переписать эти игры самому — мечта сбылась!
У меня есть вопрос: вот эта конструкция, которая гоняет главный игровой цикл приложения
вешает проц на 100%. Ну, 1 ядро, но будь ей доступны оба, повесила бы оба. Наверняка в реальных играх устроено иначе. Очень хотелось бы узнать как.
P.S. Немножко не получилось у меня, буду искать причину 🙂 вот так вышло https://skr.sh/s5wvsyM3KIH
По поводу загрузки уже примерно нашел:
https://habr.com/ru/post/136878/
https://habr.com/ru/post/134559/
Вторая ссылка вроде четче. Кратко: тоже используется while (true), но код большую часть времени "спит" и просыпается только тогда, когда возникает событие. Именно так работает стандартный Сапёр из Windows: загрузка проца держится на 0 и изредка подпрыгивает до 1%.
Используя эти идеи, сумел заварганить! Теперь проца кушает ровно 0 – event-driven подход в действии!
Вот код: https://pastebin.com/xdxFxA0V
Кратко – просто заменил функцию pollEvent() на waitEvent() и немного переставил часть кода. Ф-я waitEvent() блокирует поток и ждет события — то, что нам в данном случае нужно. Также добавил: при взрыве все игровое поле открывается.
Жаль только, что при проверке, есть ли рядом мина, мы выходим за границы массива. Это не есть хорошо.