Игра «Пятнашки» — это механическая игра-головоломка, которая была изобретена в 1878 году почтмейстером из Канастоты (деревня в штате Нью-Йорк, США) Ноем Чепмэном. И хотя официальной датой создания игры значится именно 1878 год, первый вариант своей головоломки Ной продемонстрировал в кругу друзей еще за 4 года до этого.
Уже в 1880 году игра приобрела колоссальную популярность, а спустя всего лишь несколько месяцев в нее уже вовсю играли жители таких стран, как: Эстония, Норвегия, Швеция, Австрия, Латвия, Германия, Англия, Австралия, Дания, Мексика, Италия, Нидерланды и Новая Зеландия.
Игру «Пятнашки» раскупали миллионными партиями, часто люди забывали про еду и сон. Из-за этого на некоторых предприятиях был введен запрет на «Пятнашки». Впоследствии у этого явления даже появилось название «Пятнашечное» сумасшествие.
Стоит отметить, что на данный момент головоломка «Пятнашки» занимает третье место в мире по популярности после Пазлов и кубика Рубика.
Описание игры и её правила
В классическом варианте исполнения игра «Пятнашки» представляет собой квадратное поле размером 4×4 клетки с 15-ю квадратными подвижными элементами, на которые нанесены числа от 1 до 15, и одной свободной ячейкой. Цель игры состоит в том, чтобы, перемещая элементы в горизонтальном или вертикальном направлениях, выстроить числа в правильной последовательности (1, 2, 3, 4 и т.д.).
Приступаем к разработке
Я уже неоднократно об этом говорил (а повторение, как известно, — мать учения), что практически любое SFML-приложение можно начать с нижеследующего минимального каркаса:
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 26 27 28 29 30 31 |
#include <SFML/Graphics.hpp> using namespace sf; int main() { RenderWindow window(VideoMode(256, 256), "15-Puzzle! for Ravesli.com"); // Главный цикл приложения: выполняется, пока открыто окно while (window.isOpen()) { // Обрабатываем события в цикле Event event; while (window.pollEvent(event)) { // Пользователь нажал на «крестик» и хочет закрыть окно? if (event.type == Event::Closed) { // тогда закрываем его window.close(); } } // Устанавливаем цвет фона - белый window.clear(Color::White); // Отрисовка окна window.display(); } return 0; } |
Далее нам нужно создать переменную текстуры и загрузить для нее .png-файл изображения, которое содержит графическое представление элементов игры (квадраты с нанесенными на них числами от 1 до 15):
1 2 3 4 5 6 7 8 9 |
RenderWindow window(VideoMode(256, 256), "15-Puzzle! for Ravesli.com"); // Создание и загрузка текстуры Texture texture; texture.loadFromFile("images/15.png"); // Главный цикл приложения: выполняется, пока открыто окно while (window.isOpen()) { … } |
Примечание: .png-файл с изображением для текстуры вы можете найти в конце статьи в исходниках на GitHub (папка images).
Как уже отмечалось выше, «Пятнашки» состоят из 15 подвижных элементов с числами (далее — «блоки») и 1 пустой ячейки, расположенных в 4 строках и 4 столбцах игрового поля. В нашем случае размер одного такого блока будет соответствовать квадрату 64×64 пикселя.
Так как всего будет 16 блоков, то для их графического представления нам потребуется массив из 17 спрайтов. Как вы знаете, в языке С++ нумерация элементов массива начинается с 0
, но, т.к. числа на наших блоках начинаются с 1
, для простоты мы будем использовать массив не из 16 спрайтов, а из 17. Отсчет первого элемента при этом будем начинать с индекса 1
, а не с индекса 0
.
Осталось определиться с игровым полем. Для его математического описания вполне логично воспользоваться целочисленным двумерным массивом, состоящим из 4 строк и 4 столбцов, но в дальнейшем, когда нужно будет определять, находится ли рядом с текущим элементом пустой блок (для того, чтобы поменять их местами), нам придется помимо основного кода написать еще и дополнительный код проверок на выход за границы массива игрового поля (в большей степени это относится к элементам игрового поля, представляющим граничные блоки).
Это приведет к усложнению нашей программы, ухудшит её читабельность и повысит риск сделать ошибку в коде. Выходом из данной ситуации является добавление с каждого края к нашему массиву (ниже на рисунке он обозначен оранжевым цветом) по одному дополнительному нулевому столбцу/строке (на рисунке они выделены голубым цветом). Отсчет первого элемента данного двумерного массива так же будем начинать с индекса 1
, а не с индекса 0
(на рисунке индексы выделены зеленым цветом). Таким образом, модель игрового поля принимает следующий вид:
В коде:
1 2 3 4 5 6 7 8 |
// Размер стороны одного квадратного блока составляет 64 пикселя int blockWidht = 64; // Логическое представление игрового поля int grid[6][6] = { 0 }; // Массив спрайтов Sprite sprite[17]; |
Теперь заполняем «логические ячейки» игрового поля, попутно создавая спрайты соответствующих чисел для его блоков:
1 2 3 4 5 6 7 8 9 10 |
// Создаем спрайты соответствующих чисел (1, 2, 3, ..., 15) и заполняем ячейки игрового поля int n = 0; for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) { n++; sprite[n].setTexture(texture); sprite[n].setTextureRect(IntRect(i*blockWidht, j*blockWidht, blockWidht, blockWidht)); grid[i + 1][j + 1] = n; } |
Осталось установить наши спрайты на позиции соответствующих блоков игрового поля и произвести их отрисовку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Установка цвета фона - белый window.clear(Color::White); // Установка каждого спрайта на свое место + Отрисовка спрайта for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) { // Считываем значение ячейки игрового поля… int n = grid[i + 1][j + 1]; // …и устанавливаем на нее соответствующий спрайт sprite[n].setPosition(i*blockWidht, j*blockWidht); // Отрисовка спрайта window.draw(sprite[n]); } // Отрисовка окна window.display(); |
Теперь можно попробовать запустить программу. В результате у нас должна получиться следующая статичная заготовка:
Примечание: Если у вас ничего не отображается, то попробуйте прописать полный путь к файлу текстуры. Например, вместо:
1 |
texture.loadFromFile("images/15.png"); |
укажите:
1 |
texture.loadFromFile(“C:\\images\\15.png"); |
При этом у вас на компьютере должна быть папка C:\images
, в которой должен находиться файл 15.png
.
Обработка нажатия кнопки мыши
Когда пользователь нажимает левой кнопкой мыши (сокр. «ЛКМ») на каком-либо блоке игрового поля с целью переставить его на пустое место, то мы должны отловить данное событие, а затем проверить, находится ли рядом с этим блоком пустая клетка и если действительно такая клетка присутствует, то нужно задать направление перестановки (через переменные dx
и dy
).
Ниже представлен код обработчика события щелчка ЛКМ и проверки на присутствие рядом пустой клетки:
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 26 27 28 29 30 31 32 33 34 35 |
// Пользователь нажал на «крестик» и хочет закрыть окно? if (event.type == Event::Closed) // тогда закрываем его window.close(); // Пользователь щелкнул мышкой? if (event.type == Event::MouseButtonPressed) { // Если это была ЛКМ, то пробуем выполнить перестановку "пятнашек" if (event.key.code == Mouse::Left) { // Получаем координаты того места, где был произведен щелчок Vector2i position = Mouse::getPosition(window); // Переводим эти координаты в координаты наших блоков int x = position.x / blockWidht + 1; int y = position.y / blockWidht + 1; // Переменные для задания смещения... int dx = 0; // ...горизонтального... int dy = 0; // ...и вертикального. // Если справа пустое место if (grid[x + 1][y] == 16) { dx = 1; dy = 0; }; // Если снизу пустое место if (grid[x][y + 1] == 16) { dx = 0; dy = 1; }; // Если сверху пустое место if (grid[x][y - 1] == 16) { dx = 0; dy = -1; }; // Если слева пустое место if (grid[x-1][y] == 16) { dx = -1; dy = 0; }; } } |
Также добавим механизм перестановки двух блоков. Алгоритм данной операции предельно простой — это обмен значениями двух переменных с использованием третьей переменной, в которой сохраняется промежуточный результат:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Если слева пустое место, if (grid[x-1][y] == 16) { dx = -1; dy = 0; }; // то меняем местами пустую клетку с выбранным блоком int temp = grid[x][y]; grid[x][y] = 16; grid[x + dx][y + dy] = temp; } } // тогда закрываем его window.close(); |
Снова компилируем, запускаем нашу программу и видим уже следующее:
Добавляем анимацию перемещения
Остался последний штрих — сделать более плавной перестановку выбранного блока на пустое место. Для этого добавим дополнительный цикл for, в котором мы будем двигать выбранный блок с помощью функции sprite[temp.].move(speed*dx, speed*dy);
.
На каждой итерации цикла блок будет перемещаться на speed*dx
пикселей по горизонтали и на speed*dy
пикселей по вертикали:
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 26 |
// Меняем местами пустую клетку с выбранным блоком int temp = grid[x][y]; grid[x][y] = 16; grid[x + dx][y + dy] = temp; // Добавляем анимацию перемещения блоков // Ставим пустой блок на место выбранного пользователем блока sprite[16].move(-dx*blockWidht, -dy*blockWidht); // Скорость анимации float speed = 6; for(int i = 0; i < blockWidht; i += speed) { // Двигаем выбранный блок sprite[temp].move(speed*dx, speed*dy); // Отрисовываем пустой блок window.draw(sprite[16]); // Отрисовываем выбранный блок window.draw(sprite[temp]); // Отображаем всю композицию в окне window.display(); } |
Для того, чтобы увидеть результат, нужно добавить в самое начало нашей программы еще одну строку кода, которая установит ограничение максимальной частоты кадров, равное 60
:
1 2 3 4 5 6 7 8 9 10 |
int main() { RenderWindow window(VideoMode(256, 256), "15-Puzzle! for Ravesli.com"); // Задаем максимальную частоту кадров (иначе эффект анимации может быть незаметен) window.setFramerateLimit(60); // Создание и загрузка текстуры Texture texture; texture.loadFromFile("images/15.png"); |
Скомпилировав и запустив нашу программу, мы уже сможем наблюдать финальный результат наших трудов:
GitHub / Создание игры «Пятнашки» на C++/SFML — Исходный код
Заключение
Вот мы и подошли к концу еще одного урока по C++/SFML. На мой взгляд, он является одним из самых простых примеров быстрого создания относительно несложного игрового приложения. Надеюсь, что каждый, кто дочитал эту статью до конца, сможет найти в ней что-то новое и интересное для себя. Ну а я, как всегда, говорю Вам: «До встречи на следующем уроке!» 🙂
Примечание: Статья написана по мотивам одноименного видеоролика за авторством FamTrinli.
Спасибо большое вашему сайту за прекрасный перевод отличных уроков, а так же авторские материалы. Пройден весь путь от первого урока через все практические задания, MFC и SFML.
Добрый вечер) Поставил себе задачу придумать возможность отменить последний ход. Никак не могу додуматься. Может вы подскажете ? Буду невероятно благодарен)
Здравствуйте, а как самому создать спрайт? Так и не понял, хоть и посмотрел соответствующий урок у Вас
Разве для спрайта не достаточно просто нарисовать любую картинку и использовать её как текстуры?