Мультимедийная библиотека SFML C++
April 19

Менеджер ресурсов и динамический текст на SFML

Когда Вы только начинаете изучать игровую разработку на C++ с использованием SFML, перед многими из Вас встаёт вопрос: как правильно организовать хранение и загрузку игровых ресурсов? Сначала всё кажется просто - загружаешь текстуру здесь, шрифт там, звук где-то ещё. Но когда Ваш проект начинает расти, Вы сталкиваетесь с реальными проблемами.

Одна и та же текстурная карта может загружаться в память трижды - для главного меню, для уровня и для редактора. В этом случае память расходуется неэффективно, а код превращается в спагетти из вызовов loadFromFile, разбросанных по всей кодовой базе. Тогда Вы начинаете понимать, что нужен системный подход.

Какие варианты есть, это: хранение ресурсов в отдельных классах, глобальные переменные (да, я знаю, что это плохо), система с ручным управлением жизненного цикла.

Каждый из этих подходов имеет свои недостатки. Либо ресурсы живут слишком долго, либо их освобождение происходит не в тот момент, либо доступ к ним неудобный.

Самый оптимальный вариант, это создание класса сингелтона, для управления ресурсами.

Но почему бы не упаковать все ресурсы прямо в исполняемый файл?

Мы создадим менеджер ресурсов, который использует встроенные возможности Windows.

Стоит сразу оговориться — это решение работает исключительно под Windows. Оно использует системные API-функции для доступа к ресурсам, встроенным в EXE-файл.

Главное преимущество этой системы — все ресурсы действительно хранятся внутри EXE-файла. Вы не представляете, как приятно, когда твоя игра представляет собой всего один файл, который можно скопировать куда угодно, и он просто работает. Никаких "где-то потерялась текстура", "не найден шрифт" и прочих проблем с путями.

Конечно, у этого подхода есть свои ограничения:

1.    Размер EXE-файла увеличивается (но современные компьютеры с этим легко справляются)

2.    Невозможно изменить ресурсы без перекомпиляции

3.    Работает только под Windows

Но для многих сценариев разработки игр эти ограничения не критичны. В моём случае плюсы перевешивают:

1.     Простота распространения (один файл!)

2.     Защита ресурсов от случайного изменения

3.     Гарантия, что все нужные файлы на месте

Если вам нужна кроссплатформенность, этот подход не подойдёт. Но если вы, как и я, разрабатываете игры исключительно под Windows и цените простоту распространения, то встроенные ресурсы Windows — отличное решение. В следующих разделах я подробно расскажу, как реализовать эту систему с нуля.

Структура проекта в Visual Studio 2022

📁 Проект/

├── 📄 main.cpp # Главный файл с основным циклом

├── 📄 ResourceManager.h # Заголовочный файл менеджера ресурсов

├── 📄 ResourceManager.cpp # Реализация менеджера ресурсов

├── 📄 ColorfulText.h # Заголовочный файл анимированного текста

├── 📄 ColorfulText.cpp # Реализация анимированного текста

├── 📄 resource.h # Идентификаторы ресурсов

└── 📄 resources.rc # Скрипт компиляции ресурсов

Файлы ресурсов

Ожидаемый результат от программы

Начинаем творить чудеса

Сначала создаём в ручную файлы ресурсов используя редактор кода С++ нашей IDE. Надеюсь Вы уже справились с установкой библиотеки SFML, если нет почитайте статьи выше.

resource.h

#pragma once

#define IDR_FONT1      201  // Шрифт
#define IDR_FONT2      202  // Шрифт

#define IDR_TEXTURE1   301  // Текстура

resource.rc

#include "resource.h"

// ресурсы по умолчанию
IDR_TEXTURE1   RCDATA  "assets/puzzle.png"
IDR_FONT1      RCDATA  "assets/arial.ttf"
IDR_FONT2      RCDATA  "assets/glv.ttf"
IDR_ICON1      ICON    "assets/puzzle.ico"

В нашей программе все ресурсы находятся в отдельной папке assets, которая в свою очередь находится в корневом каталоге проекта.

Теперь создадим класс управления ресурсами.

ResourceManager.h

#pragma once

