Простая игра на SFML
Будем делать игру "пятнашки" на языке C++ с использованием библиотеки SFML. Пятнашки - это широко известная головоломка, которая выглядит следующим образом:
На игровом поле размером 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:
Там же в разделе Linker в Additional Library Directories добавляем путь к папке lib:
А из каталога bin нужно скопировать DLL-файлы и сложить их в каталог с exe-файлом нашего проекта:
Кроме того, в разделе Linker, в подразделе Input, нужно в Additional Dependencies добавить используемые файлы библиотеки. В нашем случае достаточно добавить три файла: sfml-system-d.lib, sfml-window-d.lib и sfml-graphics-d.lib:
Символ -d в названии файла означает, что это отладочная версия и должна использоваться в конфигурации Debug. В настройках релизной версии нужно будет задавать файлы без буквы -d в названии.
Хорошая инструкция по подключению библиотеки SFML к проекту Visual Studio находится на сайте библиотеки.
Попробуем теперь задействовать библиотеку в нашем проекте. Создадим окно и запустим цикл обработки событий.
// 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 пикселей с черным фоном:
Окно можно закрыть обычным способом мышью, либо через клавишу Esc. Обработчик нажатий клавиш клавиатуры также включен в цикл обработки сообщений.
Прежде чем приступить к делу, нам понадобится какой-нибудь шрифт для вывода текста на экран. Я для примера взял шрифт TrueType Calibri.
Теперь можем начинать делать свою игру.
Создаем новый класс Game:
Класс будет отвечать за работу игры и за отрисовку игрового поля. Для этого будем наследовать наш класс от классов Drawable и Transformable библиотеки SFML.
Итак, начинаем описывать наш класс в файле Game.h.
Первым делом подключаем библиотеку 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::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); } } }
В методе отрисовки первым делом применяем трансформацию координат, путем умножения на матрицу трансформирования. Это нужно для того, чтобы можно было задавать координаты нашему игровому полю. Далее с помощью объектов RectangleShape библиотеки SFML, рисуем рамки игрового поля и рамки каждой плашки в игре. На плашках также еще отрисовываем текст с номером плашки. Кроме того, если головоломка решена, то цвет плашек делаем другим.
Теперь настало время вернуться к функции main:
// 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. Этот счетчик используется дальше для выполнения ходов в случайных направлениях, пока не обнулится, а плашки не перемешаются. Таким образом мы точно получим решаемое состояние головоломки.
Вот в общем-то и все, компилируем, собираем, запускаем:
В этой статье я показал, как можно быстро создать простую игру на C++ с использованием библиотеки SFML. Однако архитектура самой программы далека от идеала. В следующей статье мы попробуем с этим что-нибудь сделать.
Комментарии (0)