Будем делать игру «пятнашки» на языке C++ с использованием библиотеки SFML. Пятнашки — это широко известная головоломка, которая выглядит следующим образом:
![](https://habrastorage.org/r/w780q1/webt/7u/hl/by/7uhlbycd1tkovgw_igia0xwklc8.jpeg)
На игровом поле размером 4х4 случайным образом расположены 15 плашек с номерами от 1 до 15 и одно свободное место. Передвигать плашки можно только по одной и только на свободное место. Целью игры является выстроение плашек на игровом поле в порядке, соответствующем их номерам.
Итак, начнем.
Запускаем Visual Studio и создаем новый пустой проект. Можете назвать его как хотите, я назвал «15». В этом проекте создаем новый файл main.cpp и пустую функцию main:
Далее скачиваем библиотеку SFML с сайта sfml-dev.org и распаковываем ее. В распакованной библиотеке есть нужные нам папки: include, lib и bin. В свойствах проекта в разделе C/C++ в Additional Include Directories добавляем путь к папке include:
![](https://habrastorage.org/r/w1560/webt/zd/t8/ky/zdt8kyekzv1txcad5vhycwphdma.png)
Там же в разделе Linker в Additional Library Directories добавляем путь к папке lib:
![](https://habrastorage.org/r/w1560/webt/9l/us/lb/9luslb9vhfieeof3lczrjq_85u0.png)
А из каталога bin нужно скопировать DLL-файлы и сложить их в каталог с exe-файлом нашего проекта:
![](https://habrastorage.org/r/w1560/webt/ew/nk/ex/ewnkexitg7y6mvlxwb4fvs2_ums.png)
Кроме того, в разделе Linker, в подразделе Input, нужно в Additional Dependencies добавить используемые файлы библиотеки. В нашем случае достаточно добавить три файла: sfml-system-d.lib, sfml-window-d.lib и sfml-graphics-d.lib:
![](https://habrastorage.org/r/w1560/webt/k1/zc/p-/k1zcp-w1_jcbgd8rzy0itmhwe7m.png)
Символ -d в названии файла означает, что это отладочная версия и должна использоваться в конфигурации Debug. В настройках релизной версии нужно будет задавать файлы без символа -d в названии.
Хорошая инструкция по подключению библиотеки SFML к проекту Visual Studio находится на сайте библиотеки.
Попробуем теперь задействовать библиотеку в нашем проекте. Создадим окно и запустим цикл обработки событий:
Результатом будет квадратное окно размером 600 на 600 пикселей с черным фоном:
![](https://habrastorage.org/r/w1560/webt/qb/l3/_o/qbl3_op1hp84mtuq5b49c2q1dlc.png)
Окно можно закрыть обычным способом мышью, либо через клавишу Esc. Обработчик нажатий клавиш клавиатуры также включен в цикл обработки сообщений.
Прежде чем приступить к делу, нам понадобится какой-нибудь шрифт для вывода текста на экран. Я для примера взял шрифт TrueType Calibri.
Теперь можем начинать делать свою игру.
Создаем новый класс Game:
![](https://habrastorage.org/r/w1560/webt/gb/-b/sf/gb-bsf2p5hjalkleyc4s_nm5vso.png)
Класс будет отвечать за работу игры и за отрисовку игрового поля. Для этого будем наследовать наш класс от классов Drawable и Transformable библиотеки SFML.
Итак, начинаем описывать наш класс
Первым делом подключаем библиотеку Graphics:
Тут же объявляем некоторые константы, требуемые для игры:
Также объявляем свой тип enum, определяющий направление перемещения плашки:
Ну и наконец сам класс:
Самое главное что у нас в нем есть — это массив elements, содержащий целочисленные значения, соответствующие состоянию игрового поля. Элементы в массиве соответствуют элементам игрового поля слева направо, сверху вниз, то есть первые 4 элемента массива соответствуют первой строке поля, вторые 4 элемента — второй строке и т.д.
Далее две переменные, которые будут вычисляться каждый ход — это empty_index (индекс в массиве, соответствующий свободной ячейке) и solved (признак того, что головоломка решена).
Кроме того, в классе задана переменная font, определяющая шрифт, который будет использоваться при выводе текста в окне.
Теперь напишем реализацию методов нашего класса.
Конструктор класса загружает шрифт из внешнего файла и вызывает метод инициализации игры:
Метод инициализации игры заполняет массив элементами в правильном порядке и устанавливает признак решенной головоломки:
Да, изначально у нас игра будет инициализироваться как решенная, а перед началом игры мы будем перемешивать плашки с помощью случайных ходов.
Следующий метод проверяет, решена ли головоломка и возвращает результат проверки:
И наконец, метод, реализующий перемещение плашки в игре:
Последний метод класса — это метод, который отрисовывает игровое поле:
В методе отрисовки первым делом применяем трансформацию координат, путем умножения на матрицу трансформирования. Это нужно для того, чтобы можно было задавать координаты нашему игровому полю. Далее с помощью объектов RectangleShape библиотеки SFML, рисуем рамки игрового поля и рамки каждой плашки в игре. На плашках также еще отрисовываем текст с номером плашки. Кроме того, если головоломка решена, то цвет плашек делаем другим.
Настало время вернуться к функции main:
Вначале подгружаем шрифт и создаем объект Text для вывода на экран строки текста с назначенем клавиш. Далее создаем наш объект игры и устанавливаем позицию поля в точку с координатами (50,50) — так мы делаем отступ от края окна.
Управление игрой я решил делать через клавиатуру, так что на каждое нажатие клавиш стрелок вызываем у объекта игры метод Move — для перемещения плашки в соответствующем направлении.
Нажатие клавиши F2 — это начало новой игры, так что в обработчике этого события заново инициализируем игру (что приведет к расстановке плашек по своим местам), а также выставляем значение счетчика ходов равным 100. Этот счетчик используется дальше для выполнения ходов в случайных направлениях, пока не обнулится, а плашки не перемешаются. Таким образом мы точно получим решаемое состояние головоломки.
Вот в общем-то и все, компилируем, собираем, запускаем:
![](https://habrastorage.org/r/w1560/webt/8s/c5/pc/8sc5pcprcxdaplxgekluevknn4c.png)
В этой статье я показал, как можно быстро создать простую игру на C++ с использованием библиотеки SFML. Однако архитектура самой программы далека от идеала. В следующей статье мы попробуем с этим что-нибудь сделать.
![](https://habrastorage.org/webt/7u/hl/by/7uhlbycd1tkovgw_igia0xwklc8.jpeg)
На игровом поле размером 4х4 случайным образом расположены 15 плашек с номерами от 1 до 15 и одно свободное место. Передвигать плашки можно только по одной и только на свободное место. Целью игры является выстроение плашек на игровом поле в порядке, соответствующем их номерам.
Итак, начнем.
Запускаем Visual Studio и создаем новый пустой проект. Можете назвать его как хотите, я назвал «15». В этом проекте создаем новый файл main.cpp и пустую функцию main:
// main.cpp
int main()
{
return 0;
}
Далее скачиваем библиотеку SFML с сайта sfml-dev.org и распаковываем ее. В распакованной библиотеке есть нужные нам папки: include, lib и bin. В свойствах проекта в разделе C/C++ в Additional Include Directories добавляем путь к папке include:
![](https://habrastorage.org/webt/zd/t8/ky/zdt8kyekzv1txcad5vhycwphdma.png)
Там же в разделе Linker в Additional Library Directories добавляем путь к папке lib:
![](https://habrastorage.org/webt/9l/us/lb/9luslb9vhfieeof3lczrjq_85u0.png)
А из каталога bin нужно скопировать DLL-файлы и сложить их в каталог с exe-файлом нашего проекта:
![](https://habrastorage.org/webt/ew/nk/ex/ewnkexitg7y6mvlxwb4fvs2_ums.png)
Кроме того, в разделе Linker, в подразделе Input, нужно в Additional Dependencies добавить используемые файлы библиотеки. В нашем случае достаточно добавить три файла: sfml-system-d.lib, sfml-window-d.lib и sfml-graphics-d.lib:
![](https://habrastorage.org/webt/k1/zc/p-/k1zcp-w1_jcbgd8rzy0itmhwe7m.png)
Символ -d в названии файла означает, что это отладочная версия и должна использоваться в конфигурации Debug. В настройках релизной версии нужно будет задавать файлы без символа -d в названии.
Хорошая инструкция по подключению библиотеки SFML к проекту Visual Studio находится на сайте библиотеки.
Попробуем теперь задействовать библиотеку в нашем проекте. Создадим окно и запустим цикл обработки событий:
main.cpp
// main.cpp
#include <SFML/Graphics.hpp>
int main()
{
// Создаем окно размером 600 на 600 и частотой обновления 60 кадров в секунду
sf::RenderWindow window(sf::VideoMode(600, 600), "15");
window.setFramerateLimit(60);
sf::Event event;
while (window.isOpen())
{
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed) window.close();
if (event.type == sf::Event::KeyPressed)
{
// Получаем нажатую клавишу - выполняем соответствующее действие
if (event.key.code == sf::Keyboard::Escape) window.close();
}
}
// Выполняем необходимые действия по отрисовке
window.clear();
window.display();
}
return 0;
}
Результатом будет квадратное окно размером 600 на 600 пикселей с черным фоном:
![](https://habrastorage.org/webt/qb/l3/_o/qbl3_op1hp84mtuq5b49c2q1dlc.png)
Окно можно закрыть обычным способом мышью, либо через клавишу Esc. Обработчик нажатий клавиш клавиатуры также включен в цикл обработки сообщений.
Прежде чем приступить к делу, нам понадобится какой-нибудь шрифт для вывода текста на экран. Я для примера взял шрифт TrueType Calibri.
Теперь можем начинать делать свою игру.
Создаем новый класс Game:
![](https://habrastorage.org/webt/gb/-b/sf/gb-bsf2p5hjalkleyc4s_nm5vso.png)
Класс будет отвечать за работу игры и за отрисовку игрового поля. Для этого будем наследовать наш класс от классов Drawable и Transformable библиотеки SFML.
Итак, начинаем описывать наш класс
Game.h
#pragma once
#include <SFML/Graphics.hpp>
const int SIZE = 4; // Размер игрового поля в плашках
const int ARRAY_SIZE = SIZE * SIZE; // Размер массива
const int FIELD_SIZE = 500; // Размер игрового поля в пикселях
const int CELL_SIZE = 120; // Размер плашки в пикселях
enum class Direction { Left = 0, Right = 1, Up = 2, Down = 3 };
class Game : public sf::Drawable, public sf::Transformable
{
protected:
int elements[ARRAY_SIZE];
int empty_index;
bool solved;
sf::Font font;
public:
Game();
void Init();
bool Check();
void Move(Direction direction);
public:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;
};
Первым делом подключаем библиотеку Graphics:
#include <SFML/Graphics.hpp>
Тут же объявляем некоторые константы, требуемые для игры:
const int SIZE = 4; // Размер игрового поля в плашках
const int ARRAY_SIZE = SIZE * SIZE; // Размер массива плашек
const int FIELD_SIZE = 500; // Размер игрового поля в пикселях
const int CELL_SIZE = 120; // Размер плашки в пикселях
Также объявляем свой тип enum, определяющий направление перемещения плашки:
enum class Direction { Left = 0, Right = 1, Up = 2, Down = 3 };
Ну и наконец сам класс:
class Game : public sf::Drawable, public sf::Transformable
{
protected:
int elements[ARRAY_SIZE];
int empty_index;
bool solved;
sf::Font font;
public:
Game();
void Init();
bool Check();
void Move(Direction direction);
public:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;
};
Самое главное что у нас в нем есть — это массив elements, содержащий целочисленные значения, соответствующие состоянию игрового поля. Элементы в массиве соответствуют элементам игрового поля слева направо, сверху вниз, то есть первые 4 элемента массива соответствуют первой строке поля, вторые 4 элемента — второй строке и т.д.
Далее две переменные, которые будут вычисляться каждый ход — это empty_index (индекс в массиве, соответствующий свободной ячейке) и solved (признак того, что головоломка решена).
Кроме того, в классе задана переменная font, определяющая шрифт, который будет использоваться при выводе текста в окне.
Теперь напишем реализацию методов нашего класса.
Game.cpp
#include "Game.h"
Game::Game()
{
// Подгружаем шрифт для отрисовки элементов
font.loadFromFile("calibri.ttf");
Init();
}
void Game::Init()
{
// Заполняем массив плашек
for (int i = 0; i < ARRAY_SIZE - 1; i++) elements[i] = i + 1;
// Ставим пустую плашку в правую нижнюю позицию
empty_index = ARRAY_SIZE - 1;
elements[empty_index] = 0; // Пустая плашка имеет значение = 0
solved = true;
}
bool Game::Check()
{
// Проверка собранности головоломки
for (unsigned int i = 0; i < ARRAY_SIZE; i++)
{
if (elements[i] > 0 && elements[i] != i + 1) return false;
}
return true;
}
void Game::Move(Direction direction)
{
// Вычисляем строку и колонку пустой плашки
int col = empty_index % SIZE;
int row = empty_index / SIZE;
// Проверка на возможность перемещения и вычисление индекса перемещаемой плашки
int move_index = -1;
if (direction == Direction::Left && col < (SIZE - 1)) move_index = empty_index + 1;
if (direction == Direction::Right && col > 0) move_index = empty_index - 1;
if (direction == Direction::Up && row < (SIZE - 1)) move_index = empty_index + SIZE;
if (direction == Direction::Down && row > 0) move_index = empty_index - SIZE;
// Перемещение плашки на место пустой
if (empty_index >= 0 && move_index >= 0)
{
int tmp = elements[empty_index];
elements[empty_index] = elements[move_index];
elements[move_index] = tmp;
empty_index = move_index;
}
solved = Check();
}
void Game::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
states.transform *= getTransform();
sf::Color color = sf::Color(200, 100, 200);
// Рисуем рамку игрового поля
sf::RectangleShape shape(sf::Vector2f(FIELD_SIZE, FIELD_SIZE));
shape.setOutlineThickness(2.f);
shape.setOutlineColor(color);
shape.setFillColor(sf::Color::Transparent);
target.draw(shape, states);
// Подготавливаем рамку для отрисовки всех плашек
shape.setSize(sf::Vector2f(CELL_SIZE - 2, CELL_SIZE - 2));
shape.setOutlineThickness(2.f);
shape.setOutlineColor(color);
shape.setFillColor(sf::Color::Transparent);
// Подготавливаем текстовую заготовку для отрисовки номеров плашек
sf::Text text("", font, 52);
for (unsigned int i = 0; i < ARRAY_SIZE; i++)
{
shape.setOutlineColor(color);
text.setFillColor(color);
text.setString(std::to_string(elements[i]));
if (solved)
{
// Решенную головоломку выделяем другим цветом
shape.setOutlineColor(sf::Color::Cyan);
text.setFillColor(sf::Color::Cyan);
}
else if (elements[i] == i + 1)
{
// Номера плашек на своих местах выделяем цветом
text.setFillColor(sf::Color::Green);
}
// Рисуем все плашки, кроме пустой
if (elements[i] > 0)
{
// Вычисление позиции плашки для отрисовки
sf::Vector2f position(i % SIZE * CELL_SIZE + 10.f, i / SIZE * CELL_SIZE + 10.f);
shape.setPosition(position);
// Позицию текста подбирал вручную
text.setPosition(position.x + 30.f + (elements[i] < 10 ? 15.f : 0.f), position.y + 25.f);
target.draw(shape, states);
target.draw(text, states);
}
}
}
Конструктор класса загружает шрифт из внешнего файла и вызывает метод инициализации игры:
Game::Game()
{
// Подгружаем шрифт для отрисовки элементов
font.loadFromFile("calibri.ttf");
Init();
}
Метод инициализации игры заполняет массив элементами в правильном порядке и устанавливает признак решенной головоломки:
void Game::Init()
{
// Заполняем массив плашек
for (int i = 0; i < ARRAY_SIZE - 1; i++) elements[i] = i + 1;
// Пустая ячейка - в последнем элементе массива
empty_index = ARRAY_SIZE - 1;
elements[empty_index] = 0; // Пустая плашка имеет значение = 0
solved = true;
}
Да, изначально у нас игра будет инициализироваться как решенная, а перед началом игры мы будем перемешивать плашки с помощью случайных ходов.
Следующий метод проверяет, решена ли головоломка и возвращает результат проверки:
bool Game::Check()
{
// Проверка собранности головоломки
for (unsigned int i = 0; i < ARRAY_SIZE; i++)
{
if (elements[i] > 0 && elements[i] != i + 1) return false;
}
return true;
}
И наконец, метод, реализующий перемещение плашки в игре:
void Game::Move(Direction direction)
{
// Вычисляем строку и колонку пустой плашки
int col = empty_index % SIZE;
int row = empty_index / SIZE;
// Проверка на возможность перемещения и вычисление индекса перемещаемой плашки
int move_index = -1;
if (direction == Direction::Left && col < (SIZE - 1)) move_index = empty_index + 1;
if (direction == Direction::Right && col > 0) move_index = empty_index - 1;
if (direction == Direction::Up && row < (SIZE - 1)) move_index = empty_index + SIZE;
if (direction == Direction::Down && row > 0) move_index = empty_index - SIZE;
// Перемещение плашки на место пустой
if (empty_index >= 0 && move_index >= 0)
{
int tmp = elements[empty_index];
elements[empty_index] = elements[move_index];
elements[move_index] = tmp;
empty_index = move_index;
}
solved = Check();
}
Последний метод класса — это метод, который отрисовывает игровое поле:
draw
void Game::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
states.transform *= getTransform();
sf::Color color = sf::Color(200, 100, 200);
// Рисуем рамку игрового поля
sf::RectangleShape shape(sf::Vector2f(FIELD_SIZE, FIELD_SIZE));
shape.setOutlineThickness(2.f);
shape.setOutlineColor(color);
shape.setFillColor(sf::Color::Transparent);
target.draw(shape, states);
// Подготавливаем рамку для отрисовки всех плашек
shape.setSize(sf::Vector2f(CELL_SIZE - 2, CELL_SIZE - 2));
shape.setOutlineThickness(2.f);
shape.setOutlineColor(color);
shape.setFillColor(sf::Color::Transparent);
// Подготавливаем текстовую заготовку для отрисовки номеров плашек
sf::Text text("", font, 52);
for (unsigned int i = 0; i < ARRAY_SIZE; i++)
{
shape.setOutlineColor(color);
text.setFillColor(color);
text.setString(std::to_string(elements[i]));
if (solved)
{
// Решенную головоломку выделяем другим цветом
shape.setOutlineColor(sf::Color::Cyan);
text.setFillColor(sf::Color::Cyan);
}
else if (elements[i] == i + 1)
{
// Номера плашек на своих местах выделяем цветом
text.setFillColor(sf::Color::Green);
}
// Рисуем все плашки, кроме пустой
if (elements[i] > 0)
{
// Вычисление позиции плашки для отрисовки
sf::Vector2f position(i % SIZE * CELL_SIZE + 10.f, i / SIZE * CELL_SIZE + 10.f);
shape.setPosition(position);
// Позицию текста подбирал вручную
text.setPosition(position.x + 30.f + (elements[i] < 10 ? 15.f : 0.f), position.y + 25.f);
// Отрисовываем рамку плашки
target.draw(shape, states);
// Отрисовываем номер плашки
target.draw(text, states);
}
}
}
В методе отрисовки первым делом применяем трансформацию координат, путем умножения на матрицу трансформирования. Это нужно для того, чтобы можно было задавать координаты нашему игровому полю. Далее с помощью объектов RectangleShape библиотеки SFML, рисуем рамки игрового поля и рамки каждой плашки в игре. На плашках также еще отрисовываем текст с номером плашки. Кроме того, если головоломка решена, то цвет плашек делаем другим.
Настало время вернуться к функции main:
main.cpp
// main.cpp
#include <SFML/Graphics.hpp>
#include "Game.h"
int main()
{
// Создаем окно размером 600 на 600 и частотой обновления 60 кадров в секунду
sf::RenderWindow window(sf::VideoMode(600, 600), "15");
window.setFramerateLimit(60);
sf::Font font;
font.loadFromFile("calibri.ttf");
// Текст с обозначением клавиш
sf::Text text("F2 - New Game / Esc - Exit / Arrow Keys - Move Tile", font, 20);
text.setFillColor(sf::Color::Cyan);
text.setPosition(5.f, 5.f);
// Создаем объект игры
Game game;
game.setPosition(50.f, 50.f);
sf::Event event;
int move_counter = 0; // Счетчик случайных ходов для перемешивания головоломки
while (window.isOpen())
{
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed) window.close();
if (event.type == sf::Event::KeyPressed)
{
// Получаем нажатую клавишу - выполняем соответствующее действие
if (event.key.code == sf::Keyboard::Escape) window.close();
if (event.key.code == sf::Keyboard::Left) game.Move(Direction::Left);
if (event.key.code == sf::Keyboard::Right) game.Move(Direction::Right);
if (event.key.code == sf::Keyboard::Up) game.Move(Direction::Up);
if (event.key.code == sf::Keyboard::Down) game.Move(Direction::Down);
// Новая игра
if (event.key.code == sf::Keyboard::F2)
{
game.Init();
move_counter = 100;
}
}
}
// Если счетчик ходов больше нуля, продолжаем перемешивать головоломку
if (move_counter-- > 0) game.Move((Direction)(rand() % 4));
// Выполняем необходимые действия по отрисовке
window.clear();
window.draw(game);
window.draw(text);
window.display();
}
return 0;
}
Вначале подгружаем шрифт и создаем объект Text для вывода на экран строки текста с назначенем клавиш. Далее создаем наш объект игры и устанавливаем позицию поля в точку с координатами (50,50) — так мы делаем отступ от края окна.
Управление игрой я решил делать через клавиатуру, так что на каждое нажатие клавиш стрелок вызываем у объекта игры метод Move — для перемещения плашки в соответствующем направлении.
Нажатие клавиши F2 — это начало новой игры, так что в обработчике этого события заново инициализируем игру (что приведет к расстановке плашек по своим местам), а также выставляем значение счетчика ходов равным 100. Этот счетчик используется дальше для выполнения ходов в случайных направлениях, пока не обнулится, а плашки не перемешаются. Таким образом мы точно получим решаемое состояние головоломки.
Вот в общем-то и все, компилируем, собираем, запускаем:
![](https://habrastorage.org/webt/p0/ne/rg/p0nerg896fzcjdv2rwaqy47kr7m.png)
![](https://habrastorage.org/webt/8s/c5/pc/8sc5pcprcxdaplxgekluevknn4c.png)
В этой статье я показал, как можно быстро создать простую игру на C++ с использованием библиотеки SFML. Однако архитектура самой программы далека от идеала. В следующей статье мы попробуем с этим что-нибудь сделать.