#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#include <string>
#include <unordered_map>
#include <memory>
#include <optional>
#include <source_location>
#include <windows.h>

// Класс для управления ресурсами (шрифты, текстуры, звуки, музыка)
class ResourceManager {
public:
    // Получение единственного экземпляра (паттерн Singleton)
    static ResourceManager& getInstance() noexcept;

    // Загрузка шрифта по идентификатору ресурса
    [[nodiscard]] bool loadFont(std::string_view id, int resourceId,
        const std::source_location& loc = std::source_l
        ocation::current()) noexcept;

    // Загрузка звукового буфера по идентификатору ресурса
    [[nodiscard]] bool loadSound(std::string_view id, int resourceId,
        const std::source_location& loc = std::sour
        ce_location::current()) noexcept;

    // Загрузка текстуры по идентификатору ресурса
    [[nodiscard]] bool loadTexture(std::string_view id, int resourceId,
        const std::source_location& loc = std::s
        ource_location::current()) noexcept;

    // Загрузка музыки по идентификатору ресурса
    [[nodiscard]] bool loadMusic(std::string_view id, int resourceId,
        const std::source_location& loc = s
        td::source_location::current()) noexcept;

    // Получение шрифта по идентификатору (возвращаем указатель)
    [[nodiscard]] std::optional<sf::Font*> getFont(std::string_view id,
        const std::source_location& loc
         = std::source_location::current()) noexcept;

    // Получение звукового буфера по идентификатору (возвращаем указатель)
    [[nodiscard]] std::opti
    onal<sf::SoundBuffer*> getSoundBuffer(std::string_view id,
        const std::source_location& 
        loc = std::source_location::current()) noexcept;

    // Получение текстуры по идентификатору (возвращаем указатель)
    [[nodiscard]] std::optional<
    sf::Texture*> getTexture(std::string_view id,
        const std::source_locat
        ion& loc = std::source_location::current()) noexcept;

    // Получение изображения из текстуры по идентификатору
    [[nodiscard]] std::optional<sf::Image> getImage(std::string_view id,
        const std::source_l
        ocation& loc = std::source_location::current()) noexcept;

    //Получение музыкального потока по идентификатору (возвращаем указатель)
    [[nodiscard]] std::optional<sf::Music*> getMusic(std::string_view id,
        const std::sourc
        e_location& loc = std::source_location::current()) noexcept;

    // Запрещаем копирование и перемещение
    ResourceManager(const ResourceManager&) = delete;
    ResourceManager& operator=(const ResourceManager&) = delete;
    ResourceManager(ResourceManager&&) = delete;
    ResourceManager& operator=(ResourceManager&&) = delete;

private:
    ResourceManager() = default; // Приватный конструктор для Singleton

    // Вспомогательный класс для RAII-управления ресурсами Windows API
    class ResourceHandle {
    public:
        ResourceHandle(HRSRC resource, HGLOBAL handle) noexcept : 
        resource_(resource), handle_(handle) {}
        ~ResourceHandle() { if (handle_) FreeResource(handle_); }
        [[nodiscard]] const void* data() const noexcept { 
        return LockResource(handle_); }
        [[nodiscard]] DWORD size() const noexcept { 
        return SizeofResource(NULL, resource_); }
    private:
        HRSRC resource_;
        HGLOBAL handle_;
    };

    // Шаблонный метод для загрузки ресурса
    template<typename T>
    [[nodiscard]] bool loadResource(int resourceId, T& target,
        const std::source_location& loc = std::source_location::current()) 
        noexcept;

    // Хранилища ресурсов
    std::unordered_map<std::string, sf::Font> fonts_; // Шрифты
    // Звуковые буферы
    std::unordered_map<std::string, sf::SoundBuffer> buffers_; 
    std::unordered_map<std::string, sf::Texture> textures_; // Текстуры
    // Музыкальные потоки
    std::unordered_map<std::string, std::unique_ptr<sf::Music>> 
    musicStreams_; 
    // Кэш изображений
    std::unordered_map<std::string, sf::Image> imageCache_;
};

ResourceManager.cpp

#include "ResourceManager.h"
#include <iostream>

