Разработка игры "Пятнашки" на SFML C++
Привет! Сегодня хочу поделиться опытом создания классической головоломки — игры "Пятнашки", на C++ с использованием фреймворка SFML (Simple and Fast Multimedia Library).
Если вы начинающий разработчик или просто любите писать игры своими руками, то эта статья может вам понравиться.
Я расскажу не только о том, как отобразить окно и кнопки, но и о том, как организовать логику игры, обработать взаимодействие пользователя, а также немного поговорим о структуре проекта и возможностях SFML.
Игра "Пятнашки" — отличный способ попрактиковаться в программировании: здесь есть и логика, и работа с интерфейсом, и даже простая анимация. А SFML — это отличный выбор для тех, кто хочет создавать 2D-игры без излишней сложности и с минимальным порогом входа.
Подготовка среды разработки
Перед тем как писать код, нужно подготовить рабочую среду:
Например:
Скачайте и установите SFML:
Официальный сайт: https://www.sfml-dev.org/download.php
Выберите версию под ваш компилятор и систему.
Добавьте пути к заголовочным файлам SFML.
Слинкуйте нужные библиотеки (sfml-graphics, sfml-window, sfml-system).
Проверьте, что всё работает — откройте простое окно SFML.
Общая структура проекта
Базовый класс Screen — основа всех экранов
В процессе создания игры важно было организовать удобную архитектуру, особенно потому, что у нас есть несколько экранов: главное меню, игровой экран и экран с правилами. Чтобы не повторять один и тот же код в каждом из них, я создал абстрактный базовый класс Screen.
Класс Screen служит общей основой для всех экранов приложения:
он предоставляет доступ к окну (sf::RenderWindow);
хранит ссылки на глобальное состояние игры (GameState) и переходы между экранами (Transition);
реализует общие элементы интерфейса (например, фон);
упрощает расширяемость проекта — добавление нового экрана становится простым делом.
Основные компоненты класса
#pragma once #include <SFML/Graphics.hpp> #include "GameState.h" #include "Transition.h" #include "ResourceManager.h" #include <memory> class Screen : public sf::Drawable { protected: sf::RenderWindow& window; GameState& state; Transition& transition; // Общие элементы интерфейса sf::Sprite backgroundSprite; bool useTexture; Screen(sf::RenderWindow& window, G ameState& state, Transition& transition); // Виртуальные методы для настройки ресурсов и элементов интерфейса virtual void initialize() = 0; // Вспомогательные методы void setupBackground(const std::string& textureId); public: virtual ~Screen() = default; // Общие методы, которые будут переопределяться virtual void handleEvent(const sf::Event& event) = 0; virtual void update(float deltaTime) = 0; virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const = 0; };
#include "Screen.h" #include <iostream> Screen::Screen(sf::RenderWindow& window, GameState& state, Transition& transition) : window(window), state(state), transition(transition), useTexture(false) { } void Screen::setupBackground(const std::string& textureId) { ResourceManager& rm = ResourceManager::getInstance(); sf::Texture& bgTexture = rm.getTexture(textureId); if (bgTexture.getSize().x == 0) { std::cerr << "Ошибка: Не удалось загрузить текстуру фона '" << textureId << "' из ResourceManager.\n"; useTexture = false; } else { useTexture = true; backgroundSprite.setTexture(bgTexture); backgroundSprite.setScale( static_cast<float>(window.getSize().x) / bgTexture.getSize().x, static_cast<float>(window.getSize().y) / bgTexture.getSize().y ); } }
Конструктор принимает ссылки на важнейшие объекты: окно, состояние игры и переходы.
initialize() — чисто виртуальный метод, который должен быть реализован в каждом дочернем классе. Здесь можно загрузить специфичные для экрана ресурсы или создать кнопки.
setupBackground() — универсальный метод для настройки фона. Он использует ResourceManager, чтобы получить текстуру по ID и корректно её от масштабировать под размер окна.
Все экраны обязаны реализовать три ключевых метода:
handleEvent() — обработка событий (нажатия мыши, клавиш и т.д.)
update() — логика обновления состояния экрана (анимации, проверка условий и пр.)
draw() — отрисовка всех элементов экрана.
Реализация класса ResourceManager с поддержкой ресурсов Windows
Одним из важнейших компонентов в любой игре или мультимедийном приложении является централизованное управление ресурсами. В этой главе мы рассмотрим реализацию класса ResourceManager
на C++ с использованием SFML и Windows API.
Класс ResourceManager отвечает за загрузку и хранение следующих типов ресурсов:
· sf::SoundBuffer — звуковые эффекты;
· sf::Music — музыкальные потоки (streaming).
Все ресурсы хранятся в памяти, загружаются из ресурсов Windows .exe
файла (раздел .rc
) и доступны по строковому идентификатору.
#pragma once #define IDR_TEXTURE1 102 // Изображение PNG #define IDR_TEXTURE2 103 // Изображение PNG #define IDR_TEXTURE3 104 // Изображение PNG #define IDR_TEXTURE4 105 // Изображение PNG #define IDR_TEXTURE5 106 // Изображение PNG #define IDR_FONT1 201 // Шрифт #define IDR_FONT2 202 // Шрифт #define IDR_SOUND1 301 // Звук #define IDR_ICON1 401 // Иконка
#include "resource.h" IDR_TEXTURE1 RCDATA "assets/puzzle.png" IDR_TEXTURE2 RCDATA "assets/background.png" IDR_TEXTURE3 RCDATA "assets/background1.png" IDR_TEXTURE4 RCDATA "assets/background2.png" IDR_TEXTURE5 RCDATA "assets/numbers.png" IDR_SOUND1 RCDATA "assets/music.wav" IDR_FONT1 RCDATA "assets/arial.ttf" IDR_FONT2 RCDATA "assets/glv.ttf" IDR_ICON1 ICON "assets/puzzle.ico"
#pragma once #include <SFML/Graphics.hpp> #include <SFML/Audio.hpp> #include <windows.h> #include <map> #include <string> #include <memory> class ResourceManager { private: // Хранилища для ресурсов // Хранилище для шрифтов std::map<std::string, sf::Font> fonts; // Хранилище для звуковых буферов std::map<std::string, sf::SoundBuffer> buffers; // Хранилище для текстур std::map<std::string, sf::Texture> textures; // Хранилище для музыкальных потоков std::map<std::string, std::unique_ptr<sf::Music>> musicStreams; // Приватный конструктор для реализации синглтона ResourceManager(); // Приватный деструктор ~ResourceManager() {} // Вспомогательный метод для загрузки ресурса template<typename T> bool loadResource(HRSRC resource, HGLOBAL handle, T& target); public: // Метод получения единственного экземпляра класса static ResourceManager& getInstance(); // Запрет копирования ResourceManager(const ResourceManager&) = delete; ResourceManager& operator=(const ResourceManager&) = delete; // Методы загрузки конкретных ресурсов bool loadFont(const std::string& id, int resourceId); bool loadSound(const std::string& id, int resourceId); bool loadTexture(const std::string& id, int resourceId); bool loadMusic(const std::string& id, int resourceId); // Геттеры для получения ресурсов по идентификатору sf::Font& getFont(const std::string& id); sf::SoundBuffer& getSoundBuffer(const std::string& id); sf::Texture& getTexture(const std::string& id); sf::Image getImage(const std::string& id); sf::Music& getMusic(const std::string& id); };
#include "ResourceManager.h" #include <iostream> // Конструктор по умолчанию ResourceManager::ResourceManager() {} // Шаблонная функция для загрузки ресурса из памяти в целевой объект (например, шрифт, текстура и т.д.) template<typename T> bool ResourceManager::loadResource(HRSRC resource, HGLOBAL handle, T& target) { if (!resource) { std::cerr << "Не удалось найти ресурс" << std::endl; return false; } // Загружаем ресурс в память handle = LoadResource(NULL, resource); if (!handle) { std::cerr << "Не удалось загрузить ресурс" << std::endl; return false; } // Получаем указатель на данные ресурса const void* data = LockResource(handle); // Получаем размер ресурса DWORD size = SizeofResource(NULL, resource); // Пытаемся загрузить ресурс в целевой объект (например, sf::Font или sf::Texture) return target.loadFromMemory(data, size); } // Возвращает единственный экземпляр ResourceManager (паттерн Singleton) ResourceManager& ResourceManager::getInstance() { static ResourceManager instance; return instance; } // Загружает шрифт из ресурса Windows с заданным ID и сохраняет его под указанным идентификатором bool ResourceManager::loadFont(const std::string& id, int resourceId) { // Находим ресурс шрифта в исполняемом файле HRSRC fontResource = FindResource(NULL, MAKEINTRESOURCE(resourceId), RT_RCDATA); HGLOBAL fontHandle = NULL; sf::Font font; if (!loadResource(fontResource, fontHandle, font)) { std::cerr << "Не удалось загрузить шрифт " << id << std::endl; return false; } // Сохраняем загруженный шрифт в хранилище fonts[id] = std::move(font); return true; } // Загружает звуковой буфер из ресурса Windows с заданным ID и сохраняет его под указанным идентификатором bool ResourceManager::loadSound(const std::string& id, int resourceId) { HRSRC soundResource = FindResource(NULL, MAKEINTRESOURCE(resourceId), RT_RCDATA); HGLOBAL soundHandle = NULL; sf::SoundBuffer buffer; if (!loadResource(soundResource, soundHandle, buffer)) { std::cerr << "Не удалось загрузить звук " << id << std::endl; return false; } // Сохраняем загруженный звуковой буфер в хранилище buffers[id] = std::move(buffer); return true; } // Загружает текстуру из ресурса Windows с заданным ID и сохраняет её под указанным идентификатором bool ResourceManager::loadTexture(const std::string& id, int resourceId) { HRSRC textureResource = FindResource(NULL, MAKEINTRESOURCE(resourceId), RT_RCDATA); HGLOBAL textureHandle = NULL; sf::Texture texture; if (!loadResource(textureResource, textureHandle, texture)) { std::cerr << "Не удалось загрузить текстуру " << id << std::endl; return false; } // Сохраняем загруженную текстуру в хранилище textures[id] = std::move(texture); return true; } // Загружает музыку из ресурса Windows с заданным ID и сохраняет её под указанным идентификатором bool ResourceManager::loadMusic(const std::string& id, int resourceId) { HRSRC musicResource = FindResource(NULL, MAKEINTRESOURCE(resourceId), RT_RCDATA); HGLOBAL musicHandle = NULL; if (!musicResource) { std::cerr << "Не удалось найти музыкальный ресурс " << id << std::endl; return false; } musicHandle = LoadResource(NULL, musicResource); if (!musicHandle) { std::cerr << "Не удалось загрузить музыкальный ресурс " << id << std::endl; return false; } // Получаем указатель на данные музыки и её размер const void* data = LockResource(musicHandle); DWORD size = SizeofResource(NULL, musicResource); // Создаём объект музыки и загружаем его из памяти auto music = std::make_unique<sf::Music>(); if (!music->openFromMemory(data, size)) { std::cerr << "Не удалось открыть музыкальный поток " << id << " из памяти" << std::endl; return false; } // Сохраняем музыкальный поток в хранилище musicStreams[id] = std::move(music); return true; } // Возвращает ссылку на шрифт по указанному ID sf::Font& ResourceManager::getFont(const std::string& id) { auto it = fonts.find(id); if (it == fonts.end()) { std::cerr << "Шрифт " << id << " не найден" << std::endl; static sf::Font emptyFont; // Статический пустой шрифт на случай ошибки return emptyFont; } return it->second; } // Возвращает ссылку на звуковой буфер по указанному ID sf::SoundBuffer& ResourceManager::getSoundBuffer(const std::string& id) { auto it = buffers.find(id); if (it == buffers.end()) { std::cerr << "Звуковой буфер " << id << " не найден" << std::endl; static sf::SoundBuffer emptyBuffer; return emptyBuffer; } return it->second; } // Возвращает ссылку на текстуру по указанному ID sf::Texture& ResourceManager::getTexture(const std::string& id) { auto it = textures.find(id); if (it == textures.end()) { std::cerr << "Текстура " << id << " не найдена" << std::endl; static sf::Texture emptyTexture; return emptyTexture; } return it->second; } // Возвращает копию изображения из текстуры по указанному ID sf::Image ResourceManager::getImage(const std::string& id) { auto it = textures.find(id); if (it != textures.end()) { return it->second.copyToImage(); // Копируем текстуру в изображение } std::cerr << "Текстура для изображения " << id << " не найдена" << std::endl; return sf::Image(); } // Возвращает ссылку на музыкальный поток по указанному ID sf::Music& ResourceManager::getMusic(const std::string& id) { auto it = musicStreams.find(id); if (it == musicStreams.end()) { std::cerr << "Музыкальный поток " << id << " не найден" << std::endl; static sf::Music emptyMusic; return emptyMusic; } return *(it->second); // Разыменовываем уникальный указатель, чтобы вернуть ссылку }
Кастомный класс Button для интерфейса
Интерфейс — важная часть любой игры. В этой главе мы реализуем гибкий класс Button
, полностью совместимый с SFML 2.x и современными возможностями C++20. Он предоставляет визуальные эффекты нажатия, возможность настройки цветов, шрифтов и размеров, а также безопасную обработку кликов.
Поддержка нажатия с визуальной анимацией.
Гибкая настройка цветов: фона, обводки, текста.
Обработка клика через sf::Vector2f (позиция мыши).
Использование concepts из C++20 для безопасных шаблонов.
Центрирование текста по кнопке.
Поддержка перемещения и сравнения (<=>).
#pragma once #include <SFML/Graphics.hpp> #include <string> #include <concepts> // Класс кнопки для SFML. Наследуется от sf::Drawable // и sf::Transformable для отрисовки и трансформаций. class Button : public sf::Drawable, public sf::Transformable { public: // Конструктор кнопки с полным набором параметров Button(std::wstring text, sf::Vector2f position, sf::Font& font, sf::Color buttonColor, sf::Color pressedColor, sf::Color textColor, sf::Color pressedTextColor, float width, float height, unsigned int fontSize, sf::Color outlineColor, float outlineThickness); // Конструкторы и операторы копирования/перемещения Button(const Button&) = default; Button& operator=(const Button&) = default; Button(Button&&) noexcept; Button& operator=(Button&&) noexcept; // Отрисовка кнопки на экране void draw(sf::RenderTarget& target, sf::RenderStates states) const override; // Проверка, был ли клик по кнопке [[nodiscard]] bool isClicked(sf::Vector2f mousePos) const noexcept; // Получение глобальных границ кнопки [[nodiscard]] sf::FloatRect getGlobalBounds() const noexcept; // Установка текста кнопки void setText(std::wstring text) noexcept; // Анимация при нажатии void pressAnimation() noexcept; // Анимация при отпускании void releaseAnimation() noexcept; // Установка цвета кнопки (с использованием concepts C++20) template<typename T> requires std::same_as<T, sf::Color> void setButtonColor(T color) noexcept; // Остальные настройки кнопки void setPressedColor(sf::Color color) noexcept; void setTextColor(sf::Color color) noexcept; void setPressedTextColor(sf::Color color) noexcept; void setSize(float width, float height) noexcept; void setFontSize(unsigned int size) noexcept; void setBackgroundTransparency(unsigned int alpha) noexcept; void setOutlineColor(sf::Color color) noexcept; void setOutlineThickness(float thickness) noexcept; // Трёхстороннее сравнение (spaceship operator) auto operator<=>(const Button& other) const = default; private: // Прямоугольная форма кнопки и текст sf::RectangleShape shape; sf::Text label; // Цвета: обычный, при нажатии, для текста и при нажатии текста sf::Color buttonColor; sf::Color pressedColor; sf::Color textColor; sf::Color pressedTextColor; // Оригинальные параметры: толщина границы и размер шрифта float originalOutlineThickness; unsigned int originalFontSize; // Обновление позиции текста по центру кнопки void updateTextPosition() noexcept; };
#include "Button.h" #include <ranges> // Конструктор с параметрами Button::Button(std::wstring text, sf::Vector2f position, sf::Font& font, sf::Color buttonColor, sf::Color pressedColor, sf::Color textColor, sf::Color pressedTextColor, float width, float height, unsigned int fontSize, sf::Color outlineColor, float outlineThickness) : buttonColor{ buttonColor }, pressedColor{ pressedColor }, textColor{ textColor }, pressedTextColor{ pressedTextColor }, originalOutlineThickness{ outlineThickness }, originalFontSize{ fontSize } { shape.setSize(sf::Vector2f{ width, height }); shape.setFillColor(buttonColor); shape.setOutlineColor(outlineColor); shape.setOutlineThickness(outlineThickness); shape.setPosition(position); label.setFont(font); label.setString(std::move(text)); label.setCharacterSize(fontSize); label.setFillColor(textColor); updateTextPosition(); } // Центрирует текст по центру кнопки void Button::updateTextPosition() noexcept { const auto textBounds = label.getLocalBounds(); label.setOrigin({ textBounds.left + textBounds.width / 2.0f, textBounds.top + textBounds.height / 2.0f }); const auto shapePos = shape.getPosition(); const auto shapeSize = shape.getSize(); label.setPosition({ shapePos.x + shapeSize.x / 2.0f, shapePos.y + shapeSize.y / 2.0f }); } // Проверка попадания курсора мыши в границы кнопки [[nodiscard]] bool Button::isClicked(sf::Vector2f mousePos) const noexcept { return shape.getGlobalBounds().contains(mousePos); } // Устанавливает новый текст void Button::setText(std::wstring text) noexcept { label.setString(std::move(text)); updateTextPosition(); } // Визуальная анимация при нажатии на кнопку void Button::pressAnimation() noexcept { shape.setFillColor(pressedColor); label.setFillColor(pressedTextColor); shape.setOutlineThickness(1.0f); // уменьшаем толщину обводки label.setCharacterSize(originalFontSize - 2); // уменьшаем размер текста updateTextPosition(); } // Визуальная анимация при отпускании кнопки void Button::releaseAnimation() noexcept { shape.setFillColor(buttonColor); label.setFillColor(textColor); shape.setOutlineThickness(originalOutlineThickness); label.setCharacterSize(originalFontSize); updateTextPosition(); } // Установка цвета кнопки (через шаблон с concept) template<typename T> requires std::same_as<T, sf::Color> void Button::setButtonColor(T color) noexcept { this->buttonColor = color; shape.setFillColor(color); } // Установка цвета при нажатии void Button::setPressedColor(sf::Color color) noexcept { this->pressedColor = color; } // Установка цвета текста void Button::setTextColor(sf::Color color) noexcept { this->textColor = color; label.setFillColor(color); } // Установка цвета текста при нажатии void Button::setPressedTextColor(sf::Color color) noexcept { this->pressedTextColor = color; } // Изменение размера кнопки void Button::setSize(float width, float height) noexcept { shape.setSize({ width, height }); updateTextPosition(); } // Изменение размера шрифта void Button::setFontSize(unsigned int size) noexcept { originalFontSize = size; label.setCharacterSize(size); updateTextPosition(); } // Установка прозрачности фона кнопки void Button::setBackgroundTransparency(unsigned int alpha) noexcept { buttonColor.a = static_cast<std::uint8_t>(std::clamp(alpha, 0u, 255u)); shape.setFillColor(buttonColor); } // Установка цвета обводки void Button::setOutlineColor(sf::Color color) noexcept { shape.setOutlineColor(color); } // Установка толщины обводки void Button::setOutlineThickness(float thickness) noexcept { originalOutlineThickness = thickness; shape.setOutlineThickness(thickness); } // Отрисовка кнопки и текста void Button::draw(sf::RenderTarget& target, sf::RenderStates states) const { states.transform.combine(getTransform()); target.draw(shape, states); target.draw(label, states); } // Получение границ кнопки (для коллизий/кликов) [[nodiscard]] sf::FloatRect Button::getGlobalBounds() const noexcept { return shape.getGlobalBounds(); } // Конструктор перемещения Button::Button(Button&& other) noexcept = default; // Оператор перемещения Button& Button::operator=(Button&& other) noexcept = default;
Анимированный разноцветный текст, класс ColorfulText
В этом разделе блога я хочу показать вам, как создать красивый, динамически меняющийся текст с цветной анимацией на библиотеке SFML. Такой текст отлично подходит для заставок, титулов, экранов загрузки или игровых меню. Мы создадим собственный класс ColorfulText, где каждая буква будет представлять собой отдельный объект sf::Text, меняющий цвет через определённый интервал времени.
Возможности данного класса
- окрашивает каждую букву в случайный цвет;
- периодически обновляет цвета;
- поддерживает позиционирование с центрированием.
#pragma once #include <SFML/Graphics.hpp> #include <vector> #include <string> #include <random> #include <memory> // Класс ColorfulText — отображает анимированный разноцветный текст class ColorfulText { public: // Конструктор ColorfulText(const std::string& fontId, std::wstring_view text, unsigned int charSize, const sf::Vector2f& position, const sf::RenderWindow& window, bool centerHorizontally = false, bool centerVertically = false); // Обновление цвета текста (вызывается в цикле игры) void update(float deltaTime); // Отрисовка текста на экране void draw(sf::RenderTarget& target, sf::RenderStates states = {}) const; // Установка новой позиции текста с возможностью центрирования void setPosition(const sf::Vector2f& position, bool centerHorizontally = false, bool centerVertically = false); private: // Обновление цветов букв на случайные void updateColors(); std::shared_ptr<sf::Font> font; // Шрифт std::vector<sf::Text> letters; // Вектор отдельных букв (sf::Text) float colorTimer; // Таймер для смены цвета static constexpr float COLOR_CHANGE_INTERVAL = 0.5f; // Интервал смены цвета };
Вместо одного объекта sf::Text, как обычно, мы создаём вектор letters, в котором каждая буква строки является отдельным объектом sf::Text. Это даёт нам гибкость при анимации — можно изменять цвет каждой буквы по отдельности.
#include "ColorfulText.h" #include "ResourceManager.h" #include <algorithm> #include <random> // Конструктор ColorfulText::ColorfulText(const std::string& fontId, std::wstring_view text, unsigned int charSize, const sf::Vector2f& position, const sf::RenderWindow& window, bool centerHorizontally, bool centerVertically) : colorTimer(0.0f) { // Получаем ссылку на синглтон ResourceManager ResourceManager& rm = ResourceManager::getInstance(); // Загружаем шрифт font = std::make_shared<sf::Font>(rm.getFont(fontId)); // Проверяем, загрузился ли шрифт if (font->getInfo().family.empty()) { throw std::runtime_error("Не удалось загрузить шрифт с ID: " + std::string(fontId) + " из ResourceManager"); } // Вычисляем общую ширину текста для центрирования float totalWidth = text.size() * charSize * 0.8f; float xPos = position.x; float yPos = position.y; if (centerHorizontally) { xPos = (window.getSize().x - totalWidth) / 2.0f; } if (centerVertically) { yPos = (window.getSize().y - charSize) / 2.0f; } // Создаем отдельные буквы и задаем им позицию и цвет letters.reserve(text.size()); for (size_t i = 0; i < text.size(); ++i) { sf::Text letter; letter.setFont(*font); letter.setString(text[i]); // Каждая буква отдельно letter.setCharacterSize(charSize); letter.setFillColor(sf::Color::Magenta); // Начальный цвет letter.setPosition(xPos + i * charSize * 0.8f, yPos); letters.push_back(letter); } } // Обновление — вызывается каждый кадр, меняет цвет по таймеру void ColorfulText::update(float deltaTime) { colorTimer += deltaTime; if (colorTimer >= COLOR_CHANGE_INTERVAL) { colorTimer = 0.0f; updateColors(); } } // Функция случайной смены цвета каждой буквы void ColorfulText::updateColors() { static std::random_device rd; static std::mt19937 gen(rd()); static std::uniform_int_distribution<int> dis(0, 255); for (auto& letter : letters) { letter.setFillColor(sf::Color( static_cast<sf::Uint8>(dis(gen)), // R static_cast<sf::Uint8>(dis(gen)), // G static_cast<sf::Uint8>(dis(gen)) // B )); } } // Отрисовка всех букв текста void ColorfulText::draw(sf::RenderTarget& target, sf::RenderStates states) const { for (const auto& letter : letters) { target.draw(letter, states); } } // Установка новой позиции текста с опциональным центрированием void ColorfulText::setPosition(const sf::Vector2f& position, bool centerHorizontally, bool centerVertically) { float totalWidth = letters.size() * letters[0].getCharacterSize() * 0.8f; float xPos = position.x; float yPos = position.y; if (centerHorizontally) { xPos = position.x - totalWidth / 2.0f; } if (centerVertically) { yPos = position.y - letters[0].getCharacterSize() / 2.0f; } // Обновляем позицию каждой буквы for (size_t i = 0; i < letters.size(); ++i) { letters[i].setPosition(xPos + i * letters[0].getCharacterSize() * 0.8f, yPos); } }
Плавный переход между экранами в игре, класс Transition
Переходы между игровыми экранами — важная часть UX. Они делают игру визуально цельной и приятной. В этом разделе мы реализуем двухфазный эффект затемнения и осветления — популярный способ перехода между состояниями игры с помощью чёрного оверлея.
Возможности данного класса:
- затухание экрана до чёрного (fade-out),
- смену состояния игры (например, меню → игра),
- проявление нового состояния (fade-in).
#pragma once #include <SFML/Graphics.hpp> #include "GameState.h" class Transition : public sf::Drawable, public sf::Transformable { public: Transition(sf::RenderWindow& window); // Конструктор с окном для динамического размера void startTransition(GameState newState); bool update(GameState& currentState, float deltaTime); // Добавляем deltaTime private: virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const override; sf::RectangleShape overlay; sf::RenderWindow& window; // Ссылка на окно для получения размера GameState targetState; float alpha; // Используем float для плавности bool transitioning; bool fadingIn; // Флаг для фазы fade-in float transitionDuration; // Длительность одной фазы };
#include "Transition.h" // Конструктор: инициализирует переход с заданным окном отрисовки Transition::Transition(sf::RenderWindow& window) : window(window), alpha(255.f), transitioning(false), fadingIn(false), transitionDuration(0.5f) { // Устанавливаем размер затемняющего прямоугольника равным размеру окна overlay.setSize(sf::Vector2f(static_cast<float>(window.getSize().x), static_cast<float>(window.getSize().y))); // Цвет прямоугольника — чёрный с максимальной непрозрачностью (начальное состояние — экран полностью закрыт) overlay.setFillColor(sf::Color(0, 0, 0, static_cast<sf::Uint8>(alpha))); } // Запускает переход к новому состоянию игры (например, из меню в игру) void Transition::startTransition(GameState newState) { transitioning = true; // Начинаем переход fadingIn = false; // Сначала будет фейд-аут (затемнение) targetState = newState; // Сохраняем целевое состояние игры alpha = 255.f; // Сбрасываем прозрачность // Обновляем цвет оверлея, чтобы он был полностью непрозрачным overlay.setFillColor(sf::Color(0, 0, 0, static_cast<sf::Uint8>(alpha))); } // Обновляет состояние перехода (анимация затухания / появления) // Возвращает true, если переход всё ещё активен bool Transition::update(GameState& currentState, float deltaTime) { if (transitioning) { if (!fadingIn) { // Фаза затухания (экран темнеет) alpha -= (255.f / transitionDuration) * deltaTime; if (alpha <= 0.f) { alpha = 0.f; fadingIn = true; // Переходим к фазе проявления currentState = targetState; // Меняем текущее состояние игры } } else { // Фаза проявления (экран светлеет) alpha += (255.f / transitionDuration) * deltaTime; if (alpha >= 255.f) { alpha = 255.f; transitioning = false; // Переход завершён } } // Обновляем прозрачность оверлея // Используется 255.f - alpha, чтобы получить эффект "появления" после затемнения overlay.setFillColor(sf::Color(0, 0, 0, static_cast<sf::Uint8>(255.f - alpha))); return true; // Переход всё ещё активен } return false; // Переход завершён } // Отрисовывает оверлей (черный прямоугольник) поверх текущего содержимого окна void Transition::draw(sf::RenderTarget& target, sf::RenderStates states) const { if (transitioning) { target.draw(overlay, states); // Рисуем затемняющий слой, если переход активен } }
Реализация игры “Пятнашки” в классе FifteenPuzzle, который наследуется от sf::Drawable для отрисовки.
#pragma once #include <SFML/Graphics.hpp> #include <vector> #include <memory> #include <string> // Класс, реализующий игру "Пятнашки", наследуется от sf::Drawable для отрисовки class FifteenPuzzle : public sf::Drawable { public: // Константы по умолчанию для размера плитки и сетки static constexpr int DEFAULT_TILE_SIZE = 100; static constexpr int DEFAULT_GRID_SIZE = 4; // Конструктор с параметрами размера плитки и сетки explicit FifteenPuzzle(int tileSize = DEFAULT_TILE_SIZE, int gridSize = DEFAULT_GRID_SIZE); // Деструктор по умолчанию ~FifteenPuzzle() = default; // Запрет копирования для предотвращения дублирования ресурсов FifteenPuzzle(const FifteenPuzzle&) = delete; FifteenPuzzle& operator=(const FifteenPuzzle&) = delete; // Разрешено перемещение для поддержки семантики перемещения FifteenPuzzle(FifteenPuzzle&&) = default; FifteenPuzzle& operator=(FifteenPuzzle&&) = default; // Установка позиции пазла на экране void setPosition(float x, float y); // Получение текущей позиции пазла sf::Vector2f getPosition() const; // Получение границ пазла (для обработки событий) sf::FloatRect getBounds() const; // Обработка событий (например, кликов мыши) void handleEvent(const sf::Event& event, const sf::RenderWindow& window); // Обновление состояния игры (заглушка для возможных анимаций) void update(); // Проверка, решён ли пазл bool isSolved() const; // Перезапуск игры (перемешивание плиток) void restart(); private: const int m_tileSize; // Размер одной плитки (в пикселях) const int m_gridSize; // Размер сетки (например, 4x4) std::vector<std::vector<int>> m_board; // Двумерный массив, представляющий игровое поле std::shared_ptr<sf::Font> m_font; // Шрифт для отображения номеров плиток int m_emptyX, m_emptyY; // Координаты пустой плитки sf::Vector2f m_position; // Позиция пазла на экране bool m_isSolved; // Флаг, указывающий, решён ли пазл // Перемешивание плиток для создания новой игры void shuffleBoard(); // Проверка, можно ли переместить плитку на указанные координаты bool canMove(int x, int y) const; // Перемещение плитки на пустое место void moveTile(int x, int y); // Проверка, является ли текущая конфигурация решаемой bool isSolvable(const std::vector<int>& numbers) const; // Метод отрисовки пазла (переопределение sf::Drawable) virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const override; };
- Константы: DEFAULT_TILE_SIZE (100 пикселей) и DEFAULT_GRID_SIZE (4x4) задают размер плиток и сетки по умолчанию.
- Запрет копирования: Конструктор копирования и оператор присваивания отключены, чтобы избежать дублирования ресурсов.
- Поддержка перемещения: Конструктор перемещения и оператор присваивания включены для эффективного управления ресурсами.
- Основные методы: Методы вроде
handleEvent
,update
,isSolved
иrestart
управляют игровым процессом, аdraw
отвечает за отрисовку.
#include "FifteenPuzzle.h" #include <numeric> #include <random> #include <stdexcept> #include "ResourceManager.h" // Конструктор: инициализация игры с заданными параметрами FifteenPuzzle::FifteenPuzzle(int tileSize, int gridSize) : m_tileSize(tileSize) , m_gridSize(gridSize) , m_board(gridSize, std::vector<int>(gridSize, 0)) , m_font(std::make_shared<sf::Font>()) , m_emptyX(0) , m_emptyY(0) , m_position(0.f, 0.f) , m_isSolved(false) { // Получение экземпляра менеджера ресурсов (паттерн Singleton) ResourceManager& rm = ResourceManager::getInstance(); // Загрузка шрифта из менеджера ресурсов *m_font = rm.getFont("font"); // Проверка успешной загрузки шрифта if (m_font->getInfo().family.empty()) { throw std::runtime_error("Не удалось загрузить шрифт 'font' из ResourceManager"); } // Инициализация игрового поля shuffleBoard(); } // Установка позиции пазла на экране void FifteenPuzzle::setPosition(float x, float y) { m_position.x = x; m_position.y = y; } // Получение текущей позиции пазла sf::Vector2f FifteenPuzzle::getPosition() const { return m_position; } // Получение прямоугольника, ограничивающего пазл sf::FloatRect FifteenPuzzle::getBounds() const { return sf::FloatRect(m_position.x, m_position.y, m_tileSize * m_gridSize, m_tileSize * m_gridSize); } // Проверка, является ли текущая конфигурация пазла решаемой bool FifteenPuzzle::isSolvable(const std::vector<int>& numbers) const { // Подсчёт инверсий в последовательности чисел int inversions = 0; for (size_t i = 0; i < numbers.size(); ++i) { for (size_t j = i + 1; j < numbers.size(); ++j) { if (numbers[i] && numbers[j] && numbers[i] > numbers[j]) { ++inversions; } } } // Для нечётного размера сетки: чётное число инверсий => решаемо if (m_gridSize % 2 == 1) { return inversions % 2 == 0; } // Для чётного размера сетки: учитываем позицию пустой плитки else { int emptyRowFromBottom = m_gridSize - m_emptyY; if (emptyRowFromBottom % 2 == 0) { return inversions % 2 == 1; } else { return inversions % 2 == 0; } } } // Перемешивание игрового поля для новой игры void FifteenPuzzle::shuffleBoard() { // Создание последовательности чисел от 1 до gridSize*gridSize-1, с 0 в конце std::vector<int> numbers(m_gridSize * m_gridSize); std::iota(numbers.begin(), numbers.end(), 1); numbers.back() = 0; // Инициализация генератора случайных чисел std::random_device rd; std::mt19937 g(rd()); // Перемешивание до получения решаемой конфигурации do { std::shuffle(numbers.begin(), numbers.end(), g); // Заполнение игрового поля for (int i = 0; i < m_gridSize; ++i) { for (int j = 0; j < m_gridSize; ++j) { m_board[i][j] = numbers[i * m_gridSize + j]; if (m_board[i][j] == 0) { m_emptyX = j; m_emptyY = i; } } } } while (!isSolvable(numbers)); // Сброс флага решения m_isSolved = false; } // Проверка, можно ли переместить плитку на указанные координаты bool FifteenPuzzle::canMove(int x, int y) const { // Плитка должна быть соседней с пустой и находиться в пределах поля return (std::abs(x - m_emptyX) + std::abs(y - m_emptyY) == 1) && (x >= 0 && x < m_gridSize && y >= 0 && y < m_gridSize); } // Перемещение плитки на пустое место void FifteenPuzzle::moveTile(int x, int y) { if (canMove(x, y)) { // Обмен плитки с пустым местом std::swap(m_board[m_emptyY][m_emptyX], m_board[y][x]); m_emptyX = x; m_emptyY = y; // Проверка, решён ли пазл после хода m_isSolved = isSolved(); } } // Проверка, решён ли пазл bool FifteenPuzzle::isSolved() const { int expected = 1; for (int i = 0; i < m_gridSize; ++i) { for (int j = 0; j < m_gridSize; ++j) { if (i == m_gridSize - 1 && j == m_gridSize - 1) { return m_board[i][j] == 0; } if (m_board[i][j] != expected++) { return false; } } } return true; } // Обработка событий (например, кликов мыши) void FifteenPuzzle::handleEvent(const sf::Event& event, const sf::RenderWindow& window) { if (event.type == sf::Event::MouseButtonPressed && event.mouseButton.button == sf::Mouse::Left) { // Преобразование координат мыши в мировые координаты sf::Vector2f mousePos = window.mapPixelToCoords( sf::Vector2i(event.mouseButton.x, event.mouseButton.y)); // Проверка, находится ли клик внутри пазла if (getBounds().contains(mousePos)) { // Вычисление координат плитки int x = static_cast<int>((mousePos.x - m_position.x) / m_tileSize); int y = static_cast<int>((mousePos.y - m_position.y) / m_tileSize); moveTile(x, y); } } } // Обновление состояния игры (пока не используется) void FifteenPuzzle::update() { // Здесь можно добавить анимации или другие обновления } // Отрисовка пазла void FifteenPuzzle::draw(sf::RenderTarget& target, sf::RenderStates states) const { // Смещение координат для учёта позиции пазла states.transform.translate(m_position); // Отрисовка каждой плитки for (int i = 0; i < m_gridSize; ++i) { for (int j = 0; j < m_gridSize; ++j) { if (m_board[i][j] != 0) { // Отри Clay плитки sf::RectangleShape rect(sf::Vector2f(m_tileSize - 2, m_tileSize - 2)); rect.setPosition(j * m_tileSize + 1, i * m_tileSize + 1); rect.setFillColor(sf::Color(64, 224, 208)); // Бирюзовый цвет target.draw(rect, states); // Отрисовка номера плитки if (m_font) { sf::Text text; text.setFont(*m_font); text.setString(std::to_string(m_board[i][j])); text.setCharacterSize(m_tileSize / 2); text.setFillColor(sf::Color::Black); // Центрирование текста auto textRect = text.getLocalBounds(); text.setOrigin(textRect.width / 2.0f, textRect.height / 2.0f); text.setPosition(j * m_tileSize + m_tileSize / 2.0f, i * m_tileSize + m_tileSize / 2.5f); target.draw(text, states); } } } } } // Перезапуск игры (перемешивание плиток) void FifteenPuzzle::restart() { shuffleBoard(); }
Инициализация
В конструкторе создаётся игровое поле (m_board) как двумерный вектор размером gridSize x gridSize. Шрифт для номеров плиток загружается через ResourceManager (паттерн Singleton). Затем вызывается shuffleBoard() для создания начальной конфигурации.
Перемешивание
Метод shuffleBoard() генерирует случайную последовательность чисел от 1 до gridSize*gridSize-1 с 0 в конце (пустая плитка). Используется std::shuffle с генератором случайных чисел std::mt19937. Важно, что метод isSolvable проверяет, является ли конфигурация решаемой, подсчитывая инверсии и учитывая позицию пустой плитки.
Обработка кликов
Метод handleEvent обрабатывает клики мыши. Координаты клика преобразуются в индексы плитки, и если плитка соседствует с пустой, вызывается moveTile для её перемещения.
Отрисовка
Метод draw отрисовывает каждую плитку как прямоугольник (sf::RectangleShape) бирюзового цвета с номером, отцентрированным с помощью sf::Text. Пустая плитка (0) не отрисовывается.
Проверка решения
Метод isSolved проверяет, расположены ли плитки в порядке от 1 до 15 с пустой плиткой в правом нижнем углу.
Создаём главное меню для игры "Пятнашки", класс MainMenu
Класс MainMenu отвечает за отображение и обработку главного меню игры. Оно включает заголовок "Пятнашки" и пять кнопок: "Играть", "Правила", "Музыка: Вкл/Выкл", "Рестарт" и "Выход". Меню использует SFML для отрисовки, поддерживает анимации кнопок и фоновую музыку. Код организован так, чтобы быть понятным и легко расширяемым.
Класс MainMenu наследуется от базового класса Screen, что позволяет интегрировать его в систему управления состояниями игры.
#pragma once #include <SFML/Graphics.hpp> #include <SFML/Audio.hpp> #include "Button.h" #include "GameState.h" #include "Transition.h" #include "ColorfulText.h" #include <memory> #include "GameScreen.h" #include "Screen.h" class MainMenu : public Screen { private: // Элементы интерфейса главного меню — все они уникальные указатели для удобного управления памятью std::unique_ptr<ColorfulText> titleText; // Заголовок меню с цветной анимацией std::unique_ptr<Button> playButton; // Кнопка "Играть" std::unique_ptr<Button> rulesButton; // Кнопка "Правила" std::unique_ptr<Button> musicButton; // Кнопка включения/выключения музыки std::unique_ptr<Button> restartButton; // Кнопка "Перезапустить" (может использоваться в других состояниях) std::unique_ptr<Button> exitButton; // Кнопка "Выход" sf::Music* music; // Указатель на объект музыки (не владеем им, просто ссылка) GameScreen& gameScreen; // Ссылка на игровой экран для перехода к игре bool isMusicOn; // Флаг: включена ли музыка // Метод для инициализации элементов интерфейса меню void initialize() override; public: // Конструктор: принимает окно отрисовки, текущее состояние игры, переход и игровой экран MainMenu(sf::RenderWindow& window, GameState& state, Transition& transition, GameScreen& gameScreen); // Обработка событий (нажатия мыши, клавиш и т.д.) void handleEvent(const sf::Event& event) override; // Обновление логики меню (анимация, проверка нажатий и т.д.) void update(float deltaTime) override; // Отрисовка всех элементов меню void draw(sf::RenderTarget& target, sf::RenderStates states) const override; };
- Поля: Указатели std::unique_ptr на заголовок (ColorfulText) и кнопки (Button) обеспечивают автоматическое управление памятью. Переменная isMusicOn отслеживает состояние музыки.
- Зависимости: Класс использует sf::Music для фоновой музыки, GameScreen для перезапуска игры и Transition для переключения между экранами.
- Методы: initialize настраивает элементы меню, handleEvent обрабатывает клики, update обновляет анимации, а draw отрисовывает меню.
#include "MainMenu.h" #include <stdexcept> // Константы для расположения и размеров элементов интерфейса constexpr float BUTTON_WIDTH = 500.0f; // Ширина кнопок constexpr float BUTTON_HEIGHT = 50.0f; // Высота кнопок constexpr float SPACING = 30.0f; // Расстояние между кнопками constexpr float TITLE_Y_POS = 50.0f; // Вертикальная позиция заголовка // Конструктор: инициализирует главное меню и вызывает метод initialize() MainMenu::MainMenu(sf::RenderWindow& window, GameState& state, Transition& transition, GameScreen& gameScreen) : Screen(window, state, transition), gameScreen(gameScreen), isMusicOn(true) { initialize(); // Инициализация всех элементов интерфейса } // Метод инициализации главного меню void MainMenu::initialize() { ResourceManager& rm = ResourceManager::getInstance(); // Получаем шрифты из менеджера ресурсов sf::Font& font = rm.getFont("font"); sf::Font& font2 = rm.getFont("font1"); // Проверяем, успешно ли загружены шрифты if (font.getInfo().family.empty() || font2.getInfo().family.empty()) { throw std::runtime_error("Не удалось получить шрифты из ResourceManager"); } // Устанавливаем фоновое изображение setupBackground("background"); // Создаем анимированный цветной заголовок "Пятнашки" titleText = std::make_unique<ColorfulText>( "font1", L"Пятнашки", 100, sf::Vector2f(0, TITLE_Y_POS), window, true, false); // Рассчитываем начальную Y-координату для размещения кнопок по центру экрана float startY = window.getSize().y / 2.0f - (5 * BUTTON_HEIGHT + 4 * SPACING) / 2.0f + 50.0f; float centerX = (window.getSize().x - BUTTON_WIDTH) / 2.0f; // Создаем все кнопки меню с заданными параметрами стиля и расположения playButton = std::make_unique<Button>( L"Играть", sf::Vector2f(centerX, startY), font, sf::Color(255, 192, 203, 150), sf::Color(255, 192, 203, 200), sf::Color::Blue, sf::Color(128, 128, 128), BUTTON_WIDTH, BUTTON_HEIGHT, 30, sf::Color::Magenta, 5.0f); rulesButton = std::make_unique<Button>( L"Правила", sf::Vector2f(centerX, startY + BUTTON_HEIGHT + SPACING), font, sf::Color(255, 192, 203, 150), sf::Color(255, 192, 203, 200), sf::Color::Blue, sf::Color(128, 128, 128), BUTTON_WIDTH, BUTTON_HEIGHT, 30, sf::Color::Magenta, 5.0f); musicButton = std::make_unique<Button>( L"Музыка: Вкл", sf::Vector2f(centerX, startY + 2 * (BUTTON_HEIGHT + SPACING)), font, sf::Color(255, 192, 203, 150), sf::Color(255, 192, 203, 200), sf::Color::Blue, sf::Color(128, 128, 128), BUTTON_WIDTH, BUTTON_HEIGHT, 30, sf::Color::Magenta, 5.0f); restartButton = std::make_unique<Button>( L"Рестарт", sf::Vector2f(centerX, startY + 3 * (BUTTON_HEIGHT + SPACING)), font, sf::Color(255, 192, 203, 150), sf::Color(255, 192, 203, 200), sf::Color::Blue, sf::Color(128, 128, 128), BUTTON_WIDTH, BUTTON_HEIGHT, 30, sf::Color::Magenta, 5.0f); exitButton = std::make_unique<Button>( L"Выход", sf::Vector2f(centerX, startY + 4 * (BUTTON_HEIGHT + SPACING)), font, sf::Color(255, 192, 203, 150), sf::Color(255, 192, 203, 200), sf::Color::Blue, sf::Color(128, 128, 128), BUTTON_WIDTH, BUTTON_HEIGHT, 30, sf::Color::Magenta, 5.0f); // Получаем ссылку на музыку из менеджера ресурсов music = &rm.getMusic("music"); // Проверяем, что музыка загружена и готова к воспроизведению if (music->getStatus() == sf::Music::Stopped && music->getDuration() == sf::Time::Zero) { throw std::runtime_error("Музыкальный поток не инициализирован в ResourceManager"); } // Настраиваем зацикленное воспроизведение и запускаем музыку music->setLoop(true); music->play(); } // Обновляет логику меню — в данном случае только анимацию заголовка void MainMenu::update(float deltaTime) { titleText->update(deltaTime); // Анимация цвета заголовка } // Обрабатывает события от пользователя (например, нажатие мыши) void MainMenu::handleEvent(const sf::Event& event) { // Получаем текущую позицию курсора sf::Vector2f mousePos(static_cast<float>(sf::Mouse::getPosition(window).x), static_cast<float>(sf::Mouse::getPosition(window).y)); // Если произошло нажатие мыши if (event.type == sf::Event::MouseButtonPressed) { if (playButton->isClicked(mousePos)) playButton->pressAnimation(); if (rulesButton->isClicked(mousePos)) rulesButton->pressAnimation(); if (musicButton->isClicked(mousePos)) musicButton->pressAnimation(); if (restartButton->isClicked(mousePos)) restartButton->pressAnimation(); if (exitButton->isClicked(mousePos)) exitButton->pressAnimation(); } // Если кнопка мыши отпущена if (event.type == sf::Event::MouseButtonReleased) { if (playButton->isClicked(mousePos)) { playButton->releaseAnimation(); transition.startTransition(GameState::GAME); // Переход к игре } if (rulesButton->isClicked(mousePos)) { rulesButton->releaseAnimation(); transition.startTransition(GameState::RULES); // Переход к правилам } if (musicButton->isClicked(mousePos)) { musicButton->releaseAnimation(); isMusicOn = !isMusicOn; // Переключаем состояние музыки musicButton->setText(isMusicOn ? L"Музыка: Вкл" : L"Музыка: Выкл"); // Обновляем текст кнопки if (isMusicOn && music->getStatus() != sf::Music::Playing) { music->play(); // Включаем музыку } else if (!isMusicOn) { music->stop(); // Выключаем музыку } } if (restartButton->isClicked(mousePos)) { restartButton->releaseAnimation(); gameScreen.restartGame(); // Перезапускаем игру transition.startTransition(GameState::GAME); // Переходим в игру } if (exitButton->isClicked(mousePos)) { exitButton->releaseAnimation(); window.close(); // Закрываем окно приложения } } } // Отрисовывает всё содержимое главного меню void MainMenu::draw(sf::RenderTarget& target, sf::RenderStates states) const { // Если фоновая текстура установлена — рисуем её if (useTexture) { target.draw(backgroundSprite, states); } else { // Иначе рисуем однотонный фон (в данном случае голубой) sf::RectangleShape background(sf::Vector2f( static_cast<float>(window.getSize().x), static_cast<float>(window.getSize().y))); background.setFillColor(sf::Color::Cyan); target.draw(background, states); } // Отрисовываем заголовок titleText->draw(target, states); // Отрисовываем все кнопки target.draw(*playButton, states); target.draw(*rulesButton, states); target.draw(*musicButton, states); target.draw(*restartButton, states); target.draw(*exitButton, states); }
Создание игрового окна, класс GameScreen
- отрисовку поля игры,
- анимации и интерфейс,
- таймер прохождения,
- обработку событий,
- и даже отображение сообщения о победе.
Класс GameScreen — это наследник базового класса Screen, который управляет визуальными и логическими элементами экрана. Он включает следующие компоненты:
- FifteenPuzzle — основная игровая логика и поле;
- Button — кнопка возврата в меню;
- ColorfulText — анимированный заголовок "Пятнашки";
- sf::Text, sf::RectangleShape — для уведомлений и таймера;
- sf::Clock — измерение времени прохождения;
- флаг isGameStarted — определяет, запущена ли игра.
#pragma once #include "Screen.h" #include "FifteenPuzzle.h" #include "Button.h" #include "ColorfulText.h" class GameScreen : public Screen { private: // Указатель на объект головоломки "Пятнашки" std::unique_ptr<FifteenPuzzle> puzzle; // Кнопка выхода в меню std::unique_ptr<Button> exitButton; // Цветной анимированный заголовок "Пятнашки" std::unique_ptr<ColorfulText> titleText; // Текстовое поле для отображения сообщений (например, победы) sf::Text text; // Прямоугольник-фон под текстовые уведомления sf::RectangleShape textBackground; // Текст таймера, показывающий время игры sf::Text timerText; // Фон под таймер sf::RectangleShape timerBackground; // Часы для измерения времени игры sf::Clock gameClock; // Флаг, указывающий, начал ли пользователь игру bool isGameStarted; // Метод инициализации элементов интерфейса void initialize() override; public: /* Конструктор. Инициализирует игровой экран, связывает его с окном, состоянием и системой переходов. */ GameScreen(sf::RenderWindow& window, GameState& state, Transition& transition); /* Обработка событий (нажатия клавиш, клики мыши и др.). */ void handleEvent(const sf::Event& event) override; /* Отрисовка всех элементов экрана: головоломки, кнопок, текста и фона. */ void draw(sf::RenderTarget& target, sf::RenderStates states) const override; /* Обновление состояния экрана (анимация заголовка, обновление таймера и т.д.). */ void update(float deltaTime) override; /* Перезапуск игры: сброс позиций плиток, таймера, очистка сообщений. */ void restartGame(); };
#include "GameScreen.h" #include <iostream> #include <string> // Константа для позиции Y заголовка constexpr float TITLE_Y_POS = 15.0f; /* Конструктор класса GameScreen. Инициализирует игровой экран, связывает его с окном и системой состояний. */ GameScreen::GameScreen(sf::RenderWindow& window, GameState& state, Transition& transition) : Screen(window, state, transition), puzzle(std::make_unique<FifteenPuzzle>(100, 4)), // Создаем головоломку 4x4 с размером плитки 100 isGameStarted(false) { // Игра ещё не начата initialize(); // Вызываем метод инициализации графических элементов } /* Метод инициализации всех графических элементов интерфейса: - Кнопка выхода в меню - Текстовые поля: сообщение о победе, таймер - Заголовок игры */ void GameScreen::initialize() { ResourceManager& rm = ResourceManager::getInstance(); sf::Font& font = rm.getFont("font"); if (font.getInfo().family.empty()) { throw std::runtime_error("Не удалось загрузить шрифт 'font' из ResourceManager"); } setupBackground("background1"); // Устанавливаем фоновое изображение // Создаем кнопку "меню" с заданными стилями и размерами exitButton = std::make_unique<Button>( std::wstring(L"м е н ю"), sf::Vector2f(445, 600), font, sf::Color(64, 224, 208, 255), sf::Color(64, 224, 208, 255), sf::Color::Black, sf::Color(128, 128, 128), 395.0f, 50.0f, 30u, sf::Color::Black, 4.0f ); // Устанавливаем шрифт для текстового уведомления (например, победы) text.setFont(font); // Центрируем головоломку по горизонтали, вертикальная позиция — фиксированная puzzle->setPosition( (window.getSize().x - puzzle->getBounds().width) / 2.0f, 150.0f ); // Цветной анимированный заголовок "Пятнашки" titleText = std::make_unique<ColorfulText>("font1", L"Пятнашки", 100, sf::Vector2f(0, TITLE_Y_POS), window, true, false); // Фон под текстовые сообщения (полупрозрачный белый прямоугольник) textBackground.setFillColor(sf::Color(255, 255, 255, 200)); // Настройка текста таймера timerText.setFont(font); timerText.setCharacterSize(45); timerText.setFillColor(sf::Color::Red); timerText.setPosition( puzzle->getBounds().left + puzzle->getBounds().width + 50.0f, puzzle->getBounds().top + 200.0f ); // Подложка под таймер (с полупрозрачным фоном) timerBackground.setFillColor(sf::Color(255, 255, 255, 128)); sf::FloatRect timerRect = timerText.getLocalBounds(); timerBackground.setSize(sf::Vector2f(timerRect.width + 20.0f, timerRect.height + 40.0f)); timerBackground.setPosition( timerText.getPosition().x - 10.0f, timerText.getPosition().y ); } /* Обработка событий от пользователя: - Нажатие Esc - Клик мыши на кнопке выхода - Перемещение плиток - Начало игры - Перезапуск */ void GameScreen::handleEvent(const sf::Event& event) { // Выход в меню при нажатии Esc if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape) { transition.startTransition(GameState::MENU); } // Обработка нажатия на кнопку "меню" if (event.type == sf::Event::MouseButtonPressed) { sf::Vector2f mousePos = window.mapPixelToCoords(sf::Mouse::getPosition(window)); if (exitButton->isClicked(mousePos)) { exitButton->pressAnimation(); // Анимация нажатия } } // Обработка отпускания кнопки мыши if (event.type == sf::Event::MouseButtonReleased) { sf::Vector2f mousePos = window.mapPixelToCoords(sf::Mouse::getPosition(window)); if (exitButton->isClicked(mousePos)) { exitButton->releaseAnimation(); // Анимация отпускания transition.startTransition(GameState::MENU); // Переход в меню } } // Старт игры при первом взаимодействии (нажатие клавиши или клик) if (!isGameStarted && (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::KeyPressed)) { isGameStarted = true; gameClock.restart(); // Запускаем таймер } // Передаем событие головоломке (например, клик на плитку) puzzle->handleEvent(event, window); // Проверяем, решена ли головоломка if (puzzle->isSolved() && isGameStarted) { isGameStarted = false; // Получаем время прохождения sf::Time elapsed = gameClock.getElapsedTime(); int minutes = static_cast<int>(elapsed.asSeconds()) / 60; int seconds = static_cast<int>(elapsed.asSeconds()) % 60; int milliseconds = static_cast<int>(elapsed.asMilliseconds()) % 1000; // Форматируем строку времени std::wstring timeString = std::wstring(L"Время решения: ") + (minutes < 10 ? L"0" : L"") + std::to_wstring(minutes) + L":" + (seconds < 10 ? L"0" : L"") + std::to_wstring(seconds) + L":" + (milliseconds < 100 ? L"0" : (milliseconds < 10 ? L"00" : L"")) + std::to_wstring(milliseconds); // Отображаем сообщение о победе text.setString(L" Победа!!!\n" + timeString + L"\n" + L"Нажмите Esc или кнопку меню!"); text.setCharacterSize(50); text.setFillColor(sf::Color::Red); // Центрируем текст по окну sf::FloatRect textRect = text.getLocalBounds(); text.setOrigin(textRect.left + textRect.width / 2.0f, textRect.top + textRect.height / 2.0f); text.setPosition(window.getSize().x / 2.0f, window.getSize().y / 2.0f); // Настраиваем фон под текст textBackground.setSize(sf::Vector2f(textRect.width + 40.0f, textRect.height + 40.0f)); textBackground.setOrigin(textBackground.getSize().x / 2.0f, textBackground.getSize().y / 2.0f); textBackground.setPosition(window.getSize().x / 2.0f, window.getSize().y / 2.0f); // Скрываем таймер после победы timerText.setString(L""); timerBackground.setSize(sf::Vector2f(0, 0)); } // Перезапуск игры при нажатии R if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::R) { restartGame(); } } /* Отрисовка всех элементов экрана: - Фон - Головоломка - Сообщения (победа) - Таймер - Кнопки - Заголовок */ void GameScreen::draw(sf::RenderTarget& target, sf::RenderStates states) const { target.clear(sf::Color::White); // Очищаем экран // Рисуем фон, если он есть if (useTexture) { target.draw(backgroundSprite, states); } target.draw(*puzzle, states); // Рисуем головоломку target.draw(textBackground, states); // Фон под текстом target.draw(text, states); // Текст сообщений target.draw(timerBackground, states); // Фон под таймером target.draw(timerText, states); // Текст таймера target.draw(*exitButton, states); // Кнопка выхода titleText->draw(target, states); // Анимированный заголовок } /* Перезапуск игры: - Сброс плиток в случайное положение - Сброс таймера - Очистка текстовых сообщений - Восстановление таймера */ void GameScreen::restartGame() { puzzle->restart(); // Перемешиваем головоломку isGameStarted = false; // Сбрасываем флаг начала игры gameClock.restart(); // Сбрасываем таймер text.setString(L""); // Очищаем текст победы text.setCharacterSize(30); // Сбрасываем размер текста textBackground.setSize(sf::Vector2f(0, 0)); // Скрываем фон под текстом // Восстанавливаем начальное значение таймера timerText.setString(L"Время: 00:00:000"); sf::FloatRect timerRect = timerText.getLocalBounds(); timerBackground.setSize(sf::Vector2f(timerRect.width + 20.0f, timerRect.height + 40.0f)); timerBackground.setPosition( timerText.getPosition().x - 10.0f, timerText.getPosition().y ); } /* Обновление состояния экрана: - Анимация заголовка - Обновление таймера, если игра активна */ void GameScreen::update(float deltaTime) { titleText->update(deltaTime); // Обновляем анимацию цветного заголовка if (isGameStarted) { sf::Time elapsed = gameClock.getElapsedTime(); int minutes = static_cast<int>(elapsed.asSeconds()) / 60; int seconds = static_cast<int>(elapsed.asSeconds()) % 60; int milliseconds = static_cast<int>(elapsed.asMilliseconds()) % 1000; // Форматируем строку времени std::wstring timerString = std::wstring(L"Время: ") + (minutes < 10 ? L"0" : L"") + std::to_wstring(minutes) + L":" + (seconds < 10 ? L"0" : L"") + std::to_wstring(seconds) + L":" + (milliseconds < 100 ? L"0" : (milliseconds < 10 ? L"00" : L"")) + std::to_wstring(milliseconds); timerText.setString(timerString); // Обновляем размер фона под таймер sf::FloatRect timerRect = timerText.getLocalBounds(); timerBackground.setSize(sf::Vector2f(timerRect.width + 20.0f, timerRect.height + 20.0f)); timerBackground.setPosition( timerText.getPosition().x - 10.0f, timerText.getPosition().y ); } }
Перечисление GameState
#pragma once enum class GameState { MENU, GAME, RULES };
Это перечисление описывает три возможных состояния игры:
Оно используется для управления переходами между экранами и состояниями.
Окно "Правила игры", класс RulesScreen
Класс RulesScreen — это экран с правилами игры. Он помогает игроку быстро понять суть и цель головоломки перед началом игры.
- анимированный заголовок;
- текст с пошаговыми правилами;
- пример изображения правильно собранного поля;
- кнопку возврата в главное меню.
#pragma once #include "Screen.h" #include "Button.h" #include "ColorfulText.h" /* Класс RulesScreen представляет экран с правилами игры. Отображает описание правил игры "Пятнашки", содержит анимированный заголовок, изображение игрового поля и кнопку возврата в главное меню. */ class RulesScreen : public Screen { private: // Анимированный цветной заголовок "Правила" std::unique_ptr<ColorfulText> titleText; // Текстовое поле для отображения правил игры sf::Text text; // Прямоугольник-фон под текст, чтобы он был лучше виден на фоне sf::RectangleShape textBackground; // Спрайт с изображением правильно собранного поля (пример) sf::Sprite numbersSprite; // Кнопка "Назад" для возврата в главное меню std::unique_ptr<Button> backButton; /** Инициализирует графические элементы экрана. Метод вызывается один раз при создании экрана. Настраивает: текст, фон, спрайт, кнопку и заголовок. */ void initialize() override; public: /** Конструктор экрана правил. window Ссылка на SFML окно state Ссылка на текущее состояние приложения transition Объект для управления переходами между экранами */ RulesScreen(sf::RenderWindow& window, GameState& state, Transition& transition); /* Обрабатывает события пользователя (нажатия клавиш, клики мыши). event Обработанное событие SFML */ void handleEvent(const sf::Event& event) override; /* Обновляет логику экрана (анимации, состояние кнопок и т.д.). deltaTime Время, прошедшее с последнего кадра (в секундах) */ void update(float deltaTime) override; /* Отрисовывает все элементы экрана. target Цель отрисовки (обычно SFML окно) states Рендер-состояния (не используются по умолчанию) */ void draw(sf::RenderTarget& target, sf::RenderStates states) const override; };
#include "RulesScreen.h" #include <stdexcept> #include <iostream> // Константа для позиции Y заголовка (чтобы избежать magic numbers) constexpr float TITLE_Y_POS = 15.0f; /* Конструктор класса RulesScreen. Инициализирует экран правил, связывает его с окном и системой состояний. */ RulesScreen::RulesScreen(sf::RenderWindow& window, GameState& state, Transition& transition) : Screen(window, state, transition) { initialize(); // Вызываем метод инициализации графических элементов } /* Метод инициализации всех графических элементов интерфейса: - Заголовок - Текст с правилами игры - Фон под текст - Изображение игрового поля - Кнопка возврата в меню */ void RulesScreen::initialize() { ResourceManager& rm = ResourceManager::getInstance(); sf::Font& font = rm.getFont("font"); if (font.getInfo().family.empty()) { throw std::runtime_error("Не удалось загрузить шрифт 'font' из ResourceManager"); } setupBackground("background1"); // Устанавливаем фоновое изображение // Анимированный цветной заголовок "Пятнашки" titleText = std::make_unique<ColorfulText>("font1", L"Пятнашки", 100, sf::Vector2f(0, TITLE_Y_POS), window, true, false); // Настройка текстового поля с правилами игры text.setFont(font); text.setString( L" 1. Игровое поле состоит из 15 пронумерованных\n" L" плиток и одного пустого места.\n" L" 2. Цель игры перемещать плитки, расположить\n" L" их в правильном порядке: (см. изображение справа)\n" L" 3. Перемещать можно только плитки,\n" L" соседние с пустым местом.\n" L" 4. Используйте для перемещения плиток левую кнопку\n" L" мыши.\n\n" L" Удачи!" ); text.setCharacterSize(30); // Размер шрифта text.setFillColor(sf::Color::Black); // Цвет текста — чёрный // Позиционируем текст по центру слева sf::FloatRect textRect = text.getLocalBounds(); text.setOrigin(0, text.getLocalBounds().height / 2.0f); text.setPosition(30.0f, window.getSize().y / 2.0f); // Создаём фоновый прямоугольник под текстом для улучшения читаемости textBackground.setSize(sf::Vector2f(textRect.width + 40, textRect.height + 60)); textBackground.setFillColor(sf::Color(255, 255, 255, 200)); // Полупрозрачный белый textBackground.setOrigin(0, textBackground.getSize().y / 2.0f); textBackground.setPosition(20.0f, window.getSize().y / 2.0f + 10.0f); // Загружаем изображение правильно собранного игрового поля sf::Texture& numTexture = rm.getTexture("numbers"); if (numTexture.getSize().x == 0) { std::cerr << "Ошибка: Не удалось загрузить изображение 'numbers' из ResourceManager.\n"; } numbersSprite.setTexture(numTexture); sf::FloatRect numbersRect = numbersSprite.getLocalBounds(); numbersSprite.setOrigin(numbersRect.width / 2.0f, numbersRect.height / 2.0f); numbersSprite.setScale(0.9f, 0.9f); // Уменьшаем немного изображение numbersSprite.setPosition(window.getSize().x * 0.75f + 100, window.getSize().y / 2.0f); // Создаём кнопку "Назад" внизу экрана backButton = std::make_unique<Button>( std::wstring(L"м е н ю"), sf::Vector2f(445, 600), font, sf::Color(64, 224, 208, 255), sf::Color(64, 224, 208, 255), sf::Color::Black, sf::Color(128, 128, 128), 395.0f, 50.0f, 30u, sf::Color::Black, 4.0f ); } /* Обработка событий от пользователя: - Нажатие Esc — возврат в меню - Нажатие/отпускание кнопки "Назад" */ void RulesScreen::handleEvent(const sf::Event& event) { // Выход в меню при нажатии Esc if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape) { transition.startTransition(GameState::MENU); } // Обработка нажатия на кнопку "Назад" if (event.type == sf::Event::MouseButtonPressed) { sf::Vector2f mousePos = window.mapPixelToCoords(sf::Mouse::getPosition(window)); if (backButton->isClicked(mousePos)) { backButton->pressAnimation(); // Анимация нажатия } } // Обработка отпускания кнопки мыши if (event.type == sf::Event::MouseButtonReleased) { sf::Vector2f mousePos = window.mapPixelToCoords(sf::Mouse::getPosition(window)); if (backButton->isClicked(mousePos)) { backButton->releaseAnimation(); // Анимация отпускания transition.startTransition(GameState::MENU); // Переход в меню } } } /* Отрисовка всех элементов экрана: - Фон - Текст с правилами - Фон под текстом - Изображение игрового поля - Кнопка "Назад" - Анимированный заголовок */ void RulesScreen::draw(sf::RenderTarget& target, sf::RenderStates states) const { target.clear(sf::Color::White); // Очищаем экран // Рисуем фон, если он есть if (useTexture) { target.draw(backgroundSprite, states); } target.draw(textBackground, states); // Рисуем фон под текстом target.draw(numbersSprite, states); // Рисуем изображение игрового поля target.draw(text, states); // Рисуем текст с правилами target.draw(*backButton, states); // Рисуем кнопку "Назад" titleText->draw(target, states); // Рисуем анимированный заголовок } /* Обновление состояния экрана: - Обновление анимации заголовка */ void RulesScreen::update(float deltaTime) { titleText->update(deltaTime); // Обновляем анимацию цветного заголовка }
main — Сердце игры "Пятнашки"
Файл main.cpp — это отправная точка всей игры. Он выполняет ключевые задачи: загружает ресурсы, создаёт окно, переключает экраны, обрабатывает события и управляет рендерингом.
#include <SFML/Graphics.hpp> #include <SFML/Audio.hpp> #include <iostream> #include "MainMenu.h" #include "GameScreen.h" #include "RulesScreen.h" #include "ResourceManager.h" #include "Screen.h" #include "resource.h" // Подключаем ресурсы из .rc файла (например, иконки, текстуры) // Константы для размера окна constexpr unsigned int WINDOW_WIDTH = 1280; constexpr unsigned int WINDOW_HEIGHT = 720; // Ограничение дельта-времени, чтобы предотвратить большие скачки времени между кадрами constexpr float MAX_DELTA_TIME = 1.0f / 30.0f; /* Инициализация всех игровых ресурсов. Метод загружает текстуры, шрифты, музыку и другие ресурсы из файла ресурсов (.rc). Если какой-то ресурс не загружен — выбрасывается исключение. */ void initializeResources() { ResourceManager& rm = ResourceManager::getInstance(); if (!rm.loadTexture("puzzle", IDR_TEXTURE1)) { throw std::runtime_error("Не удалось загрузить текстуру 'puzzle'"); } if (!rm.loadTexture("background", IDR_TEXTURE2)) { throw std::runtime_error("Не удалось загрузить текстуру 'background'"); } if (!rm.loadTexture("background1", IDR_TEXTURE3)) { throw std::runtime_error("Не удалось загрузить текстуру 'background1'"); } if (!rm.loadTexture("background2", IDR_TEXTURE4)) { throw std::runtime_error("Не удалось загрузить текстуру 'background2'"); } if (!rm.loadTexture("numbers", IDR_TEXTURE5)) { throw std::runtime_error("Не удалось загрузить текстуру 'numbers'"); } if (!rm.loadMusic("music", IDR_SOUND1)) { throw std::runtime_error("Не удалось загрузить звук 'music'"); } if (!rm.loadFont("font", IDR_FONT1)) { throw std::runtime_error("Не удалось загрузить шрифт 'font'"); } if (!rm.loadFont("font1", IDR_FONT2)) { throw std::runtime_error("Не удалось загрузить шрифт 'font1'"); } } /* Точка входа в приложение. Создаёт окно игры, инициализирует ресурсы, создаёт экраны (меню, игра, правила), запускает основной цикл отрисовки и обработки событий. */ int main() { system("chcp 1251"); // Устанавливаем кодовую страницу Windows-1251 для корректного вывода в консоль try { // Загружаем все игровые ресурсы initializeResources(); // Создаем главное окно приложения sf::RenderWindow window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), L"Пятнашки"); window.setFramerateLimit(60); // Ограничиваем частоту кадров до 60 FPS // Загружаем и устанавливаем иконку окна ResourceManager& rm = ResourceManager::getInstance(); sf::Image icon = rm.getImage("puzzle"); if (icon.getSize().x == 0 || icon.getSize().y == 0) { throw std::runtime_error("Ошибка: не удалось загрузить иконку 'puzzle' из ресурсов!"); } window.setIcon(icon.getSize().x, icon.getSize().y, icon.getPixelsPtr()); // Создаем игровые экраны GameState state = GameState::MENU; // Начальное состояние — главное меню Transition transition(window); // Объект для анимации перехода между экранами GameScreen gameScreen(window, state, transition); // Игровой экран MainMenu mainMenu(window, state, transition, gameScreen); // Главное меню RulesScreen rulesScreen(window, state, transition); // Экран с правилами // Указатель на текущий активный экран Screen* currentScreen = &mainMenu; // Часы для измерения времени между кадрами sf::Clock clock; while (window.isOpen()) { // Вычисляем время с последнего кадра (ограничиваем его) float deltaTime = clock.restart().asSeconds(); if (deltaTime > MAX_DELTA_TIME) deltaTime = MAX_DELTA_TIME; // Обработка событий SFML sf::Event event; while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) { window.close(); // Закрытие окна по нажатию крестика } currentScreen->handleEvent(event); // Передаем событие текущему экрану } // Обновляем состояние перехода между экранами bool isTransitioning = transition.update(state, deltaTime); // Обновляем текущий экран currentScreen->update(deltaTime); // Отрисовка window.clear(); // Очищаем окно window.draw(*currentScreen); // Рисуем текущий экран if (isTransitioning) { window.draw(transition); // Рисуем анимацию перехода поверх экрана } window.display(); // Отображаем всё на экране // Переключение экранов в зависимости от состояния switch (state) { case GameState::MENU: currentScreen = &mainMenu; break; case GameState::GAME: currentScreen = &gameScreen; break; case GameState::RULES: currentScreen = &rulesScreen; break; default: break; } } } catch (const std::exception& e) { // Ловим и выводим ошибки std::cerr << e.what() << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS; }
main.cpp — это диспетчер игры. Он не содержит логику самих экранов, а только управляет их жизненным циклом и переключениями. Благодаря такой структуре код легко поддерживать и расширять — можно добавлять новые экраны или менять ресурсы, не затрагивая остальное.