// Получение единственного экземпляра менеджера ресурсов (синглтон)
ResourceManager& ResourceManager::getInstance() noexcept {
    static ResourceManager instance;
    return instance;
}

// Шаблонный метод для загрузки ресурса любого типа
template<typename T>
bool ResourceManager::loadResource(int resourceId, 
T& target, const std::source_location& loc) noexcept {
    // Поиск ресурса в исполняемом файле
    HRSRC resource = FindResource(NULL, MAKEINTRESOURCE(
    resourceId), RT_RCDATA);
    if (!resource) {
        std::cerr << "Ошибка в " << loc.function_name() 
        << " (строка " << loc.line()
            << "): Не удалось найти ресурс с ID " << resourceId << '\n';
        return false;
    }

    // Загрузка ресурса с использованием RAII
    HGLOBAL handle = LoadResource(NULL, resource);
    if (!handle) {
        std::cerr << "Ошибка в " << loc.function
        _name() << " (строка " << loc.line()
            << "): Не удалось загрузить ресу
            рс с ID " << resourceId << '\n';
        return false;
    }

    // Использование RAII-обертки для управления ресурсом
    ResourceHandle resourceHandle(resource, handle);
    const void* data = resourceHandle.data();
    if (!data) {
        std::cerr << "Ошибка в " << loc
        .function_name() << " (строка " << loc.line()
            << "): Не удалось заблокиро
            вать ресурс с ID " << resourceId << '\n';
        return false;
    }

    // Получение размера ресурса и загрузка в целевой объект
    DWORD size = resourceHandle.size();
    return target.loadFromMemory(data, size);
}

// Загрузка шрифта из ресурсов
bool ResourceManager::loa
dFont(std::string_view id, int resourceId, 
const std::source_location& loc) noexcept {
    sf::Font font;
    if (!loadResource(resourceId, font, loc)) {
        std::cerr << "Оши
        бка в " << loc.function_name() << " (строка " << loc.line()
            << "): Не удалось загрузить шрифт '" << id << "'\n";
        return false;
    }
    // Сохранение шрифта в словаре под указанным идентификатором
    fonts_[std::string(id)] = std::move(font);
    return true;
}

// Загрузка звукового буфера из ресурсов
bool Resource
Manager::loadSound(std::string_view id, int resourceId, const 
std::source_location& loc) noexcept {
    sf::SoundBuffer buffer;
    if (!loadResource(resourceId, buffer, loc)) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Не удалось загрузить звук '" << id << "'\n";
        return false;
    }
    // Сохранение звукового буфера в словаре
    buffers_[std::string(id)] = std::move(buffer);
    return true;
}

// Загрузка текстуры из ресурсов
bool ResourceManager::loadTexture(std::string_view id, int resourceId, 
const std::source_location& loc) noexcept {
    sf::Texture texture;
    if (!loadResource(resourceId, texture, loc)) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Не удалось загрузить текстуру '" << id << "'\n";
        return false;
    }
    // Сохранение текстуры в словаре
    textures_[std::string(id)] = std::move(texture);
    return true;
}

// Загрузка музыки из ресурсов (особый случай, так как Music в SFML 
//работает с потоками)
bool ResourceManager::loadMusic(std::string_view id, int resourceId, 
const std::source_location& loc) noexcept {
    // Поиск музыкального ресурса
    HRSRC resource = FindResource(NULL, MAKEINTRESOURCE(resourceId), 
    RT_RCDATA);
    if (!resource) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Не удалось найти музыкальный ресурс '" << id << "'\n";
        return false;
    }

    // Загрузка ресурса
    HGLOBAL handle = LoadResource(NULL, resource);
    if (!handle) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Не удалось загрузить музыкальный ресурс '" << id 
            << "'\n";
        return false;
    }

    // Использование RAII-обертки
    ResourceHandle resourceHandle(resource, handle);
    const void* data = resourceHandle.data();
    if (!data) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Не удалось заблокировать музыкальный ресурс '" << id 
            << "'\n";
        return false;
    }

    // Создание музыкального потока из данных в памяти
    DWORD size = resourceHandle.size();
    auto music = std::make_unique<sf::Music>();
    if (!music->openFromMemory(data, size)) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Не удалось открыть музыкальный поток '" << id 
            << "' из памяти\n";
        return false;
    }

    // Сохранение музыкального потока в словаре
    musicStreams_[std::string(id)] = std::move(music);
    return true;
}

// Получение шрифта по идентификатору
std::optional<sf::Font*> ResourceManager::getFont(std::string_view id, 
const std::source_location& loc) noexcept {
    auto it = fonts_.find(std::string(id));
    if (it == fonts_.end()) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Шрифт '" << id << "' не найден\n";
        return std::nullopt;
    }
    return &(it->second);
}

// Получение звукового буфера по идентификатору
std::optional<sf::SoundBuffer*> ResourceManager::getSoundBuffer(
std::string_view id, const std::source_location& loc) noexcept {
    auto it = buffers_.find(std::string(id));
    if (it == buffers_.end()) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Звуковой буфер '" << id << "' не найден\n";
        return std::nullopt;
    }
    return &(it->second);
}

// Получение текстуры по идентификатору
std::optional<sf::Texture*> ResourceManager::getTexture(
std::string_view id, const std::source_location& loc) noexcept {
    auto it = textures_.find(std::string(id));
    if (it == textures_.end()) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Текстура '" << id << "' не найдена\n";
        return std::nullopt;
    }
    return &(it->second);
}

// Получение изображения по идентификатору (с кэшированием)
std::optional<sf::Image> ResourceManager::getImage(std::string_view id, 
const std::source_location& loc) noexcept {
    // Проверяем, есть ли изображение в кэше
    auto cacheIt = imageCache_.find(std::string(id));
    if (cacheIt != imageCache_.end()) {
        return cacheIt->second;
    }

    // Если нет в кэше, загружаем из текстуры
    auto texIt = textures_.find(std::string(id));
    if (texIt == textures_.end()) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Текстура для изображения '" << id << "' не найдена\n";
        return std::nullopt;
    }

    // Конвертируем текстуру в изображение и кэшируем результат
    sf::Image image = texIt->second.copyToImage();
    imageCache_[std::string(id)] = image; // Кэшируем изображение
    return image;
}

// Получение музыкального потока по идентификатору
std::optional<sf::Music*> ResourceManager::getMusic(std::string_view id, 
const std::source_location& loc) noexcept {
    auto it = musicStreams_.find(std::string(id));
    if (it == musicStreams_.end()) {
        std::cerr << "Ошибка в " << loc.function_name() << " (строка " 
        << loc.line()
            << "): Музыкальный поток '" << id << "' не найден\n";
        return std::nullopt;
    }
    return it->second.get();
}

Теперь мы можем создать класс для создания феерического текста

ColorfulText.h

#pragma once

#include <SFML/Graphics.hpp>
#include <vector>
#include <string_view>
#include <random>
#include <memory>
#include <optional>
#include <ranges>
#include <concepts>
#include <source_location>

class ResourceManager; // Предполагаем, что определен где-то еще

class ColorfulText : public sf::Drawable {
public:
    // Перечисление эффектов с scoped enum
    enum class Effect {
        RandomColors, // Случайные цвета для каждой буквы
        Wave,         // Волна цветов, бегущая по тексту
        Blink,        // Мигание букв с изменением яркости
        Rainbow,      // Радужный переход цветов
        Pulse,        // Пульсация размера букв
        Fade,         // Затухание и появление цветов
        Twinkle       // Мерцание случайных букв
    };

    // Статический метод создания с поддержкой юникода
    [[nodiscard]] static std::optional<ColorfulText> create(
        std::string_view fon
        tId, std::wstring_view text, unsigned int charSize,
        const sf::Vector2f& position, const sf::RenderWindow& window,
        bool centerHorizontally = false, bool centerVertically = false,
        Effect effect = Effect::RandomColors,
        const std::source_locat
        ion& loc = std::source_location::current());

    // Обновление состояния текста
    void update(float deltaTime) noexcept;

    // Установка эффекта с использованием концепций C++20
    template<typename T>
        requires std::same_as<T, Effect>
    void setEffect(T newEffect) noexcept { setEffectImpl(newEffect); }

    // Установка позиции текста
    void setPosition(const sf::Vector2f& position, 
    bool centerHorizontally = false, bool centerVertically = false) 
    noexcept;

    // Запрещаем копирование, разрешаем перемещение
    ColorfulText(const ColorfulText&) = delete;
    ColorfulText& operator=(const ColorfulText&) = delete;
    ColorfulText(ColorfulText&&) = default;
    ColorfulText& operator=(ColorfulText&&) = default;

private:
    // Приватный конструктор для внутреннего использования в create
    ColorfulText() = default;

    // Переопределение метода отрисовки из sf::Drawable
    void draw(sf::RenderTarget& target, sf::RenderStates states) 
    const override;

    // Методы обновления эффектов
    void updateRandomColors();        // Случайные цвета
    void updateWave(float deltaTime); // Волна цветов
    void updateBlink(float deltaTime); // Мигание букв
    void updateRainbow(float deltaTime); // Радужный переход
    void updatePulse(float deltaTime);   // Пульсация размера
    void updateFade(float deltaTime);    // Затухание цветов
    void updateTwinkle(float deltaTime); // Мерцание случайных букв

    // Внутренняя реализия смены эффекта
    void setEffectImpl(Effect newEffect) noexcept;

    // Данные класса
    // Указатель на шрифт (хранится в ResourceManager)
    sf::Font* font_ = nullptr; 
    std::vector<sf::Text> letters_;          // Вектор букв текста
    float colorTimer_ = 0.0f;                // Таймер для эффектов
    Effect currentEffect_;                   // Текущий эффект
    float waveOffset_ = 0.0f;           // Смещение для плавных эффектов
    std::vector<float> twinkleTimers_;  // Таймеры для эффекта мерцания

    // Константы
    // Интервал смены эффектов
    static constexpr float COLOR_CHANGE_INTERVAL = 0.5f; 
    static constexpr float WAVE_SPEED = 1.0f;       // Скорость волны
    static constexpr float PULSE_AMPLITUDE = 0.2f;  // Амплитуда пульсации
    static constexpr float TWINKLE_CHANCE = 0.1f;   // Шанс мерцания
};

ColorfulText.cpp

#include "ColorfulText.h"
#include "ResourceManager.h"
#include <cmath>
#include <algorithm>
#include <iostream>

std::optional<ColorfulText> ColorfulText::create(
    std::string_view fontId, std::wstring_view
     text, unsigned int charSize,
    const sf::Vector2f& position, const sf::RenderWindow& window,
    bool centerHorizontally, bool centerVertically, Effect effect,
    const std::source_location& loc) {

    // Создаем объект с помощью приватного конструктора
    ColorfulText result;
    ResourceManager& rm = ResourceManager::getInstance();
    auto fontOpt = rm.getFont(fontId, loc);
    if (!fontOpt.has_value()) {
        return std::nullopt; // Ошибка уже выведена в getFont
    }
    result.font_ = fontOpt.value(); // Извлекаем указатель на шрифт

    // Расчет позиции текста
    float totalWidth = text.size() * charSize * 0.8f;
    float xPos = centerHorizontally ? (window.get
    Size().x - totalWidth) / 2.0f : position.x;
    float yPos = centerVertically ? (window.
    getSize().y - charSize) / 2.0f : position.y;

    // Резервируем память
    result.letters_.reserve(text.size());
    result.twinkleTimers_.resize(text.size(), 0.0f);

    // Инициализация букв с использованием std::ranges (C++20)
    std::ranges::for_each(std::views::i
    ota(size_t{ 0 }, text.size()), [&](size_t i) {
        sf::Text letter;
        letter.setFont(*result.font_);
        // Устанавливаем символ юникода
        letter.setCharacterSize(charSize);
        letter.setString(std::wstring(1, text[i])); 
        letter.setFillColor(sf::Color::Magenta);
        letter.setPosition(xPos + i * charSize * 0.8f, yPos);
        result.letters_.push_back(std::move(letter));
        });

    // Инициализация остальных полей
    result.currentEffect_ = effect;
    result.colorTimer_ = 0.0f;
    result.waveOffset_ = 0.0f;

    return result;
}

void ColorfulText::update(float deltaTime) noexcept {
    colorTimer_ += deltaTime;
    waveOffset_ += deltaTime * WAVE_SPEED;

    // Обновление в зависимости от текущего эффекта
    switch (currentEffect_) {
    case Effect::RandomColors:
        if (colorTimer_ >= COLOR_CHANGE_INTERVAL) {
            colorTimer_ = 0.0f;
            updateRandomColors();
        }
        break;
    case Effect::Wave: updateWave(deltaTime); break;
    case Effect::Blink: updateBlink(deltaTime); break;
    case Effect::Rainbow: updateRainbow(deltaTime); break;
    case Effect::Pulse: updatePulse(deltaTime); break;
    case Effect::Fade: updateFade(deltaTime); break;
    case Effect::Twinkle: updateTwinkle(deltaTime); break;
    }
}

void ColorfulText::setEffectImpl(Effect newEffect) noexcept {
    currentEffect_ = newEffect;
    colorTimer_ = 0.0f;
    waveOffset_ = 0.0f;
    std::ranges::fill(twinkleTimers_, 0.0f); // Сброс таймеров мерцания
    update(0.0f); // Немедленное обновление состояния
}

void ColorfulText::updateRandomColors() {
    // Генерация случайных цветов для каждой буквы
    thread_local std::mt19937 gen{ std::random_device{}() };
    std::uniform_int_distribution<int> dis(0, 255);

    std::ranges::for_each(letters_, [&](auto& letter) {
        letter.setFillColor(sf::Color(
            static_cast<sf::Uint8>(dis(gen)),
            static_cast<sf::Uint8>(dis(gen)),
            static_cast<sf::Uint8>(dis(gen))));
        });
}

void ColorfulText::updateWave(float deltaTime) {
    // Эффект волны цветов
    std::ranges::for_each(std::views::iota(size_t{ 0 }, 
    letters_.size()), [&](size_t i) {
        float phase = waveOffset_ + i * 0.2f;
        sf::Uint8 value = static_cast<sf::Uint8>(127.5f + 
        127.5f * std::sin(phase));
        letters_[i].setFillColor(sf::Color(value, 255 - 
        value, value / 2));
        });
}

void ColorfulText::updateBlink(float deltaTime) {
    // Мигание букв через изменение прозрачности
    float alpha = 127.5f + 127.5f * std::sin(colorTimer_ * 5.0f);
    std::ranges::for_each(letters_, [alpha](auto& letter) {
        sf::Color color = letter.getFillColor();
        color.a = static_cast<sf::Uint8>(alpha);
        letter.setFillColor(color);
        });
}

void ColorfulText::updateRainbow(float deltaTime) {
    // Радужный эффект с плавным переходом
    std::ranges::for_each(std::views::iota(size_t{ 0 }, 
    letters_.size()), [&](size_t i) {
        float phase = waveOffset_ + i * 0.1f;
        sf::Uint8 r = static_cast<sf::Uint8>(127.5f + 
        127.5f * std::sin(phase));
        sf::Uint8 g = static_cast<sf::Uint8>(127.5f + 
        127.5f * std::sin(phase + 2.0f));
        sf::Uint8 b = static_cast<sf::Uint8>(127.5f + 
        127.5f * std::sin(phase + 4.0f));
        letters_[i].setFillColor(sf::Color(r, g, b));
        });
}

void ColorfulText::updatePulse(float deltaTime) {
    // Пульсация размера букв
    float scale = 1.0f + PULSE_AMPLITUDE * std::sin(colorTimer_ * 3.0f);
    std::ranges::for_each(letters_, [scale](auto& letter) {
        letter.setScale(scale, scale);
        });
}

void ColorfulText::updateFade(float deltaTime) {
    // Затухание и появление цветов
    float alpha = 127.5f + 127.5f * std::sin(colorTimer_ * 2.0f);
    std::ranges::for_each(letters_, [alpha](auto& letter) {
        letter.setFillColor(sf::Color(255, 255, 255, 
        static_cast<sf::Uint8>(alpha)));
        });
}

void ColorfulText::updateTwinkle(float deltaTime) {
    // Мерцание случайных букв
    thread_local std::mt19937 gen{ std::random_device{}() };
    std::uniform_real_distribution<float> dis(0.0f, 1.0f);

    std::ranges::for_each(std::views::iota(size_t{ 0 }, 
    letters_.size()), [&](size_t i) {
        twinkleTimers_[i] += deltaTime;
        if (twinkleTimers_[i] >= COLOR_CHANGE_INTERVAL) {
            if (dis(gen) < TWINKLE_CHANCE) {
                letters_[i].setFillColor(sf::Color::White);
                twinkleTimers_[i] = 0.0f;
            }
            else {
                letters_[i].setFillColor(sf::Color(100, 100, 100));
            }
        }
        });
}

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) noexcept {
    // Установка новой позиции текста
    float totalWidth = letters_.size() * letters_[0].getCharacterSize() 
    * 0.8f;
    float xPos = centerHorizontally ? position.x - totalWidth 
    / 2.0f : position.x;
    float yPos = centerVertically ? position.y - 
    letters_[0].getCharacterSize() / 2.0f : position.y;

    std::ranges::for_each(std::views::iota(size_t{ 0 }, 
    letters_.size()), [&](size_t i) {
        letters_[i].setPosition(xPos + i * 
        letters_[0].getCharacterSize() * 0.8f, yPos);
        });
}

А теперь все объединим в исполняем файле.

Мигающие_буквы.cpp

#include <SFML/Graphics.hpp>
#include "ColorfulText.h"
#include "ResourceManager.h"
#include "resource.h"
#include <iostream>

// Инициализация ресурсов
void initializeResources() {
    ResourceManager& rm = ResourceManager::getInstance();
    if (!rm.loadTexture("puzzle", IDR_TEXTURE1) || 
    !rm.loadFont("font", IDR_FONT1)) {
        throw std::runtime_error("Не удалось загрузить ресурсы");
    }
}

int main() {
    initializeResources();
    sf::RenderWindow window(sf::VideoMode(1280, 720), 
    L"Весёлый текст", sf::Style::Default);
    window.setFramerateLimit(60);

    // Установка иконки окна
    ResourceManager& rm = ResourceManager::getInstance();
    auto iconOpt = rm.getImage("puzzle");
    if (!iconOpt) {
        std::cerr << "Не удалось загрузить иконку 'puzzle'\n";
        return 1;
    }
    sf::Image icon = *iconOpt; // Извлекаем значение из std::optional
    if (icon.getSize().x == 0 || icon.getSize().y == 0) {
        std::cerr << "Иконка 'puzzle' пуста\n";
        return 1;
    }
    window.setIcon(icon.getSize().x, icon.getSize().y, 
    icon.getPixelsPtr());

    // Создание текста с юникодом
    auto text = ColorfulText::create("font", L"Привет, мир!", 100,
        sf::Vector2f(0, 0), window, true, true, 
        ColorfulText::Effect::Pulse);
    if (!text) {
        std::cerr << "Не удалось создать ColorfulText\n";
        return 1;
    }

    sf::Clock clock;
    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) {
                window.close();
            }
            // Переключение эффектов по нажатию клавиш (только 7 эффектов)
            if (event.type == sf::Event::KeyPressed) {
                switch (event.key.code) {
                case sf::Keyboard::F1: text->setEffect(
                ColorfulText::Effect::RandomColors); break;
                case sf::Keyboard::F2: text->setEffect(
                ColorfulText::Effect::Wave); break;
                case sf::Keyboard::F3: text->setEffect(
                ColorfulText::Effect::Blink); break;
                case sf::Keyboard::F4: text->setEffect(
                ColorfulText::Effect::Rainbow); break;
                case sf::Keyboard::F5: text->setEffect(
                ColorfulText::Effect::Pulse); break;
                case sf::Keyboard::F6: text->setEffect(
                ColorfulText::Effect::Fade); break;
                case sf::Keyboard::F7: text->setEffect(
                ColorfulText::Effect::Twinkle); break;
                }
            }
        }

        // Обновление и отрисовка текста
        float deltaTime = clock.restart().asSeconds();
        text->update(deltaTime);

        window.clear(sf::Color::Blue);
        window.draw(*text);
        window.display();
    }
    return 0;
}

Надеюсь у Вас всё получилось и до новых встреч !!!

Телеграмм канал - Программирование игр С++