<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>Искусство разработки игр</title><author><name>Искусство разработки игр</name></author><id>https://teletype.in/atom/gamedeveloper</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/gamedeveloper?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/gamedeveloper?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-08-20T00:12:45.834Z</updated><entry><id>gamedeveloper:lLmUM5VUZhi</id><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper/lLmUM5VUZhi?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><title>Разработка игры «Сокобан» на SFML C++: ящики, лабиринты и чистая архитектура</title><published>2026-08-07T19:19:09.296Z</published><updated>2026-08-07T19:19:09.296Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img3.teletype.in/files/24/af/24afaaa4-ae0c-4559-bb8c-0d1e2d1a820e.png"></media:thumbnail><category term="mul-timedijnaya-biblioteka-sfml-c" label="Мультимедийная библиотека  SFML C++"></category><summary type="html">&lt;img src=&quot;https://img2.teletype.in/files/96/a6/96a6c49e-f0f5-4ac6-beaa-e6372f098892.png&quot;&gt;Привет, разработчики!</summary><content type="html">
  &lt;figure id=&quot;YyMT&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/96/a6/96a6c49e-f0f5-4ac6-beaa-e6372f098892.png&quot; width=&quot;801&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;mqmR&quot;&gt;Привет, разработчики!&lt;/p&gt;
  &lt;p id=&quot;BhFv&quot;&gt;Сегодня предлагаю написать классическую головоломку &lt;strong&gt;«Сокобан»&lt;/strong&gt; на C++20 с&lt;br /&gt;использованием библиотеки SFML. На первый взгляд игра кажется совсем простой: есть человек, несколько ящиков и отмеченные клетки. Ходим по лабиринту, толкаем ящики — что здесь вообще может пойти не так?&lt;/p&gt;
  &lt;p id=&quot;EpJo&quot;&gt;А пойти не так может многое.&lt;/p&gt;
  &lt;p id=&quot;Qng4&quot;&gt;Ящик нельзя тянуть назад. Два ящика могут заблокировать друг друга. Один&lt;br /&gt;неудачный толчок в угол способен сделать уровень непроходимым. Кроме того, нам понадобятся отмена хода, несколько уровней, плавная анимация, русский текст и нормальная структура проекта, которую не захочется переписать через неделю. Именно поэтому Sokoban — замечательный учебный проект. Здесь нет сложной физики и огромного количества графики, зато есть настоящие игровые правила, состояния, работа с картами и хороший повод отделить логику от отрисовки.&lt;/p&gt;
  &lt;p id=&quot;tTer&quot;&gt;В результате мы получим игру с тремя лабиринтами, счётчиком ходов, отменой&lt;br /&gt;действий и аккуратным интерфейсом. Давайте разберём всё по порядку.&lt;/p&gt;
  &lt;h2 id=&quot;7vEm&quot;&gt;Что мы будем использовать&lt;/h2&gt;
  &lt;p id=&quot;sLGH&quot;&gt;Для проекта нам понадобятся:&lt;/p&gt;
  &lt;ol id=&quot;ySwf&quot;&gt;
    &lt;li id=&quot;ueqb&quot;&gt;Visual Studio 2022 с компонентом «Разработка классических приложений на C++».&lt;/li&gt;
    &lt;li id=&quot;OU9w&quot;&gt;C++20.&lt;/li&gt;
    &lt;li id=&quot;xxZz&quot;&gt;SFML 2.6.1.&lt;/li&gt;
    &lt;li id=&quot;eDS3&quot;&gt;Немного терпения — ящики иногда упрямее, чем компилятор.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;BnOm&quot;&gt;У меня SFML установлена в каталоге:&lt;/p&gt;
  &lt;pre id=&quot;pGXp&quot;&gt;C:/IT/SFML-2.6.1-windows-vc17-64-bit/SFML-2.6.1&lt;/pre&gt;
  &lt;p id=&quot;FQCa&quot;&gt;Вы можете использовать другой путь. Главное — правильно указать каталоги&lt;br /&gt;&lt;code&gt;include&lt;/code&gt; и &lt;code&gt;lib&lt;/code&gt; в Visual Studio либо передать &lt;code&gt;SFML_DIR&lt;/code&gt; при настройке CMake.&lt;/p&gt;
  &lt;h2 id=&quot;E9GY&quot;&gt;Вспоминаем правила Sokoban&lt;/h2&gt;
  &lt;p id=&quot;UPwv&quot;&gt;Игрок перемещается по клетчатому полю в четырёх направлениях. Он может зайти на&lt;br /&gt;свободную клетку, но не может проходить через стены.&lt;/p&gt;
  &lt;p id=&quot;fVON&quot;&gt;Если перед игроком стоит ящик, происходит дополнительная проверка:&lt;/p&gt;
  &lt;ul id=&quot;uK9S&quot;&gt;
    &lt;li id=&quot;vCzu&quot;&gt;клетка за ящиком свободна — ящик можно толкнуть;&lt;/li&gt;
    &lt;li id=&quot;hotU&quot;&gt;там стена или другой ящик — ход запрещён;&lt;/li&gt;
    &lt;li id=&quot;QldP&quot;&gt;тянуть ящик на себя нельзя.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;nmzz&quot;&gt;Уровень завершён, когда каждый ящик находится на цели.&lt;/p&gt;
  &lt;p id=&quot;sleD&quot;&gt;Важно заметить одну вещь: игровое правило ничего не говорит о пикселях,&lt;br /&gt;спрайтах или &lt;code&gt;sf::RectangleShape&lt;/code&gt;. Для него существуют только клетки, стены,&lt;br /&gt;ящики и направление движения. Это наблюдение поможет нам правильно разделить&lt;br /&gt;проект.&lt;/p&gt;
  &lt;h2 id=&quot;yGcY&quot;&gt;Структура проекта&lt;/h2&gt;
  &lt;p id=&quot;Uuit&quot;&gt;Я не советую складывать всё в один &lt;code&gt;main.cpp&lt;/code&gt;. На первом уровне это ещё будет&lt;br /&gt;работать, но после добавления undo и интерфейса файл быстро превратится в&lt;br /&gt;смесь условий, координат и вызовов &lt;code&gt;window.draw()&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;dqrW&quot;&gt;Используем следующую структуру:&lt;/p&gt;
  &lt;pre id=&quot;k9sJ&quot; data-lang=&quot;cpp&quot;&gt;Sokoban/
├── src/
│   ├── main.cpp
│   ├── core/
│   │   ├── Game.hpp
│   │   ├── Game.cpp
│   │   ├── GameConfig.hpp
│   │   └── GameState.hpp
│   ├── entities/
│   │   ├── Cell.hpp
│   │   ├── Board.hpp
│   │   └── Board.cpp
│   ├── levels/
│   │   ├── LevelDefinition.hpp
│   │   ├── LevelManager.hpp
│   │   └── LevelManager.cpp
│   ├── rendering/
│   │   ├── SceneRenderer.hpp
│   │   └── SceneRenderer.cpp
│   ├── resources/
│   │   ├── SystemFontLoader.hpp
│   │   └── SystemFontLoader.cpp
│   └── ui/
│       ├── Hud.hpp
│       └── Hud.cpp
└── tests/
    └── SokobanTests.cpp&lt;/pre&gt;
  &lt;p id=&quot;hzfr&quot;&gt;Здесь каждый класс решает одну задачу:&lt;/p&gt;
  &lt;ul id=&quot;vvlg&quot;&gt;
    &lt;li id=&quot;j54b&quot;&gt;&lt;code&gt;Game&lt;/code&gt; связывает все системы и содержит главный цикл;&lt;/li&gt;
    &lt;li id=&quot;PCng&quot;&gt;&lt;code&gt;Board&lt;/code&gt; знает правила Sokoban;&lt;/li&gt;
    &lt;li id=&quot;DkYK&quot;&gt;&lt;code&gt;LevelManager&lt;/code&gt; хранит карты и переключает уровни;&lt;/li&gt;
    &lt;li id=&quot;ooFp&quot;&gt;&lt;code&gt;SceneRenderer&lt;/code&gt; рисует лабиринт и игровые объекты;&lt;/li&gt;
    &lt;li id=&quot;Y1VS&quot;&gt;&lt;code&gt;Hud&lt;/code&gt; отвечает за текст и сообщения;&lt;/li&gt;
    &lt;li id=&quot;7bll&quot;&gt;&lt;code&gt;SystemFontLoader&lt;/code&gt; ищет установленный системный шрифт.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;aYX8&quot;&gt;Самое важное — &lt;code&gt;Board&lt;/code&gt; вообще не зависит от SFML. Его можно тестировать без&lt;br /&gt;окна, OpenGL-контекста и графики.&lt;/p&gt;
  &lt;h2 id=&quot;Ib9R&quot;&gt;Создаём координату клетки&lt;/h2&gt;
  &lt;p id=&quot;DCYj&quot;&gt;Начнём с самого маленького типа — координаты клетки.&lt;/p&gt;
  &lt;h3 id=&quot;axdr&quot;&gt;Cell.hpp&lt;/h3&gt;
  &lt;pre id=&quot;KVlf&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;lt;compare&amp;gt;

struct Cell {
    int x{0};
    int y{0};

    auto operator&amp;lt;=&amp;gt;(const Cell&amp;amp;) const = default;
};

inline Cell operator+(Cell left, Cell right) noexcept
{
    return {left.x + right.x, left.y + right.y};
}

enum class Direction {
    Up,
    Down,
    Left,
    Right
};

[[nodiscard]] inline Cell directionOffset(Direction direction) noexcept
{
    switch (direction) {
    case Direction::Up:    return {0, -1};
    case Direction::Down:  return {0, 1};
    case Direction::Left:  return {-1, 0};
    case Direction::Right: return {1, 0};
    }
    return {};
}&lt;/pre&gt;
  &lt;p id=&quot;oZbY&quot;&gt;Почему не &lt;code&gt;sf::Vector2i&lt;/code&gt;? Потому что клетка относится к правилам игры, а не к&lt;br /&gt;SFML. Если однажды мы захотим сделать консольную версию или перенести проект на&lt;br /&gt;другую библиотеку, доменную часть менять не придётся.&lt;/p&gt;
  &lt;p id=&quot;80dT&quot;&gt;Оператор сравнения нужен для хранения клеток в &lt;code&gt;std::set&lt;/code&gt;. Оператор сложения&lt;br /&gt;позволяет получить соседнюю клетку без постоянного ручного сложения &lt;code&gt;x&lt;/code&gt; и &lt;code&gt;y&lt;/code&gt;.&lt;/p&gt;
  &lt;h2 id=&quot;4VBR&quot;&gt;Описываем уровень обычным текстом&lt;/h2&gt;
  &lt;p id=&quot;pzqK&quot;&gt;Карту удобно хранить как набор строк. Для этого создадим небольшую структуру.&lt;/p&gt;
  &lt;h3 id=&quot;Zx6B&quot;&gt;LevelDefinition.hpp&lt;/h3&gt;
  &lt;pre id=&quot;ItBN&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;lt;string&amp;gt;
#include &amp;lt;vector&amp;gt;

struct LevelDefinition {
    std::wstring name;
    std::wstring subtitle;
    std::vector&amp;lt;std::string&amp;gt; map;
};&lt;/pre&gt;
  &lt;p id=&quot;tHGv&quot;&gt;Условимся использовать следующие символы:&lt;/p&gt;
  &lt;pre id=&quot;lB11&quot;&gt;#  стена
@  игрок
$  ящик
.  цель
*  ящик, уже стоящий на цели
+  игрок, стоящий на цели
   свободный пол&lt;/pre&gt;
  &lt;p id=&quot;4lWE&quot;&gt;Например, первый лабиринт выглядит так:&lt;/p&gt;
  &lt;pre id=&quot;yp12&quot;&gt;{
    L&amp;quot;Лабиринт I&amp;quot;,
    L&amp;quot;Развилка смотрителя&amp;quot;,
    {
        &amp;quot;###########&amp;quot;,
        &amp;quot;#   #     #&amp;quot;,
        &amp;quot;# .   # . #&amp;quot;,
        &amp;quot;# ##  #   #&amp;quot;,
        &amp;quot;#  $  ##$ #&amp;quot;,
        &amp;quot;##   @    #&amp;quot;,
        &amp;quot;#   ###   #&amp;quot;,
        &amp;quot;#         #&amp;quot;,
        &amp;quot;###########&amp;quot;
    }
}&lt;/pre&gt;
  &lt;p id=&quot;7jrj&quot;&gt;Такую карту легко читать прямо в исходном коде. Чтобы передвинуть стену или&lt;br /&gt;ящик, не нужно высчитывать пиксельную позицию — достаточно изменить символ.&lt;/p&gt;
  &lt;p id=&quot;mpOO&quot;&gt;Есть и обратная сторона: опечатка в карте может сломать уровень. Поэтому при&lt;br /&gt;загрузке мы обязательно проверим одинаковую длину строк, количество игроков,&lt;br /&gt;число целей и замкнутую внешнюю стену.&lt;/p&gt;
  &lt;h2 id=&quot;SpBn&quot;&gt;Сердце игры — класс Board&lt;/h2&gt;
  &lt;p id=&quot;PrdQ&quot;&gt;Теперь создадим класс игрового поля. Он будет хранить стены, цели, ящики,&lt;br /&gt;позицию игрока и историю ходов.&lt;/p&gt;
  &lt;p id=&quot;A70D&quot;&gt;Для начала опишем результат одного движения:&lt;/p&gt;
  &lt;pre id=&quot;CVqM&quot; data-lang=&quot;cpp&quot;&gt;using CellSet = std::set&amp;lt;Cell&amp;gt;;

struct MoveResult {
    bool moved{false};
    bool pushedBox{false};
    Cell playerFrom{};
    Cell playerTo{};
    std::optional&amp;lt;Cell&amp;gt; boxFrom;
    std::optional&amp;lt;Cell&amp;gt; boxTo;
    bool levelCompleted{false};
};&lt;/pre&gt;
  &lt;p id=&quot;4OqC&quot;&gt;Можно было вернуть из &lt;code&gt;move()&lt;/code&gt; обычный &lt;code&gt;bool&lt;/code&gt;, но тогда отрисовка не знала бы,&lt;br /&gt;откуда и куда переместился объект. &lt;code&gt;MoveResult&lt;/code&gt; пригодится для плавной анимации&lt;br /&gt;и при этом не заставит &lt;code&gt;Board&lt;/code&gt; заниматься графикой.&lt;/p&gt;
  &lt;h3 id=&quot;OLW7&quot;&gt;Board.hpp&lt;/h3&gt;
  &lt;pre id=&quot;niuw&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;quot;entities/Cell.hpp&amp;quot;
#include &amp;quot;levels/LevelDefinition.hpp&amp;quot;

#include &amp;lt;optional&amp;gt;
#include &amp;lt;set&amp;gt;
#include &amp;lt;vector&amp;gt;

using CellSet = std::set&amp;lt;Cell&amp;gt;;

class Board {
public:
    explicit Board(LevelDefinition definition);

    [[nodiscard]] MoveResult move(Direction direction);
    [[nodiscard]] MoveResult undo();
    void restart();

    [[nodiscard]] bool isComplete() const noexcept;
    [[nodiscard]] bool isWall(Cell cell) const noexcept;
    [[nodiscard]] bool hasBox(Cell cell) const noexcept;
    [[nodiscard]] bool isGoal(Cell cell) const noexcept;

    [[nodiscard]] Cell player() const noexcept;
    [[nodiscard]] const CellSet&amp;amp; walls() const noexcept;
    [[nodiscard]] const CellSet&amp;amp; goals() const noexcept;
    [[nodiscard]] const CellSet&amp;amp; boxes() const noexcept;

private:
    struct Snapshot {
        Cell player;
        CellSet boxes;
        int moves{0};
        int pushes{0};
    };

    void parseAndValidate();

    LevelDefinition definition_;
    CellSet walls_;
    CellSet goals_;
    CellSet initialBoxes_;
    CellSet boxes_;
    Cell initialPlayer_{};
    Cell player_{};
    std::vector&amp;lt;Snapshot&amp;gt; history_;
    int moves_{0};
    int pushes_{0};
};&lt;/pre&gt;
  &lt;p id=&quot;lTle&quot;&gt;В заголовке нет ни одного типа SFML. Именно этого мы и добивались.&lt;/p&gt;
  &lt;h2 id=&quot;9h46&quot;&gt;Разбираем карту и проверяем ошибки&lt;/h2&gt;
  &lt;p id=&quot;H8nd&quot;&gt;В конструкторе сохраняем определение, разбираем символы и запускаем уровень:&lt;/p&gt;
  &lt;pre id=&quot;cOwa&quot; data-lang=&quot;cpp&quot;&gt;Board::Board(LevelDefinition definition)
    : definition_(std::move(definition))
{
    parseAndValidate();
    restart();
}&lt;/pre&gt;
  &lt;p id=&quot;hWhk&quot;&gt;Основная часть разбора выглядит так:&lt;/p&gt;
  &lt;pre id=&quot;AimG&quot; data-lang=&quot;cpp&quot;&gt;for (int y = 0; y &amp;lt; height_; ++y) {
    const auto&amp;amp; row = definition_.map[static_cast&amp;lt;std::size_t&amp;gt;(y)];

    if (static_cast&amp;lt;int&amp;gt;(row.size()) != width_) {
        throw std::invalid_argument(
            &amp;quot;Every level row must have the same width&amp;quot;);
    }

    for (int x = 0; x &amp;lt; width_; ++x) {
        const char tile = row[static_cast&amp;lt;std::size_t&amp;gt;(x)];
        const Cell cell{x, y};

        if (tile == &amp;#x27;#&amp;#x27;) {
            walls_.insert(cell);
        }
        if (tile == &amp;#x27;.&amp;#x27; || tile == &amp;#x27;*&amp;#x27; || tile == &amp;#x27;+&amp;#x27;) {
            goals_.insert(cell);
        }
        if (tile == &amp;#x27;$&amp;#x27; || tile == &amp;#x27;*&amp;#x27;) {
            initialBoxes_.insert(cell);
        }
        if (tile == &amp;#x27;@&amp;#x27; || tile == &amp;#x27;+&amp;#x27;) {
            initialPlayer_ = cell;
            ++playerCount;
        }
    }
}&lt;/pre&gt;
  &lt;p id=&quot;fmZf&quot;&gt;После этого проверяем два важных условия:&lt;/p&gt;
  &lt;pre id=&quot;sC8G&quot; data-lang=&quot;cpp&quot;&gt;if (playerCount != 1) {
    throw std::invalid_argument(
        &amp;quot;Level must contain exactly one player&amp;quot;);
}

if (goals_.empty() || goals_.size() != initialBoxes_.size()) {
    throw std::invalid_argument(
        &amp;quot;The number of boxes and goals must match&amp;quot;);
}&lt;/pre&gt;
  &lt;p id=&quot;m4Qr&quot;&gt;Я также проверяю, что вся внешняя граница карты заполнена стенами. Без такой&lt;br /&gt;проверки игрок может уйти за пределы массива, а это уже совсем другая игра —&lt;br /&gt;скорее побег со склада.&lt;/p&gt;
  &lt;h2 id=&quot;CBt7&quot;&gt;Реализуем движение игрока и ящика&lt;/h2&gt;
  &lt;p id=&quot;YkKK&quot;&gt;Теперь самая интересная часть. Метод &lt;code&gt;move()&lt;/code&gt; должен проверить ход, сохранить&lt;br /&gt;состояние для undo и только потом изменить позиции.&lt;/p&gt;
  &lt;h3 id=&quot;f8fs&quot;&gt;Board::move()&lt;/h3&gt;
  &lt;pre id=&quot;dsu8&quot; data-lang=&quot;cpp&quot;&gt;MoveResult Board::move(Direction direction)
{
    const Cell next = player_ + directionOffset(direction);

    if (isWall(next)) {
        return {};
    }

    const bool pushesBox = hasBox(next);
    const Cell boxDestination = next + directionOffset(direction);

    if (pushesBox
        &amp;amp;&amp;amp; (isWall(boxDestination) || hasBox(boxDestination))) {
        return {};
    }

    history_.push_back({player_, boxes_, moves_, pushes_});

    MoveResult result;
    result.moved = true;
    result.playerFrom = player_;
    result.playerTo = next;

    if (pushesBox) {
        boxes_.erase(next);
        boxes_.insert(boxDestination);
        ++pushes_;

        result.pushedBox = true;
        result.boxFrom = next;
        result.boxTo = boxDestination;
    }

    player_ = next;
    ++moves_;
    result.levelCompleted = isComplete();
    return result;
}&lt;/pre&gt;
  &lt;p id=&quot;wQKd&quot;&gt;Обратите внимание: снимок добавляется в &lt;code&gt;history_&lt;/code&gt; только после всех проверок.&lt;br /&gt;Если игрок нажал клавишу возле стены, состояние не изменилось, поэтому и&lt;br /&gt;отменять здесь нечего.&lt;/p&gt;
  &lt;p id=&quot;y6ac&quot;&gt;Порядок проверки простой:&lt;/p&gt;
  &lt;ol id=&quot;7WSA&quot;&gt;
    &lt;li id=&quot;2RhZ&quot;&gt;Находим соседнюю клетку.&lt;/li&gt;
    &lt;li id=&quot;Hbsh&quot;&gt;Останавливаемся, если это стена.&lt;/li&gt;
    &lt;li id=&quot;T1lS&quot;&gt;Определяем, находится ли там ящик.&lt;/li&gt;
    &lt;li id=&quot;L8UV&quot;&gt;Для ящика проверяем следующую клетку.&lt;/li&gt;
    &lt;li id=&quot;x0Jt&quot;&gt;Сохраняем снимок.&lt;/li&gt;
    &lt;li id=&quot;XvYc&quot;&gt;Изменяем игровое состояние.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;MlPS&quot;&gt;Именно в этом методе живёт почти всё главное правило Sokoban.&lt;/p&gt;
  &lt;h2 id=&quot;0qY2&quot;&gt;Проверяем победу&lt;/h2&gt;
  &lt;p id=&quot;9tb5&quot;&gt;Уровень считается завершённым, если каждый ящик находится в наборе целей:&lt;/p&gt;
  &lt;pre id=&quot;Mlup&quot; data-lang=&quot;cpp&quot;&gt;bool Board::isComplete() const noexcept
{
    return std::all_of(
        boxes_.begin(),
        boxes_.end(),
        [this](Cell box) {
            return goals_.contains(box);
        });
}&lt;/pre&gt;
  &lt;p id=&quot;vv6t&quot;&gt;Благодаря C++20 и методу &lt;code&gt;contains()&lt;/code&gt; код читается почти как обычное&lt;br /&gt;предложение: «все ящики содержатся среди целей».&lt;/p&gt;
  &lt;h2 id=&quot;VKNu&quot;&gt;Добавляем отмену хода&lt;/h2&gt;
  &lt;p id=&quot;VTaq&quot;&gt;Sokoban без undo может быть довольно беспощадным. Один неверный толчок — и&lt;br /&gt;приходится начинать уровень заново. Поэтому сохраним перед каждым успешным&lt;br /&gt;ходом:&lt;/p&gt;
  &lt;ul id=&quot;3Xs0&quot;&gt;
    &lt;li id=&quot;Fco3&quot;&gt;позицию игрока;&lt;/li&gt;
    &lt;li id=&quot;OVUA&quot;&gt;набор ящиков;&lt;/li&gt;
    &lt;li id=&quot;LSkS&quot;&gt;число ходов;&lt;/li&gt;
    &lt;li id=&quot;ziVT&quot;&gt;число толчков.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;0QTr&quot;&gt;Само восстановление получается небольшим:&lt;/p&gt;
  &lt;pre id=&quot;6Zi8&quot; data-lang=&quot;cpp&quot;&gt;MoveResult Board::undo()
{
    if (history_.empty()) {
        return {};
    }

    const Snapshot snapshot = history_.back();
    history_.pop_back();

    MoveResult result = transitionTo(snapshot);
    player_ = snapshot.player;
    boxes_ = snapshot.boxes;
    moves_ = snapshot.moves;
    pushes_ = snapshot.pushes;
    return result;
}&lt;/pre&gt;
  &lt;p id=&quot;mIeC&quot;&gt;Метод &lt;code&gt;transitionTo()&lt;/code&gt; сравнивает текущий и предыдущий наборы ящиков. Так мы&lt;br /&gt;узнаём, двигался ли ящик при отмене, и можем анимировать его возвращение.&lt;/p&gt;
  &lt;p id=&quot;dB5p&quot;&gt;Для трёх небольших уровней полная копия &lt;code&gt;std::set&lt;/code&gt; вполне подходит. Если Вы&lt;br /&gt;создаёте огромную карту и храните тысячи ходов, можно перейти к компактным&lt;br /&gt;командам, но начинать проект с такой оптимизации точно не нужно.&lt;/p&gt;
  &lt;h2 id=&quot;6ngh&quot;&gt;Управляем уровнями&lt;/h2&gt;
  &lt;p id=&quot;bvN0&quot;&gt;&lt;code&gt;Board&lt;/code&gt; отвечает за одну карту. Кампанией занимается &lt;code&gt;LevelManager&lt;/code&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;3aSM&quot;&gt;LevelManager.hpp&lt;/h3&gt;
  &lt;pre id=&quot;zuDT&quot; data-lang=&quot;cpp&quot;&gt;class LevelManager {
public:
    LevelManager();

    [[nodiscard]] MoveResult move(Direction direction);
    [[nodiscard]] MoveResult undo();

    void resetCurrentLevel();
    void restartCampaign();
    [[nodiscard]] bool loadNextLevel();
    [[nodiscard]] bool selectLevel(std::size_t index);

    [[nodiscard]] std::size_t currentLevelIndex() const noexcept;
    [[nodiscard]] std::size_t levelCount() const noexcept;
    [[nodiscard]] bool isLastLevel() const noexcept;
    [[nodiscard]] const Board&amp;amp; board() const noexcept;

private:
    void createLevelDefinitions();
    void loadLevel(std::size_t index);

    std::vector&amp;lt;LevelDefinition&amp;gt; definitions_;
    std::unique_ptr&amp;lt;Board&amp;gt; board_;
    std::size_t currentIndex_{0};
};&lt;/pre&gt;
  &lt;p id=&quot;OAUe&quot;&gt;При загрузке создаём новый &lt;code&gt;Board&lt;/code&gt; из определения:&lt;/p&gt;
  &lt;pre id=&quot;qtWH&quot; data-lang=&quot;cpp&quot;&gt;void LevelManager::loadLevel(std::size_t index)
{
    if (index &amp;gt;= definitions_.size()) {
        throw std::out_of_range(&amp;quot;Level index is out of range&amp;quot;);
    }

    currentIndex_ = index;
    board_ = std::make_unique&amp;lt;Board&amp;gt;(definitions_[currentIndex_]);
}&lt;/pre&gt;
  &lt;p id=&quot;42K0&quot;&gt;Менеджер не обрабатывает клавиатуру и ничего не рисует. Его задача — хранить&lt;br /&gt;уровни и активное поле. Чем меньше класс знает о соседях, тем проще его менять.&lt;/p&gt;
  &lt;h2 id=&quot;emux&quot;&gt;Разделяем игру и отрисовку&lt;/h2&gt;
  &lt;p id=&quot;haFL&quot;&gt;Теперь у нас есть полностью рабочая логика, но на экране пока ничего нет.&lt;/p&gt;
  &lt;p id=&quot;w5vf&quot;&gt;Можно передать &lt;code&gt;Board&amp;amp;&lt;/code&gt; в класс отрисовки и позволить ему делать всё подряд.&lt;br /&gt;Но тогда одна случайная строка в рендерере сможет переместить ящик. Лучше&lt;br /&gt;передавать константную ссылку и небольшой снимок анимации:&lt;/p&gt;
  &lt;pre id=&quot;Zjbt&quot; data-lang=&quot;cpp&quot;&gt;struct SceneData {
    const Board&amp;amp; board;
    float totalTime{0.0f};
    float moveProgress{1.0f};
    const MoveResult* transition{nullptr};
};&lt;/pre&gt;
  &lt;p id=&quot;R73w&quot;&gt;&lt;code&gt;SceneRenderer&lt;/code&gt; рисует:&lt;/p&gt;
  &lt;ol id=&quot;i5oK&quot;&gt;
    &lt;li id=&quot;6UXg&quot;&gt;фон;&lt;/li&gt;
    &lt;li id=&quot;VJPQ&quot;&gt;пол;&lt;/li&gt;
    &lt;li id=&quot;V8wN&quot;&gt;цели;&lt;/li&gt;
    &lt;li id=&quot;csxD&quot;&gt;стены;&lt;/li&gt;
    &lt;li id=&quot;2dZk&quot;&gt;ящики;&lt;/li&gt;
    &lt;li id=&quot;Nhxc&quot;&gt;игрока.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;ctbD&quot;&gt;Позиция клетки преобразуется в пиксели только здесь:&lt;/p&gt;
  &lt;pre id=&quot;hYRr&quot; data-lang=&quot;cpp&quot;&gt;sf::Vector2f SceneRenderer::cellPosition(
    const SceneLayout&amp;amp; layout,
    Cell cell)
{
    return layout.boardOrigin
        + sf::Vector2f(
            static_cast&amp;lt;float&amp;gt;(cell.x),
            static_cast&amp;lt;float&amp;gt;(cell.y))
        * layout.cellSize;
}&lt;/pre&gt;
  &lt;p id=&quot;DpbQ&quot;&gt;Получается полезная граница: &lt;code&gt;Board&lt;/code&gt; оперирует клетками, а &lt;code&gt;SceneRenderer&lt;/code&gt; —&lt;br /&gt;пикселями.&lt;/p&gt;
  &lt;h2 id=&quot;T7wx&quot;&gt;Делаем плавное движение&lt;/h2&gt;
  &lt;p id=&quot;7JVQ&quot;&gt;Логика перемещает игрока сразу из одной клетки в другую. Это правильно:&lt;br /&gt;состояние поля всегда однозначно. Но визуально резкие скачки выглядят не очень&lt;br /&gt;приятно.&lt;/p&gt;
  &lt;p id=&quot;QPSd&quot;&gt;Для анимации сохраним &lt;code&gt;MoveResult&lt;/code&gt; и время перехода:&lt;/p&gt;
  &lt;pre id=&quot;owm5&quot; data-lang=&quot;cpp&quot;&gt;std::optional&amp;lt;MoveResult&amp;gt; transition_;
float animationElapsed_{0.0f};&lt;/pre&gt;
  &lt;p id=&quot;1GPc&quot;&gt;Прогресс движения вычислим через &lt;code&gt;smoothstep&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;WfGX&quot; data-lang=&quot;cpp&quot;&gt;float Game::animationProgress() const noexcept
{
    if (!transition_) {
        return 1.0f;
    }

    const float linear = std::clamp(
        animationElapsed_ / GameConfig::MoveAnimationDuration,
        0.0f,
        1.0f);

    return linear * linear * (3.0f - 2.0f * linear);
}&lt;/pre&gt;
  &lt;p id=&quot;hQDE&quot;&gt;После этого рендерер интерполирует координаты:&lt;/p&gt;
  &lt;pre id=&quot;fPoM&quot; data-lang=&quot;cpp&quot;&gt;sf::Vector2f interpolate(Cell from, Cell to, float amount)
{
    return {
        static_cast&amp;lt;float&amp;gt;(from.x)
            + static_cast&amp;lt;float&amp;gt;(to.x - from.x) * amount,
        static_cast&amp;lt;float&amp;gt;(from.y)
            + static_cast&amp;lt;float&amp;gt;(to.y - from.y) * amount
    };
}&lt;/pre&gt;
  &lt;p id=&quot;KHEF&quot;&gt;Игровое правило при этом не меняется. &lt;code&gt;Board&lt;/code&gt; уже выполнил ход, а мы только&lt;br /&gt;показываем красивый переход.&lt;/p&gt;
  &lt;h2 id=&quot;eIAm&quot;&gt;Рисуем HUD отдельно&lt;/h2&gt;
  &lt;p id=&quot;0Bej&quot;&gt;Текстовый интерфейс лучше не смешивать со стенами и ящиками. Создадим снимок&lt;br /&gt;данных:&lt;/p&gt;
  &lt;pre id=&quot;Yfwe&quot; data-lang=&quot;cpp&quot;&gt;struct HudData {
    std::size_t levelNumber{1};
    std::size_t levelCount{1};
    std::wstring levelName;
    std::wstring levelSubtitle;
    int moves{0};
    int pushes{0};
    bool boardComplete{false};
    bool showCompletion{false};
    GameState state{GameState::Playing};
    sf::FloatRect panel{};
};&lt;/pre&gt;
  &lt;p id=&quot;h8Qm&quot;&gt;Теперь &lt;code&gt;Hud&lt;/code&gt; не получает доступ к &lt;code&gt;Game&lt;/code&gt; и не может менять уровень. Он знает&lt;br /&gt;только те значения, которые действительно нужны для текущего кадра.&lt;/p&gt;
  &lt;h2 id=&quot;8SvQ&quot;&gt;Загружаем системный шрифт&lt;/h2&gt;
  &lt;p id=&quot;kkXg&quot;&gt;С русским текстом иногда возникает знакомая проблема: окно работает, панели&lt;br /&gt;рисуются, а вместо букв появляются квадраты. Причина почти всегда в шрифте.&lt;/p&gt;
  &lt;p id=&quot;Fm6c&quot;&gt;Чтобы не хранить отдельный TTF-файл возле программы, найдём системный каталог&lt;br /&gt;Windows:&lt;/p&gt;
  &lt;pre id=&quot;WBpr&quot; data-lang=&quot;cpp&quot;&gt;std::filesystem::path SystemFontLoader::systemFontDirectory()
{
#ifdef _WIN32
    std::array&amp;lt;wchar_t, MAX_PATH&amp;gt; buffer{};
    const UINT length = GetWindowsDirectoryW(
        buffer.data(),
        static_cast&amp;lt;UINT&amp;gt;(buffer.size()));

    if (length &amp;gt; 0 &amp;amp;&amp;amp; length &amp;lt; buffer.size()) {
        return std::filesystem::path(buffer.data()) / L&amp;quot;Fonts&amp;quot;;
    }
#endif
    return {};
}&lt;/pre&gt;
  &lt;p id=&quot;ESXt&quot;&gt;Затем пробуем известные шрифты с кириллицей:&lt;/p&gt;
  &lt;pre id=&quot;YsmC&quot; data-lang=&quot;cpp&quot;&gt;for (const wchar_t* filename : {
         L&amp;quot;segoeui.ttf&amp;quot;,
         L&amp;quot;tahoma.ttf&amp;quot;,
         L&amp;quot;arial.ttf&amp;quot;,
         L&amp;quot;calibri.ttf&amp;quot;,
         L&amp;quot;verdana.ttf&amp;quot;}) {
    result.push_back(windowsFonts / filename);
}&lt;/pre&gt;
  &lt;p id=&quot;WIhS&quot;&gt;В нашем случае загружается &lt;code&gt;Segoe UI&lt;/code&gt;, который уже установлен в Windows. Если&lt;br /&gt;стандартные имена не найдены, можно пройти по каталогу &lt;code&gt;Fonts&lt;/code&gt; и попробовать&lt;br /&gt;другой TTF или OTF-файл.&lt;/p&gt;
  &lt;h2 id=&quot;Xqeh&quot;&gt;Собираем всё в классе Game&lt;/h2&gt;
  &lt;p id=&quot;F98A&quot;&gt;&lt;code&gt;Game&lt;/code&gt; — координатор. Он создаёт окно, принимает события, обновляет анимацию и&lt;br /&gt;вызывает отрисовку.&lt;/p&gt;
  &lt;p id=&quot;2BuI&quot;&gt;Главный цикл выглядит привычно:&lt;/p&gt;
  &lt;pre id=&quot;Tozr&quot; data-lang=&quot;cpp&quot;&gt;void Game::run()
{
    while (window_.isOpen()) {
        processEvents();

        const float deltaTime = std::min(
            frameClock_.restart().asSeconds(),
            GameConfig::MaximumDeltaTime);

        update(deltaTime);
        render();
    }
}&lt;/pre&gt;
  &lt;p id=&quot;o32Q&quot;&gt;Обработка клавиши переводит код SFML в наше доменное направление:&lt;/p&gt;
  &lt;pre id=&quot;VRwq&quot; data-lang=&quot;cpp&quot;&gt;if (key == sf::Keyboard::Up || key == sf::Keyboard::W) {
    movePlayer(Direction::Up);
}
else if (key == sf::Keyboard::Down || key == sf::Keyboard::S) {
    movePlayer(Direction::Down);
}
else if (key == sf::Keyboard::Left || key == sf::Keyboard::A) {
    movePlayer(Direction::Left);
}
else if (key == sf::Keyboard::Right || key == sf::Keyboard::D) {
    movePlayer(Direction::Right);
}&lt;/pre&gt;
  &lt;p id=&quot;29uM&quot;&gt;А &lt;code&gt;movePlayer()&lt;/code&gt; обращается к менеджеру уровня:&lt;/p&gt;
  &lt;pre id=&quot;8jAi&quot; data-lang=&quot;cpp&quot;&gt;void Game::movePlayer(Direction direction)
{
    const MoveResult result = levelManager_.move(direction);
    startAnimation(result);

    if (result.levelCompleted) {
        state_ = levelManager_.isLastLevel()
            ? GameState::CampaignComplete
            : GameState::LevelComplete;

        completionDelay_ = GameConfig::CompletionRevealDelay;
        completionVisible_ = false;
    }
}&lt;/pre&gt;
  &lt;p id=&quot;POgD&quot;&gt;Обратите внимание, как обязанности проходят по цепочке:&lt;/p&gt;
  &lt;pre id=&quot;pcWy&quot;&gt;Клавиатура -&amp;gt; Game -&amp;gt; LevelManager -&amp;gt; Board&lt;/pre&gt;
  &lt;p id=&quot;c3ml&quot;&gt;В обратную сторону возвращается &lt;code&gt;MoveResult&lt;/code&gt;, который используется для&lt;br /&gt;анимации и проверки победы.&lt;/p&gt;
  &lt;h2 id=&quot;XeHy&quot;&gt;Точка входа&lt;/h2&gt;
  &lt;p id=&quot;o6z8&quot;&gt;После такого разделения &lt;code&gt;main.cpp&lt;/code&gt; остаётся очень маленьким:&lt;/p&gt;
  &lt;pre id=&quot;j4Yt&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;core/Game.hpp&amp;quot;

#include &amp;lt;exception&amp;gt;
#include &amp;lt;iostream&amp;gt;

int main()
{
    try {
        Game game;
        game.run();
    }
    catch (const std::exception&amp;amp; exception) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Sokoban startup error: &amp;quot;
                  &amp;lt;&amp;lt; exception.what() &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
        return 1;
    }

    return 0;
}&lt;/pre&gt;
  &lt;p id=&quot;bcdX&quot;&gt;Так и должно быть. Точка входа создаёт приложение, запускает его и обрабатывает&lt;br /&gt;критическую ошибку запуска.&lt;/p&gt;
  &lt;h2 id=&quot;KHow&quot;&gt;Проверяем уровни автоматически&lt;/h2&gt;
  &lt;p id=&quot;hCjB&quot;&gt;Нарисовать красивый лабиринт недостаточно — он должен иметь решение. Причём&lt;br /&gt;желательно узнать об ошибке до того, как уровень попадёт к игроку.&lt;/p&gt;
  &lt;p id=&quot;7SgB&quot;&gt;Для каждой карты я сохранил проверенную последовательность команд:&lt;/p&gt;
  &lt;pre id=&quot;7ghl&quot; data-lang=&quot;cpp&quot;&gt;const std::vector&amp;lt;std::string&amp;gt; verifiedSolutions = {
    &amp;quot;LLLURDRUURULLRDDDRRRRUU&amp;quot;,
    &amp;quot;LLLLLULLLULLDRRRRDRUDRRURDRULLULLDLUUULUULRRDLDRRRRDRUU&amp;quot;,
    &amp;quot;DLLLULDDDDDLDRRRDRULLLLLDLLLDLUURUUURUULRDDRRRRDRUU&amp;quot;
};&lt;/pre&gt;
  &lt;p id=&quot;veAJ&quot;&gt;Буквы означают направления &lt;code&gt;Up&lt;/code&gt;, &lt;code&gt;Down&lt;/code&gt;, &lt;code&gt;Left&lt;/code&gt;, &lt;code&gt;Right&lt;/code&gt;. Тест загружает каждый&lt;br /&gt;уровень, выполняет все команды и проверяет результат:&lt;/p&gt;
  &lt;pre id=&quot;F65W&quot; data-lang=&quot;cpp&quot;&gt;for (std::size_t index = 0;
     index &amp;lt; verifiedSolutions.size();
     ++index) {
    levels.selectLevel(index);

    for (const char command : verifiedSolutions[index]) {
        const MoveResult result = levels.move(
            parseDirection(command));

        require(result.moved,
                &amp;quot;A verified solution contains an illegal move&amp;quot;);
    }

    require(levels.board().isComplete(),
            &amp;quot;A verified solution did not complete its level&amp;quot;);
}&lt;/pre&gt;
  &lt;p id=&quot;Roxd&quot;&gt;Отдельно проверяются undo, счётчики ходов, перезапуск и выбор уровня. В этом и&lt;br /&gt;есть практическая польза отделения логики от SFML: тесту не нужно открывать&lt;br /&gt;окно и рисовать ни одного прямоугольника.&lt;/p&gt;
  &lt;h2 id=&quot;pyZ4&quot;&gt;Настраиваем CMake&lt;/h2&gt;
  &lt;p id=&quot;YyLI&quot;&gt;Доменную часть удобно собрать в отдельную библиотеку:&lt;/p&gt;
  &lt;pre id=&quot;vCsd&quot; data-lang=&quot;cmake&quot;&gt;add_library(sokoban_domain STATIC
    src/entities/Board.cpp
    src/entities/Board.hpp
    src/entities/Cell.hpp
    src/levels/LevelDefinition.hpp
    src/levels/LevelManager.cpp
    src/levels/LevelManager.hpp
)

target_include_directories(sokoban_domain
    PUBLIC &amp;quot;${CMAKE_CURRENT_SOURCE_DIR}/src&amp;quot;)&lt;/pre&gt;
  &lt;p id=&quot;Ozsc&quot;&gt;Исполняемый файл уже зависит от SFML:&lt;/p&gt;
  &lt;pre id=&quot;rmqy&quot; data-lang=&quot;cmake&quot;&gt;add_executable(Sokoban
    src/main.cpp
    src/core/Game.cpp
    src/rendering/SceneRenderer.cpp
    src/resources/SystemFontLoader.cpp
    src/ui/Hud.cpp
)

target_link_libraries(Sokoban PRIVATE
    sokoban_domain
    sfml-graphics
    sfml-window
    sfml-system
)&lt;/pre&gt;
  &lt;p id=&quot;QKwr&quot;&gt;А тесты используют только доменный слой:&lt;/p&gt;
  &lt;pre id=&quot;MHWt&quot; data-lang=&quot;cmake&quot;&gt;add_executable(sokoban_tests tests/SokobanTests.cpp)
target_link_libraries(sokoban_tests PRIVATE sokoban_domain)
add_test(NAME sokoban_domain_levels_and_undo
         COMMAND sokoban_tests)&lt;/pre&gt;
  &lt;h2 id=&quot;tYOZ&quot;&gt;Собираем и запускаем проект&lt;/h2&gt;
  &lt;p id=&quot;PYZF&quot;&gt;Для Visual Studio 2022:&lt;/p&gt;
  &lt;ol id=&quot;Oifi&quot;&gt;
    &lt;li id=&quot;32LQ&quot;&gt;Откройте &lt;code&gt;Sokoban.sln&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;0bc3&quot;&gt;Выберите платформу &lt;code&gt;x64&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;OZFl&quot;&gt;Выберите &lt;code&gt;Debug&lt;/code&gt; или &lt;code&gt;Release&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;mEot&quot;&gt;Нажмите &lt;code&gt;Ctrl+Shift+B&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;9y5k&quot;&gt;Запустите проект.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;NgXJ&quot;&gt;Альтернативная сборка через CMake:&lt;/p&gt;
  &lt;pre id=&quot;OT0I&quot; data-lang=&quot;shell&quot;&gt;cmake -S . -B build-cmake -A x64
cmake --build build-cmake --config Release
ctest --test-dir build-cmake -C Release --output-on-failure
.\build-cmake\Release\Sokoban.exe&lt;/pre&gt;
  &lt;p id=&quot;LvY7&quot;&gt;Если SFML установлена в другом месте:&lt;/p&gt;
  &lt;pre id=&quot;aKE9&quot; data-lang=&quot;shell&quot;&gt;cmake -S . -B build-cmake -A x64 &amp;#x60;
  -DSFML_DIR=&amp;quot;D:/Libraries/SFML/lib/cmake/SFML&amp;quot;&lt;/pre&gt;
  &lt;h2 id=&quot;BObD&quot;&gt;Что получилось&lt;/h2&gt;
  &lt;p id=&quot;l651&quot;&gt;В итоге у нас есть не просто перемещение квадратиков по экрану, а законченный&lt;br /&gt;небольшой проект:&lt;/p&gt;
  &lt;ul id=&quot;IILd&quot;&gt;
    &lt;li id=&quot;4lR0&quot;&gt;три лабиринтных уровня;&lt;/li&gt;
    &lt;li id=&quot;uo9X&quot;&gt;правила классического Sokoban;&lt;/li&gt;
    &lt;li id=&quot;tDsl&quot;&gt;отмена последнего хода;&lt;/li&gt;
    &lt;li id=&quot;NPgy&quot;&gt;перезапуск и выбор уровня;&lt;/li&gt;
    &lt;li id=&quot;an9s&quot;&gt;счётчик ходов и толчков;&lt;/li&gt;
    &lt;li id=&quot;D8mY&quot;&gt;плавное движение игрока и ящиков;&lt;/li&gt;
    &lt;li id=&quot;sYyB&quot;&gt;русский интерфейс на системном шрифте;&lt;/li&gt;
    &lt;li id=&quot;9LdQ&quot;&gt;автоматическая проверка всех решений;&lt;/li&gt;
    &lt;li id=&quot;SYzM&quot;&gt;разделение логики, уровней, отрисовки и UI.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;R1gm&quot;&gt;Мне особенно нравится, что самая важная часть игры — &lt;code&gt;Board&lt;/code&gt; — получилась&lt;br /&gt;полностью независимой от SFML. Интерфейс можно перерисовать, библиотеку можно&lt;br /&gt;заменить, а правила Sokoban останутся прежними.&lt;/p&gt;
  &lt;p id=&quot;7vvu&quot;&gt;Конечно, проект можно развивать дальше. Добавить редактор уровней, загрузку карт&lt;br /&gt;из файлов, звук движения ящиков, сохранение прогресса, главное меню и таблицу&lt;br /&gt;лучших результатов. Но даже в текущем виде это хороший пример того, как из&lt;br /&gt;простого правила постепенно вырастает полноценная игра.&lt;/p&gt;
  &lt;p id=&quot;4PIZ&quot;&gt;Надеюсь, у Вас всё получилось. Если появились вопросы или Вы нашли интересный способ улучшить архитектуру — пишите в комментариях и делитесь своими идеями!&lt;/p&gt;
  &lt;p id=&quot;9nbc&quot;&gt;&lt;a href=&quot;https://github.com/cnet-sudo/Sokoban.git&quot; target=&quot;_blank&quot;&gt;Клонировать репозиторий&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;M3NW&quot;&gt;&lt;a href=&quot;https://t.me/C_Verhovcevo_NVK&quot; target=&quot;_blank&quot;&gt;Наш Телеграмм&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;vvNL&quot;&gt;&lt;a href=&quot;https://www.youtube.com/@%D0%98%D1%81%D0%BA%D1%83%D1%81%D1%81%D1%82%D0%B2%D0%BE%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%D0%BD%D0%B0%D1%8F%D0%B7%D1%8B&quot; target=&quot;_blank&quot;&gt;Наш Ютюб &lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>gamedeveloper:HDvDdtYnlAN</id><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper/HDvDdtYnlAN?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><title>Разработка игры «Рикошет во тьме» на SFML C++</title><published>2026-08-02T17:51:50.457Z</published><updated>2026-08-02T19:28:17.889Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/d7/00/d700074d-44bc-4cff-8ff0-6e4ad1127259.png"></media:thumbnail><category term="mul-timedijnaya-biblioteka-sfml-c" label="Мультимедийная библиотека  SFML C++"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/8e/86/8e862f89-c733-4d99-ab9c-65f7010089cc.png&quot;&gt;Привет, разработчики и любители экспериментов с графикой! Сегодня хочу рассказать о создании небольшой, но атмосферной игры «Рикошет во тьме» на C++20 с использованием SFML.</summary><content type="html">
  &lt;p id=&quot;CcHh&quot;&gt;Привет, разработчики и любители экспериментов с графикой! Сегодня хочу рассказать о создании небольшой, но атмосферной игры «Рикошет во тьме» на C++20 с использованием SFML.&lt;/p&gt;
  &lt;figure id=&quot;Lhrx&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/8e/86/8e862f89-c733-4d99-ab9c-65f7010089cc.png&quot; width=&quot;1200&quot; /&gt;
    &lt;figcaption&gt;Концепт игрового поля «Рикошет во тьме»&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;yaeC&quot;&gt;Идея родилась из простого вопроса: можно ли построить игровой процесс вокруг одного физического действия — отражения шара от поверхности? Чтобы механика не превратилась в обычный экранный пинг-понг, я добавил темноту, ограниченную область видимости и цели, которые игрок должен находить по вспышкам света.&lt;/p&gt;
  &lt;p id=&quot;cPGi&quot;&gt;В этой статье мы разберём архитектуру проекта, игровой цикл, движение светящегося снаряда, отражение от стен и препятствий, уничтожение целей и простую систему освещения. Проект хорошо подходит тем, кто уже знает основы C++, но хочет перейти от отдельных упражнений к небольшой законченной игре.&lt;/p&gt;
  &lt;p id=&quot;3zAk&quot;&gt;Давайте начнём!&lt;/p&gt;
  &lt;h2 id=&quot;jSUW&quot;&gt;Идея игры&lt;/h2&gt;
  &lt;p id=&quot;t7c6&quot;&gt;Игрок запускает светящийся шар в тёмное помещение. Шар движется с постоянной скоростью, рикошетит от стен и препятствий и на короткое время освещает пространство вокруг себя. Задача — уничтожить все спрятанные цели за ограниченное количество запусков.&lt;/p&gt;
  &lt;p id=&quot;mrwF&quot;&gt;Основные правила получились такими:&lt;/p&gt;
  &lt;ul id=&quot;GatY&quot;&gt;
    &lt;li id=&quot;6Stw&quot;&gt;направление выстрела задаётся мышью;&lt;/li&gt;
    &lt;li id=&quot;Jr9s&quot;&gt;левая кнопка мыши запускает шар;&lt;/li&gt;
    &lt;li id=&quot;O06L&quot;&gt;после столкновения со стеной или препятствием шар отражается;&lt;/li&gt;
    &lt;li id=&quot;08rQ&quot;&gt;при попадании цель исчезает, а игрок получает очки;&lt;/li&gt;
    &lt;li id=&quot;mKoy&quot;&gt;клавиша R перезапускает уровень;&lt;/li&gt;
    &lt;li id=&quot;3yXW&quot;&gt;Esc возвращает в меню или закрывает игру.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;kgMf&quot;&gt;На первый взгляд всё просто. Но именно в таких проектах хорошо видны важные темы игровой разработки: работа со временем, нормализация векторов, проверка столкновений, разделение ответственности между классами и создание визуальной атмосферы.&lt;/p&gt;
  &lt;h2 id=&quot;XXUu&quot;&gt;Подготовка проекта&lt;/h2&gt;
  &lt;p id=&quot;Cl0y&quot;&gt;Для разработки я использовал C++20 и SFML 2.6. Проект можно собрать в Visual Studio, CLion или через CMake.&lt;/p&gt;
  &lt;p id=&quot;OJnu&quot;&gt;Нам понадобятся модули:&lt;/p&gt;
  &lt;ul id=&quot;BMHV&quot;&gt;
    &lt;li id=&quot;pWOx&quot;&gt;&lt;strong&gt;sfml-graphics&lt;/strong&gt; — отрисовка объектов и света;&lt;/li&gt;
    &lt;li id=&quot;AuPH&quot;&gt;&lt;strong&gt;sfml-window&lt;/strong&gt; — окно и события;&lt;/li&gt;
    &lt;li id=&quot;UNjy&quot;&gt;&lt;strong&gt;sfml-system&lt;/strong&gt; — время и векторы;&lt;/li&gt;
    &lt;li id=&quot;xD90&quot;&gt;&lt;strong&gt;sfml-audio&lt;/strong&gt; — звуки выстрела и столкновений.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;5sjV&quot;&gt;&lt;strong&gt;CMakeLists.txt&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;7Hji&quot; data-lang=&quot;cmake&quot;&gt;cmake_minimum_required(VERSION 3.20)
project(RicochetInTheDark)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(SFML 2.6 COMPONENTS
    graphics
    window
    system
    audio
    REQUIRED
)

add_executable(RicochetInTheDark
    src/main.cpp
    src/Game.cpp
    src/Ball.cpp
    src/Obstacle.cpp
    src/Target.cpp
    src/LightSystem.cpp
)

target_link_libraries(RicochetInTheDark PRIVATE
    sfml-graphics
    sfml-window
    sfml-system
    sfml-audio
)&lt;/pre&gt;
  &lt;p id=&quot;1KMY&quot;&gt;Если SFML подключена правильно, сначала откройте обычное окно. Это маленькая проверка, которая экономит много времени: лучше сразу убедиться, что компилятор видит заголовочные файлы и библиотеки.&lt;/p&gt;
  &lt;h2 id=&quot;xnr1&quot;&gt;Структура проекта&lt;/h2&gt;
  &lt;p id=&quot;qIDZ&quot;&gt;Я не стал помещать всю игру в один main.cpp. Для маленького прототипа это кажется удобным, но уже через несколько механик файл превращается в тёмный лабиринт — причём без красивых световых эффектов.&lt;/p&gt;
  &lt;pre id=&quot;kxHB&quot;&gt;RicochetInTheDark/
├── assets/
│   ├── fonts/
│   ├── sounds/
│   └── shaders/
│       └── light.frag
├── src/
│   ├── main.cpp
│   ├── Game.h
│   ├── Game.cpp
│   ├── Ball.h
│   ├── Ball.cpp
│   ├── Obstacle.h
│   ├── Obstacle.cpp
│   ├── Target.h
│   ├── Target.cpp
│   ├── LightSystem.h
│   └── LightSystem.cpp
└── CMakeLists.txt&lt;/pre&gt;
  &lt;p id=&quot;I1H8&quot;&gt;Класс &lt;code&gt;Game&lt;/code&gt; управляет игровым циклом. &lt;code&gt;Ball&lt;/code&gt; отвечает за движение и отражение. &lt;code&gt;Obstacle&lt;/code&gt; хранит геометрию препятствия, &lt;code&gt;Target&lt;/code&gt; — состояние цели, а &lt;code&gt;LightSystem&lt;/code&gt; создаёт карту освещения.&lt;/p&gt;
  &lt;h2 id=&quot;MzDi&quot;&gt;Главный игровой цикл&lt;/h2&gt;
  &lt;p id=&quot;0vaT&quot;&gt;Основа любой игры на SFML — цикл обработки событий, обновления и отрисовки. Для движения обязательно используем &lt;code&gt;deltaTime&lt;/code&gt;, иначе скорость шара будет зависеть от частоты кадров.&lt;/p&gt;
  &lt;p id=&quot;vLYg&quot;&gt;&lt;strong&gt;main.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;hngx&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;quot;Game.h&amp;quot;

int main()
{
    sf::RenderWindow window(
        sf::VideoMode(1280, 720),
        &amp;quot;Рикошет во тьме&amp;quot;,
        sf::Style::Close
    );

    window.setFramerateLimit(144);

    Game game(window);
    sf::Clock clock;

    while (window.isOpen())
    {
        const float deltaTime = clock.restart().asSeconds();

        sf::Event event{};
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
                window.close();

            game.handleEvent(event);
        }

        game.update(deltaTime);

        window.clear(sf::Color(5, 7, 12));
        game.render();
        window.display();
    }

    return 0;
}&lt;/pre&gt;
  &lt;p id=&quot;MvvH&quot;&gt;Здесь важно соблюдать порядок: сначала читаем ввод, затем изменяем состояние мира и только после этого рисуем новый кадр.&lt;/p&gt;
  &lt;h2 id=&quot;IXYh&quot;&gt;Создаём светящийся шар&lt;/h2&gt;
  &lt;p id=&quot;EDWf&quot;&gt;Шар хранит позицию, скорость, радиус и флаг активности. Пока игрок выбирает направление, шар остаётся на стартовой точке. После выстрела он начинает двигаться.&lt;/p&gt;
  &lt;p id=&quot;nETC&quot;&gt;&lt;strong&gt;Ball.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;UR1d&quot; data-lang=&quot;cpp&quot;&gt;#pragma once
#include &amp;lt;SFML/Graphics.hpp&amp;gt;

class Ball : public sf::Drawable
{
public:
    explicit Ball(float radius = 12.0f);

    void launch(const sf::Vector2f&amp;amp; direction, float speed);
    void reset(const sf::Vector2f&amp;amp; position);
    void setPosition(const sf::Vector2f&amp;amp; position);
    void update(float deltaTime);

    void reflect(const sf::Vector2f&amp;amp; normal);

    [[nodiscard]] bool isActive() const;
    [[nodiscard]] float getRadius() const;
    [[nodiscard]] sf::Vector2f getPosition() const;
    [[nodiscard]] sf::Vector2f getVelocity() const;

private:
    sf::CircleShape shape;
    sf::Vector2f velocity{};
    bool active{false};

    void draw(
        sf::RenderTarget&amp;amp; target,
        sf::RenderStates states
    ) const override;
};&lt;/pre&gt;
  &lt;p id=&quot;dLQq&quot;&gt;&lt;strong&gt;Ball.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;wIog&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;Ball.h&amp;quot;
#include &amp;lt;cmath&amp;gt;

namespace
{
    sf::Vector2f normalize(const sf::Vector2f&amp;amp; vector)
    {
        const float length = std::sqrt(
            vector.x * vector.x + vector.y * vector.y
        );

        if (length == 0.0f)
            return {0.0f, 0.0f};

        return vector / length;
    }

    float dot(
        const sf::Vector2f&amp;amp; left,
        const sf::Vector2f&amp;amp; right
    )
    {
        return left.x * right.x + left.y * right.y;
    }
}

Ball::Ball(float radius)
{
    shape.setRadius(radius);
    shape.setOrigin(radius, radius);
    shape.setFillColor(sf::Color(190, 235, 255));
    shape.setOutlineThickness(3.0f);
    shape.setOutlineColor(sf::Color(80, 180, 255));
}

void Ball::launch(
    const sf::Vector2f&amp;amp; direction,
    float speed
)
{
    velocity = normalize(direction) * speed;
    active = true;
}

void Ball::reset(const sf::Vector2f&amp;amp; position)
{
    shape.setPosition(position);
    velocity = {};
    active = false;
}

void Ball::setPosition(const sf::Vector2f&amp;amp; position)
{
    shape.setPosition(position);
}

void Ball::update(float deltaTime)
{
    if (active)
        shape.move(velocity * deltaTime);
}

void Ball::reflect(const sf::Vector2f&amp;amp; normal)
{
    velocity -= 2.0f * dot(velocity, normal) * normal;
}

bool Ball::isActive() const
{
    return active;
}

float Ball::getRadius() const
{
    return shape.getRadius();
}

sf::Vector2f Ball::getPosition() const
{
    return shape.getPosition();
}

sf::Vector2f Ball::getVelocity() const
{
    return velocity;
}

void Ball::draw(
    sf::RenderTarget&amp;amp; target,
    sf::RenderStates states
) const
{
    target.draw(shape, states);
}&lt;/pre&gt;
  &lt;p id=&quot;lLow&quot;&gt;Формула отражения выглядит так:&lt;/p&gt;
  &lt;p id=&quot;Nces&quot;&gt;&lt;code&gt;V′ = V − 2(V · N)N&lt;/code&gt;&lt;/p&gt;
  &lt;p id=&quot;i3eT&quot;&gt;Здесь &lt;code&gt;V&lt;/code&gt; — текущая скорость, а &lt;code&gt;N&lt;/code&gt; — нормаль поверхности. Эта формула универсальна: она работает и для горизонтальной стены, и для вертикальной, и для наклонных поверхностей.&lt;/p&gt;
  &lt;h2 id=&quot;EeLE&quot;&gt;Запуск шара мышью&lt;/h2&gt;
  &lt;p id=&quot;eJBy&quot;&gt;При нажатии левой кнопки мыши вычисляем направление от стартовой позиции к курсору.&lt;/p&gt;
  &lt;pre id=&quot;8VnU&quot; data-lang=&quot;cpp&quot;&gt;void Game::handleEvent(const sf::Event&amp;amp; event)
{
    if (event.type == sf::Event::KeyPressed)
    {
        if (event.key.code == sf::Keyboard::Escape)
            window.close();

        if (event.key.code == sf::Keyboard::R)
            restartLevel();
    }

    if (event.type == sf::Event::MouseButtonPressed
        &amp;amp;&amp;amp; event.mouseButton.button == sf::Mouse::Left
        &amp;amp;&amp;amp; !ball.isActive())
    {
        const sf::Vector2f mousePosition =
            window.mapPixelToCoords({
                event.mouseButton.x,
                event.mouseButton.y
            });

        const sf::Vector2f direction =
            mousePosition - ball.getPosition();

        ball.launch(direction, 520.0f);
    }
}&lt;/pre&gt;
  &lt;p id=&quot;GOV8&quot;&gt;Перед выстрелом можно рисовать линию прицеливания. Она помогает игроку, но не показывает весь будущий путь — после первого рикошета начинается самое интересное.&lt;/p&gt;
  &lt;h2 id=&quot;rNCx&quot;&gt;Рикошет от границ окна&lt;/h2&gt;
  &lt;p id=&quot;jCYU&quot;&gt;Для стен уровня достаточно проверить положение центра шара с учётом его радиуса. После столкновения меняем направление движения и возвращаем объект внутрь игрового поля.&lt;/p&gt;
  &lt;pre id=&quot;zQm3&quot; data-lang=&quot;cpp&quot;&gt;void Game::handleWindowCollision()
{
    auto position = ball.getPosition();
    const float radius = ball.getRadius();

    if (position.x - radius &amp;lt; 0.0f)
    {
        position.x = radius;
        ball.setPosition(position);
        ball.reflect({1.0f, 0.0f});
    }
    else if (position.x + radius &amp;gt; window.getSize().x)
    {
        position.x = window.getSize().x - radius;
        ball.setPosition(position);
        ball.reflect({-1.0f, 0.0f});
    }

    if (position.y - radius &amp;lt; 0.0f)
    {
        position.y = radius;
        ball.setPosition(position);
        ball.reflect({0.0f, 1.0f});
    }
    else if (position.y + radius &amp;gt; window.getSize().y)
    {
        position.y = window.getSize().y - radius;
        ball.setPosition(position);
        ball.reflect({0.0f, -1.0f});
    }
}&lt;/pre&gt;
  &lt;p id=&quot;iO2F&quot;&gt;Простое отражение скорости без коррекции позиции иногда приводит к дрожанию: шар остаётся внутри стены и на следующем кадре сталкивается с ней снова.&lt;/p&gt;
  &lt;h2 id=&quot;O4DG&quot;&gt;Столкновение с препятствиями&lt;/h2&gt;
  &lt;p id=&quot;KODO&quot;&gt;Препятствия удобно хранить как прямоугольники. Для столкновения круга с прямоугольником находим ближайшую к центру шара точку внутри границ препятствия.&lt;/p&gt;
  &lt;pre id=&quot;8xFx&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;algorithm&amp;gt;

bool intersects(
    const Ball&amp;amp; ball,
    const sf::FloatRect&amp;amp; rectangle
)
{
    const sf::Vector2f center = ball.getPosition();

    const float closestX = std::clamp(
        center.x,
        rectangle.left,
        rectangle.left + rectangle.width
    );

    const float closestY = std::clamp(
        center.y,
        rectangle.top,
        rectangle.top + rectangle.height
    );

    const float dx = center.x - closestX;
    const float dy = center.y - closestY;
    const float radius = ball.getRadius();

    return dx * dx + dy * dy &amp;lt; radius * radius;
}&lt;/pre&gt;
  &lt;p id=&quot;XrYi&quot;&gt;После обнаружения столкновения определяем нормаль. Для прямоугольных препятствий можно сравнить проникновение по осям X и Y. Отражаем скорость по той оси, где проникновение меньше, и выталкиваем шар наружу.&lt;/p&gt;
  &lt;pre id=&quot;zhdU&quot; data-lang=&quot;cpp&quot;&gt;void Game::resolveObstacleCollision(
    const sf::FloatRect&amp;amp; bounds
)
{
    if (!intersects(ball, bounds))
        return;

    auto position = ball.getPosition();

    const sf::Vector2f obstacleCenter{
        bounds.left + bounds.width * 0.5f,
        bounds.top + bounds.height * 0.5f
    };

    const float dx = position.x - obstacleCenter.x;
    const float dy = position.y - obstacleCenter.y;

    const float overlapX =
        bounds.width * 0.5f + ball.getRadius() - std::abs(dx);
    const float overlapY =
        bounds.height * 0.5f + ball.getRadius() - std::abs(dy);

    if (overlapX &amp;lt; overlapY)
    {
        const float sign = dx &amp;lt; 0.0f ? -1.0f : 1.0f;
        position.x += sign * overlapX;
        ball.setPosition(position);
        ball.reflect({sign, 0.0f});
    }
    else
    {
        const float sign = dy &amp;lt; 0.0f ? -1.0f : 1.0f;
        position.y += sign * overlapY;
        ball.setPosition(position);
        ball.reflect({0.0f, sign});
    }
}&lt;/pre&gt;
  &lt;p id=&quot;vbKQ&quot;&gt;Для быстрых объектов позже стоит добавить непрерывную проверку столкновений. Иначе на низкой частоте кадров шар может успеть «проскочить» через тонкую стену.&lt;/p&gt;
  &lt;h2 id=&quot;Ns9z&quot;&gt;Освещение и атмосфера&lt;/h2&gt;
  &lt;p id=&quot;Njxr&quot;&gt;Главная особенность проекта — темнота. Простого чёрного прямоугольника поверх сцены недостаточно: нам нужно оставить видимой область вокруг шара.&lt;/p&gt;
  &lt;p id=&quot;bjrB&quot;&gt;Я использовал отдельную &lt;code&gt;sf::RenderTexture&lt;/code&gt; как карту света. Каждый кадр она очищается тёмным цветом, после чего на неё с аддитивным смешиванием рисуется мягкий световой круг. Затем фрагментный шейдер умножает цвет игрового мира на значение карты света.&lt;/p&gt;
  &lt;p id=&quot;ZWye&quot;&gt;&lt;strong&gt;light.frag&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;By70&quot; data-lang=&quot;flow&quot;&gt;uniform sampler2D texture;
uniform sampler2D lightMap;

void main()
{
    vec2 uv = gl_TexCoord[0].xy;

    vec4 worldColor = texture2D(texture, uv);
    vec4 lightColor = texture2D(lightMap, uv);

    float ambient = 0.08;
    float brightness = max(
        ambient,
        max(lightColor.r, max(lightColor.g, lightColor.b))
    );

    gl_FragColor = vec4(
        worldColor.rgb * brightness,
        worldColor.a
    );
}&lt;/pre&gt;
  &lt;p id=&quot;UwDn&quot;&gt;Мягкий круг можно подготовить в графическом редакторе или создать программно как радиальный градиент. Его центр совпадает с позицией шара.&lt;/p&gt;
  &lt;pre id=&quot;M2Hy&quot; data-lang=&quot;cpp&quot;&gt;lightMap.clear(sf::Color::Black);

lightSprite.setPosition(ball.getPosition());
lightMap.draw(lightSprite, sf::BlendAdd);

lightMap.display();

lightShader.setUniform(
    &amp;quot;lightMap&amp;quot;,
    lightMap.getTexture()
);

window.draw(worldSprite, &amp;amp;lightShader);&lt;/pre&gt;
  &lt;p id=&quot;WcSp&quot;&gt;Чтобы рикошет ощущался сильнее, в момент столкновения я ненадолго увеличиваю радиус света и создаю несколько частиц. Получается короткая вспышка: игрок видит кусок уровня и успевает запомнить расположение следующего препятствия.&lt;/p&gt;
  &lt;h2 id=&quot;XOsJ&quot;&gt;Цели и подсчёт очков&lt;/h2&gt;
  &lt;p id=&quot;QQfw&quot;&gt;Каждая цель хранит форму и флаг активности. При столкновении с шаром цель выключается, счёт увеличивается, а на её месте появляется вспышка.&lt;/p&gt;
  &lt;pre id=&quot;upxe&quot; data-lang=&quot;cpp&quot;&gt;void Game::checkTargets()
{
    for (auto&amp;amp; target : targets)
    {
        if (!target.isActive())
            continue;

        const sf::Vector2f difference =
            target.getPosition() - ball.getPosition();

        const float radius =
            target.getRadius() + ball.getRadius();

        const float distanceSquared =
            difference.x * difference.x
            + difference.y * difference.y;

        if (distanceSquared &amp;lt;= radius * radius)
        {
            target.destroy();
            score += 100;
            createHitParticles(target.getPosition());
        }
    }
}&lt;/pre&gt;
  &lt;p id=&quot;L3Kp&quot;&gt;Когда активных целей не осталось, уровень завершён. Игроку можно начислять дополнительный бонус за количество рикошетов или за неиспользованные попытки.&lt;/p&gt;
  &lt;h2 id=&quot;qlxJ&quot;&gt;Обновление игрового состояния&lt;/h2&gt;
  &lt;p id=&quot;eiSH&quot;&gt;Метод &lt;code&gt;update&lt;/code&gt; соединяет отдельные механики, но не содержит их внутреннюю реализацию.&lt;/p&gt;
  &lt;pre id=&quot;O8c3&quot; data-lang=&quot;cpp&quot;&gt;void Game::update(float deltaTime)
{
    ball.update(deltaTime);

    if (ball.isActive())
    {
        handleWindowCollision();

        for (const auto&amp;amp; obstacle : obstacles)
            resolveObstacleCollision(obstacle.getBounds());

        checkTargets();
        updateTrail(deltaTime);
        updateParticles(deltaTime);
    }

    lightSystem.setLightPosition(ball.getPosition());
    lightSystem.update(deltaTime);

    if (allTargetsDestroyed())
        state = GameState::Victory;
}&lt;/pre&gt;
  &lt;p id=&quot;H2fU&quot;&gt;Такой код легко читать сверху вниз. Мы видим порядок действий, но детали столкновений, частиц и света находятся в отдельных функциях и классах.&lt;/p&gt;
  &lt;h2 id=&quot;Md41&quot;&gt;Небольшие детали, которые делают игру живой&lt;/h2&gt;
  &lt;p id=&quot;PEXG&quot;&gt;Механика может работать идеально, но без обратной связи игра всё равно будет ощущаться пустой. Я добавил несколько простых эффектов:&lt;/p&gt;
  &lt;ul id=&quot;mL9S&quot;&gt;
    &lt;li id=&quot;LiCI&quot;&gt;световой след за шаром;&lt;/li&gt;
    &lt;li id=&quot;TIAr&quot;&gt;короткую вспышку при каждом рикошете;&lt;/li&gt;
    &lt;li id=&quot;hwIl&quot;&gt;частицы при уничтожении цели;&lt;/li&gt;
    &lt;li id=&quot;2heq&quot;&gt;разные звуки для стены, препятствия и цели;&lt;/li&gt;
    &lt;li id=&quot;SrHh&quot;&gt;лёгкое дрожание камеры при сильном ударе;&lt;/li&gt;
    &lt;li id=&quot;3OT0&quot;&gt;изменение цвета шара при увеличении серии попаданий.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;kdJl&quot;&gt;Важно не переборщить. Если экран постоянно трясётся и мигает, игрок уже сражается не с уровнем, а с собственным монитором.&lt;/p&gt;
  &lt;p id=&quot;9nbc&quot;&gt;&lt;a href=&quot;https://github.com/cnet-sudo/Ricochet-in-the-Dark.git&quot; target=&quot;_blank&quot;&gt;Клонировать репозиторий&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;M3NW&quot;&gt;&lt;a href=&quot;https://t.me/ue4_dev_soft/194&quot; target=&quot;_blank&quot;&gt;Скачать файлы&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;h3h3&quot;&gt;&lt;a href=&quot;https://t.me/C_Verhovcevo_NVK&quot; target=&quot;_blank&quot;&gt;Наш Телеграмм&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;vvNL&quot;&gt;&lt;a href=&quot;https://www.youtube.com/@%D0%98%D1%81%D0%BA%D1%83%D1%81%D1%81%D1%82%D0%B2%D0%BE%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%D0%BD%D0%B0%D1%8F%D0%B7%D1%8B&quot; target=&quot;_blank&quot;&gt;Наш Ютюб &lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>gamedeveloper:1qgQI3T2-7A</id><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper/1qgQI3T2-7A?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><title>Разработка игры &quot;Пятнашки&quot; на SFML C++</title><published>2025-06-05T14:48:49.656Z</published><updated>2025-06-05T14:48:49.656Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img1.teletype.in/files/83/95/839596a4-1d23-406e-970e-9c61c78b0e49.png"></media:thumbnail><category term="mul-timedijnaya-biblioteka-sfml-c" label="Мультимедийная библиотека  SFML C++"></category><summary type="html">&lt;img src=&quot;https://img2.teletype.in/files/53/d9/53d9953b-85d9-43d8-ab6a-f8d9e11d1c66.png&quot;&gt;Привет! Сегодня хочу поделиться опытом создания классической головоломки — игры &quot;Пятнашки&quot;, на C++ с использованием фреймворка SFML (Simple and Fast Multimedia Library).</summary><content type="html">
  &lt;figure id=&quot;OTNK&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/53/d9/53d9953b-85d9-43d8-ab6a-f8d9e11d1c66.png&quot; width=&quot;1241&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;LYDt&quot;&gt;Привет! Сегодня хочу поделиться опытом создания классической головоломки — игры &amp;quot;Пятнашки&amp;quot;, на C++ с использованием фреймворка SFML (Simple and Fast Multimedia Library).&lt;/p&gt;
  &lt;p id=&quot;lffC&quot;&gt;Если вы начинающий разработчик или просто любите писать игры своими руками, то эта статья может вам понравиться. &lt;br /&gt;Я расскажу не только о том, как отобразить окно и кнопки, но и о том, как организовать логику игры, обработать взаимодействие пользователя, а также немного поговорим о структуре проекта и возможностях SFML.&lt;/p&gt;
  &lt;p id=&quot;2RyM&quot;&gt;Игра &amp;quot;Пятнашки&amp;quot; — отличный способ попрактиковаться в программировании: здесь есть и логика, и работа с интерфейсом, и даже простая анимация. А SFML — это отличный выбор для тех, кто хочет создавать 2D-игры без излишней сложности и с минимальным порогом входа.&lt;/p&gt;
  &lt;p id=&quot;DyQK&quot;&gt;Давайте начнём!&lt;/p&gt;
  &lt;h2 id=&quot;ROYB&quot;&gt;Подготовка среды разработки&lt;/h2&gt;
  &lt;p id=&quot;66oV&quot;&gt;Перед тем как писать код, нужно подготовить рабочую среду:&lt;/p&gt;
  &lt;p id=&quot;AuPl&quot;&gt;Установите компилятор C++.&lt;/p&gt;
  &lt;h3 id=&quot;zvWP&quot;&gt;&lt;u&gt;Например:&lt;/u&gt;&lt;/h3&gt;
  &lt;p id=&quot;GTMR&quot;&gt;GCC (Linux)&lt;/p&gt;
  &lt;p id=&quot;53nN&quot;&gt;MinGW (Windows)&lt;/p&gt;
  &lt;p id=&quot;4BVp&quot;&gt;Visual Studio (Windows)&lt;/p&gt;
  &lt;h3 id=&quot;Ql9A&quot;&gt;&lt;u&gt;Скачайте и установите SFML:&lt;/u&gt;&lt;/h3&gt;
  &lt;p id=&quot;FDw2&quot;&gt;Официальный сайт: https://www.sfml-dev.org/download.php&lt;/p&gt;
  &lt;p id=&quot;0Yz6&quot;&gt;Выберите версию под ваш компилятор и систему.&lt;/p&gt;
  &lt;p id=&quot;Hf93&quot;&gt;Настройте свой проект:&lt;/p&gt;
  &lt;p id=&quot;FkYO&quot;&gt;Добавьте пути к заголовочным файлам SFML.&lt;/p&gt;
  &lt;p id=&quot;l1i8&quot;&gt;Слинкуйте нужные библиотеки (sfml-graphics, sfml-window, sfml-system).&lt;/p&gt;
  &lt;p id=&quot;1Xpo&quot;&gt;Проверьте, что всё работает — откройте простое окно SFML.&lt;/p&gt;
  &lt;h2 id=&quot;0APa&quot;&gt;Общая структура проекта&lt;/h2&gt;
  &lt;p id=&quot;iK2u&quot;&gt;Вот как выглядел мой проект:&lt;/p&gt;
  &lt;figure id=&quot;1qrE&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/5d/7c/5d7c9b8c-7f8e-4eab-bbe8-f8baca91739a.png&quot; width=&quot;281&quot; /&gt;
  &lt;/figure&gt;
  &lt;h2 id=&quot;MKm2&quot;&gt;&lt;strong&gt;Базовый класс Screen — основа всех экранов&lt;/strong&gt;&lt;/h2&gt;
  &lt;p id=&quot;gUtu&quot;&gt;В процессе создания игры важно было организовать удобную архитектуру, особенно потому, что у нас есть несколько экранов: главное меню, игровой экран и экран с правилами. Чтобы не повторять один и тот же код в каждом из них, я создал абстрактный базовый класс Screen.&lt;/p&gt;
  &lt;p id=&quot;wpXz&quot;&gt;&lt;u&gt;Зачем он нужен?&lt;/u&gt;&lt;/p&gt;
  &lt;p id=&quot;DjuU&quot;&gt;Класс Screen служит общей основой для всех экранов приложения:&lt;/p&gt;
  &lt;p id=&quot;24tw&quot;&gt;он предоставляет доступ к окну (sf::RenderWindow);&lt;/p&gt;
  &lt;p id=&quot;tCTl&quot;&gt;хранит ссылки на глобальное состояние игры (GameState) и переходы между экранами (Transition);&lt;/p&gt;
  &lt;p id=&quot;oQb3&quot;&gt;реализует общие элементы интерфейса (например, фон);&lt;/p&gt;
  &lt;p id=&quot;0v44&quot;&gt;упрощает расширяемость проекта — добавление нового экрана становится простым делом.&lt;/p&gt;
  &lt;h3 id=&quot;QqdI&quot;&gt;Основные компоненты класса&lt;/h3&gt;
  &lt;p id=&quot;fBC1&quot;&gt;&lt;strong&gt;&lt;u&gt;Screen.h&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;EzGn&quot; data-lang=&quot;cpp&quot;&gt;#pragma once
#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;quot;GameState.h&amp;quot;
#include &amp;quot;Transition.h&amp;quot;
#include &amp;quot;ResourceManager.h&amp;quot;
#include &amp;lt;memory&amp;gt;

class Screen : public sf::Drawable {
protected:
    sf::RenderWindow&amp;amp; window;
    GameState&amp;amp; state;
    Transition&amp;amp; transition;

    // Общие элементы интерфейса
    sf::Sprite backgroundSprite;
    bool useTexture;

    Screen(sf::RenderWindow&amp;amp; window, G
    ameState&amp;amp; state, Transition&amp;amp; transition);

    // Виртуальные методы для настройки ресурсов и элементов интерфейса
    virtual void initialize() = 0;

    // Вспомогательные методы
    void setupBackground(const std::string&amp;amp; textureId);

public:
    virtual ~Screen() = default;

    // Общие методы, которые будут переопределяться
    virtual void handleEvent(const sf::Event&amp;amp; event) = 0;
    virtual void update(float deltaTime) = 0;
    virtual void 
    draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) const = 0;
};

&lt;/pre&gt;
  &lt;p id=&quot;1hku&quot;&gt;&lt;strong&gt;&lt;u&gt;Screen.cpp&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;hfhR&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;Screen.h&amp;quot;
#include &amp;lt;iostream&amp;gt;

Screen::Screen(sf::RenderWindow&amp;amp; window, GameState&amp;amp; state,
 Transition&amp;amp; transition)
    : window(window), state(state), transition(transition), 
    useTexture(false) {
}

void Screen::setupBackground(const std::string&amp;amp; textureId) {
    ResourceManager&amp;amp; rm = ResourceManager::getInstance();
    sf::Texture&amp;amp; bgTexture = rm.getTexture(textureId);
    if (bgTexture.getSize().x == 0) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Ошибка: Не удалось загрузить текстуру фона &amp;#x27;&amp;quot; 
        &amp;lt;&amp;lt; textureId &amp;lt;&amp;lt; &amp;quot;&amp;#x27; из ResourceManager.\n&amp;quot;;
        useTexture = false;
    }
    else {
        useTexture = true;
        backgroundSprite.setTexture(bgTexture);
        backgroundSprite.setScale(
            static_cast&amp;lt;float&amp;gt;(window.getSize().x) / bgTexture.getSize().x,
            static_cast&amp;lt;float&amp;gt;(window.getSize().y) / bgTexture.getSize().y
        );
    }
}
&lt;/pre&gt;
  &lt;p id=&quot;AWtH&quot;&gt;&lt;u&gt;Что здесь происходит?&lt;/u&gt;&lt;/p&gt;
  &lt;p id=&quot;ixSE&quot;&gt;Конструктор принимает ссылки на важнейшие объекты: окно, состояние игры и переходы.&lt;/p&gt;
  &lt;p id=&quot;2Eey&quot;&gt;initialize() — чисто виртуальный метод, который должен быть реализован в каждом дочернем классе. Здесь можно загрузить специфичные для экрана ресурсы или создать кнопки.&lt;/p&gt;
  &lt;p id=&quot;nB8I&quot;&gt;setupBackground() — универсальный метод для настройки фона. Он использует ResourceManager, чтобы получить текстуру по ID и корректно её от масштабировать под размер окна.&lt;/p&gt;
  &lt;p id=&quot;T4Me&quot;&gt;Все экраны обязаны реализовать три ключевых метода:&lt;/p&gt;
  &lt;p id=&quot;9Zem&quot;&gt;handleEvent() — обработка событий (нажатия мыши, клавиш и т.д.)&lt;/p&gt;
  &lt;p id=&quot;ay1C&quot;&gt;update() — логика обновления состояния экрана (анимации, проверка условий и пр.)&lt;/p&gt;
  &lt;p id=&quot;80zq&quot;&gt;draw() — отрисовка всех элементов экрана.&lt;/p&gt;
  &lt;h2 id=&quot;FKq7&quot;&gt;Реализация класса ResourceManager  с поддержкой ресурсов Windows&lt;/h2&gt;
  &lt;p id=&quot;5uLE&quot;&gt;Одним из важнейших компонентов в любой игре или мультимедийном приложении является централизованное управление ресурсами. В этой главе мы рассмотрим реализацию класса &lt;code&gt;ResourceManager&lt;/code&gt; на C++ с использованием SFML и Windows API. &lt;/p&gt;
  &lt;p id=&quot;S6kZ&quot;&gt;Класс ResourceManager отвечает за загрузку и хранение следующих типов ресурсов:&lt;/p&gt;
  &lt;p id=&quot;i6uJ&quot;&gt;·         sf::Font — шрифты;&lt;/p&gt;
  &lt;p id=&quot;DX8c&quot;&gt;·         sf::SoundBuffer — звуковые эффекты;&lt;/p&gt;
  &lt;p id=&quot;vWY0&quot;&gt;·         sf::Texture — текстуры;&lt;/p&gt;
  &lt;p id=&quot;2Z9I&quot;&gt;·         sf::Music — музыкальные потоки (streaming).&lt;/p&gt;
  &lt;p id=&quot;TY4T&quot;&gt;Все ресурсы хранятся в памяти, загружаются из ресурсов Windows &lt;code&gt;.exe&lt;/code&gt; файла (раздел &lt;code&gt;.rc&lt;/code&gt;) и доступны по строковому идентификатору.&lt;/p&gt;
  &lt;p id=&quot;3wUu&quot;&gt;&lt;strong&gt;resource.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;5FAG&quot; data-lang=&quot;cpp&quot;&gt;#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  // Иконка
&lt;/pre&gt;
  &lt;p id=&quot;hAVM&quot;&gt;&lt;strong&gt;fifteen_puzzle.rc&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;ImfY&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;resource.h&amp;quot;

IDR_TEXTURE1   RCDATA  &amp;quot;assets/puzzle.png&amp;quot;
IDR_TEXTURE2   RCDATA  &amp;quot;assets/background.png&amp;quot;
IDR_TEXTURE3   RCDATA  &amp;quot;assets/background1.png&amp;quot;
IDR_TEXTURE4   RCDATA  &amp;quot;assets/background2.png&amp;quot;
IDR_TEXTURE5   RCDATA  &amp;quot;assets/numbers.png&amp;quot;

IDR_SOUND1     RCDATA &amp;quot;assets/music.wav&amp;quot;

IDR_FONT1      RCDATA &amp;quot;assets/arial.ttf&amp;quot;
IDR_FONT2      RCDATA &amp;quot;assets/glv.ttf&amp;quot;

IDR_ICON1      ICON &amp;quot;assets/puzzle.ico&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;t1Yq&quot;&gt;&lt;strong&gt;ResourceManager.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;j8Ng&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;SFML/Audio.hpp&amp;gt;
#include &amp;lt;windows.h&amp;gt;
#include &amp;lt;map&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;memory&amp;gt;

class ResourceManager {
private:
	// Хранилища для ресурсов
    // Хранилище для шрифтов
    std::map&amp;lt;std::string, sf::Font&amp;gt; fonts;
    // Хранилище для звуковых буферов
    std::map&amp;lt;std::string, sf::SoundBuffer&amp;gt; buffers;
    // Хранилище для текстур
    std::map&amp;lt;std::string, sf::Texture&amp;gt; textures;
    // Хранилище для музыкальных потоков
    std::map&amp;lt;std::string, std::unique_ptr&amp;lt;sf::Music&amp;gt;&amp;gt; musicStreams; 

    // Приватный конструктор для реализации синглтона
    ResourceManager();

    // Приватный деструктор
    ~ResourceManager() {}

    // Вспомогательный метод для загрузки ресурса
    template&amp;lt;typename T&amp;gt;
    bool loadResource(HRSRC resource, HGLOBAL handle, T&amp;amp; target);

public:
    // Метод получения единственного экземпляра класса
    static ResourceManager&amp;amp; getInstance();

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

    // Методы загрузки конкретных ресурсов
    bool loadFont(const std::string&amp;amp; id, int resourceId);
    bool loadSound(const std::string&amp;amp; id, int resourceId);
    bool loadTexture(const std::string&amp;amp; id, int resourceId);
    bool loadMusic(const std::string&amp;amp; id, int resourceId); 

    // Геттеры для получения ресурсов по идентификатору
    sf::Font&amp;amp; getFont(const std::string&amp;amp; id);
    sf::SoundBuffer&amp;amp; getSoundBuffer(const std::string&amp;amp; id);
    sf::Texture&amp;amp; getTexture(const std::string&amp;amp; id);
    sf::Image getImage(const std::string&amp;amp; id);
    sf::Music&amp;amp; getMusic(const std::string&amp;amp; id); 
};&lt;/pre&gt;
  &lt;p id=&quot;lMw7&quot;&gt;&lt;strong&gt;ResourceManager.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;22Sc&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;ResourceManager.h&amp;quot;
#include &amp;lt;iostream&amp;gt;

// Конструктор по умолчанию
ResourceManager::ResourceManager() {}

// Шаблонная функция для загрузки ресурса из памяти в целевой объект (например, шрифт, текстура и т.д.)
template&amp;lt;typename T&amp;gt;
bool ResourceManager::loadResource(HRSRC resource, HGLOBAL handle, T&amp;amp; target) {
    if (!resource) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Не удалось найти ресурс&amp;quot; &amp;lt;&amp;lt; std::endl;
        return false;
    }

    // Загружаем ресурс в память
    handle = LoadResource(NULL, resource);
    if (!handle) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Не удалось загрузить ресурс&amp;quot; &amp;lt;&amp;lt; 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&amp;amp; ResourceManager::getInstance() {
    static ResourceManager instance;
    return instance;
}

// Загружает шрифт из ресурса Windows с заданным ID и сохраняет его под указанным идентификатором
bool ResourceManager::loadFont(const std::string&amp;amp; id, int resourceId) {
    // Находим ресурс шрифта в исполняемом файле
    HRSRC fontResource = FindResource(NULL, MAKEINTRESOURCE(resourceId), RT_RCDATA);
    HGLOBAL fontHandle = NULL;

    sf::Font font;
    if (!loadResource(fontResource, fontHandle, font)) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Не удалось загрузить шрифт &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; std::endl;
        return false;
    }

    // Сохраняем загруженный шрифт в хранилище
    fonts[id] = std::move(font);
    return true;
}

// Загружает звуковой буфер из ресурса Windows с заданным ID и сохраняет его под указанным идентификатором
bool ResourceManager::loadSound(const std::string&amp;amp; id, int resourceId) {
    HRSRC soundResource = FindResource(NULL, MAKEINTRESOURCE(resourceId), RT_RCDATA);
    HGLOBAL soundHandle = NULL;

    sf::SoundBuffer buffer;
    if (!loadResource(soundResource, soundHandle, buffer)) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Не удалось загрузить звук &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; std::endl;
        return false;
    }

    // Сохраняем загруженный звуковой буфер в хранилище
    buffers[id] = std::move(buffer);
    return true;
}

// Загружает текстуру из ресурса Windows с заданным ID и сохраняет её под указанным идентификатором
bool ResourceManager::loadTexture(const std::string&amp;amp; id, int resourceId) {
    HRSRC textureResource = FindResource(NULL, MAKEINTRESOURCE(resourceId), RT_RCDATA);
    HGLOBAL textureHandle = NULL;

    sf::Texture texture;
    if (!loadResource(textureResource, textureHandle, texture)) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Не удалось загрузить текстуру &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; std::endl;
        return false;
    }

    // Сохраняем загруженную текстуру в хранилище
    textures[id] = std::move(texture);
    return true;
}

// Загружает музыку из ресурса Windows с заданным ID и сохраняет её под указанным идентификатором
bool ResourceManager::loadMusic(const std::string&amp;amp; id, int resourceId) {
    HRSRC musicResource = FindResource(NULL, MAKEINTRESOURCE(resourceId), RT_RCDATA);
    HGLOBAL musicHandle = NULL;

    if (!musicResource) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Не удалось найти музыкальный ресурс &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; std::endl;
        return false;
    }

    musicHandle = LoadResource(NULL, musicResource);
    if (!musicHandle) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Не удалось загрузить музыкальный ресурс &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; std::endl;
        return false;
    }

    // Получаем указатель на данные музыки и её размер
    const void* data = LockResource(musicHandle);
    DWORD size = SizeofResource(NULL, musicResource);

    // Создаём объект музыки и загружаем его из памяти
    auto music = std::make_unique&amp;lt;sf::Music&amp;gt;();
    if (!music-&amp;gt;openFromMemory(data, size)) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Не удалось открыть музыкальный поток &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot; из памяти&amp;quot; &amp;lt;&amp;lt; std::endl;
        return false;
    }

    // Сохраняем музыкальный поток в хранилище
    musicStreams[id] = std::move(music);
    return true;
}

// Возвращает ссылку на шрифт по указанному ID
sf::Font&amp;amp; ResourceManager::getFont(const std::string&amp;amp; id) {
    auto it = fonts.find(id);
    if (it == fonts.end()) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Шрифт &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot; не найден&amp;quot; &amp;lt;&amp;lt; std::endl;
        static sf::Font emptyFont;  // Статический пустой шрифт на случай ошибки
        return emptyFont;
    }
    return it-&amp;gt;second;
}

// Возвращает ссылку на звуковой буфер по указанному ID
sf::SoundBuffer&amp;amp; ResourceManager::getSoundBuffer(const std::string&amp;amp; id) {
    auto it = buffers.find(id);
    if (it == buffers.end()) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Звуковой буфер &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot; не найден&amp;quot; &amp;lt;&amp;lt; std::endl;
        static sf::SoundBuffer emptyBuffer;
        return emptyBuffer;
    }
    return it-&amp;gt;second;
}

// Возвращает ссылку на текстуру по указанному ID
sf::Texture&amp;amp; ResourceManager::getTexture(const std::string&amp;amp; id) {
    auto it = textures.find(id);
    if (it == textures.end()) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Текстура &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot; не найдена&amp;quot; &amp;lt;&amp;lt; std::endl;
        static sf::Texture emptyTexture;
        return emptyTexture;
    }
    return it-&amp;gt;second;
}

// Возвращает копию изображения из текстуры по указанному ID
sf::Image ResourceManager::getImage(const std::string&amp;amp; id) {
    auto it = textures.find(id);
    if (it != textures.end()) {
        return it-&amp;gt;second.copyToImage();  // Копируем текстуру в изображение
    }
    std::cerr &amp;lt;&amp;lt; &amp;quot;Текстура для изображения &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot; не найдена&amp;quot; &amp;lt;&amp;lt; std::endl;
    return sf::Image();
}

// Возвращает ссылку на музыкальный поток по указанному ID
sf::Music&amp;amp; ResourceManager::getMusic(const std::string&amp;amp; id) {
    auto it = musicStreams.find(id);
    if (it == musicStreams.end()) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Музыкальный поток &amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot; не найден&amp;quot; &amp;lt;&amp;lt; std::endl;
        static sf::Music emptyMusic;
        return emptyMusic;
    }
    return *(it-&amp;gt;second);  // Разыменовываем уникальный указатель, чтобы вернуть ссылку
}&lt;/pre&gt;
  &lt;h2 id=&quot;60sN&quot;&gt;Кастомный класс Button для интерфейса&lt;/h2&gt;
  &lt;p id=&quot;BbSP&quot;&gt;Интерфейс — важная часть любой игры. В этой главе мы реализуем &lt;strong&gt;гибкий класс &lt;code&gt;Button&lt;/code&gt;&lt;/strong&gt;, полностью совместимый с SFML 2.x и современными возможностями C++20. Он предоставляет визуальные эффекты нажатия, возможность настройки цветов, шрифтов и размеров, а также безопасную обработку кликов.&lt;/p&gt;
  &lt;p id=&quot;TnKd&quot;&gt;&lt;strong&gt;Возможности данного класса&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;sL2U&quot;&gt;Поддержка нажатия с визуальной анимацией.&lt;/p&gt;
  &lt;p id=&quot;WPvr&quot;&gt;Гибкая настройка цветов: фона, обводки, текста.&lt;/p&gt;
  &lt;p id=&quot;iQYc&quot;&gt;Обработка клика через sf::Vector2f (позиция мыши).&lt;/p&gt;
  &lt;p id=&quot;VB3i&quot;&gt;Использование concepts из C++20 для безопасных шаблонов.&lt;/p&gt;
  &lt;p id=&quot;x8hg&quot;&gt;Центрирование текста по кнопке.&lt;/p&gt;
  &lt;p id=&quot;deoO&quot;&gt;Поддержка перемещения и сравнения (&amp;lt;=&amp;gt;).&lt;/p&gt;
  &lt;p id=&quot;QlLD&quot;&gt;&lt;strong&gt;Button.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;Jy2Q&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;concepts&amp;gt;

// Класс кнопки для SFML. Наследуется от sf::Drawable 
// и sf::Transformable для отрисовки и трансформаций.
class Button : public sf::Drawable, public sf::Transformable {

public:
    // Конструктор кнопки с полным набором параметров
    Button(std::wstring text, sf::Vector2f position, sf::Font&amp;amp; 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&amp;amp;) = default;
    Button&amp;amp; operator=(const Button&amp;amp;) = default;
    Button(Button&amp;amp;&amp;amp;) noexcept;
    Button&amp;amp; operator=(Button&amp;amp;&amp;amp;) noexcept;

    // Отрисовка кнопки на экране
    void draw(sf::RenderTarget&amp;amp; 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&amp;lt;typename T&amp;gt; requires std::same_as&amp;lt;T, sf::Color&amp;gt;
    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&amp;lt;=&amp;gt;(const Button&amp;amp; 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;
};&lt;/pre&gt;
  &lt;p id=&quot;AJMq&quot;&gt;&lt;strong&gt;Button.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;i31D&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;Button.h&amp;quot;
#include &amp;lt;ranges&amp;gt;

// Конструктор с параметрами
Button::Button(std::wstring text, sf::Vector2f position, sf::Font&amp;amp; 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&amp;lt;typename T&amp;gt;
    requires std::same_as&amp;lt;T, sf::Color&amp;gt;
void Button::setButtonColor(T color) noexcept {
    this-&amp;gt;buttonColor = color;
    shape.setFillColor(color);
}

// Установка цвета при нажатии
void Button::setPressedColor(sf::Color color) noexcept {
    this-&amp;gt;pressedColor = color;
}

// Установка цвета текста
void Button::setTextColor(sf::Color color) noexcept {
    this-&amp;gt;textColor = color;
    label.setFillColor(color);
}

// Установка цвета текста при нажатии
void Button::setPressedTextColor(sf::Color color) noexcept {
    this-&amp;gt;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&amp;lt;std::uint8_t&amp;gt;(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&amp;amp; 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&amp;amp;&amp;amp; other) noexcept = default;

// Оператор перемещения
Button&amp;amp; Button::operator=(Button&amp;amp;&amp;amp; other) noexcept = default;
&lt;/pre&gt;
  &lt;h2 id=&quot;TBV9&quot;&gt;Анимированный разноцветный текст, класс ColorfulText&lt;/h2&gt;
  &lt;p id=&quot;sx8Q&quot;&gt;В этом разделе блога я хочу показать вам, как создать красивый, динамически меняющийся текст с цветной анимацией на библиотеке SFML. Такой текст отлично подходит для заставок, титулов, экранов загрузки или игровых меню. Мы создадим собственный класс ColorfulText, где каждая буква будет представлять собой отдельный объект sf::Text, меняющий цвет через определённый интервал времени.&lt;/p&gt;
  &lt;h3 id=&quot;Jbqv&quot;&gt;Возможности данного класса&lt;/h3&gt;
  &lt;p id=&quot;Y5BF&quot;&gt;- отображает строку текста;&lt;/p&gt;
  &lt;p id=&quot;1pEk&quot;&gt;- окрашивает каждую букву в случайный цвет;&lt;/p&gt;
  &lt;p id=&quot;kVC0&quot;&gt;- периодически обновляет цвета;&lt;/p&gt;
  &lt;p id=&quot;r54T&quot;&gt;- поддерживает позиционирование с центрированием.&lt;/p&gt;
  &lt;p id=&quot;ziI1&quot;&gt;&lt;strong&gt;ColorfulText.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;sAFT&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;random&amp;gt;
#include &amp;lt;memory&amp;gt;

// Класс ColorfulText — отображает анимированный разноцветный текст
class ColorfulText {
public:
    // Конструктор
    ColorfulText(const std::string&amp;amp; fontId, std::wstring_view text, unsigned int charSize,
        const sf::Vector2f&amp;amp; position, const sf::RenderWindow&amp;amp; window,
        bool centerHorizontally = false, bool centerVertically = false);

    // Обновление цвета текста (вызывается в цикле игры)
    void update(float deltaTime);

    // Отрисовка текста на экране
    void draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states = {}) const;

    // Установка новой позиции текста с возможностью центрирования
    void setPosition(const sf::Vector2f&amp;amp; position,
        bool centerHorizontally = false,
        bool centerVertically = false);

private:
    // Обновление цветов букв на случайные
    void updateColors();

    std::shared_ptr&amp;lt;sf::Font&amp;gt; font;       // Шрифт
    std::vector&amp;lt;sf::Text&amp;gt; letters;        // Вектор отдельных букв (sf::Text)
    float colorTimer;                     // Таймер для смены цвета
    static constexpr float COLOR_CHANGE_INTERVAL = 0.5f; // Интервал смены цвета
};
&lt;/pre&gt;
  &lt;p id=&quot;o1DG&quot;&gt;Вместо одного объекта sf::Text, как обычно, мы создаём вектор letters, в котором каждая буква строки является отдельным объектом sf::Text. Это даёт нам гибкость при анимации — можно изменять цвет каждой буквы по отдельности.&lt;/p&gt;
  &lt;p id=&quot;sdF9&quot;&gt;&lt;strong&gt;ColorfulText.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;OkTG&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;ColorfulText.h&amp;quot;
#include &amp;quot;ResourceManager.h&amp;quot;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;random&amp;gt;

// Конструктор
ColorfulText::ColorfulText(const std::string&amp;amp; fontId, std::wstring_view text, unsigned int charSize,
    const sf::Vector2f&amp;amp; position, const sf::RenderWindow&amp;amp; window,
    bool centerHorizontally, bool centerVertically)
    : colorTimer(0.0f) {

    // Получаем ссылку на синглтон ResourceManager
    ResourceManager&amp;amp; rm = ResourceManager::getInstance();

    // Загружаем шрифт
    font = std::make_shared&amp;lt;sf::Font&amp;gt;(rm.getFont(fontId));

    // Проверяем, загрузился ли шрифт
    if (font-&amp;gt;getInfo().family.empty()) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить шрифт с ID: &amp;quot; + std::string(fontId) + &amp;quot; из ResourceManager&amp;quot;);
    }

    // Вычисляем общую ширину текста для центрирования
    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 &amp;lt; 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 &amp;gt;= 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&amp;lt;int&amp;gt; dis(0, 255);

    for (auto&amp;amp; letter : letters) {
        letter.setFillColor(sf::Color(
            static_cast&amp;lt;sf::Uint8&amp;gt;(dis(gen)),  // R
            static_cast&amp;lt;sf::Uint8&amp;gt;(dis(gen)),  // G
            static_cast&amp;lt;sf::Uint8&amp;gt;(dis(gen))   // B
        ));
    }
}

// Отрисовка всех букв текста
void ColorfulText::draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) const {
    for (const auto&amp;amp; letter : letters) {
        target.draw(letter, states);
    }
}

// Установка новой позиции текста с опциональным центрированием
void ColorfulText::setPosition(const sf::Vector2f&amp;amp; 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 &amp;lt; letters.size(); ++i) {
        letters[i].setPosition(xPos + i * letters[0].getCharacterSize() * 0.8f, yPos);
    }
}
&lt;/pre&gt;
  &lt;h2 id=&quot;kx7F&quot;&gt;Плавный переход между экранами в игре, класс Transition&lt;/h2&gt;
  &lt;p id=&quot;NtxP&quot;&gt;Переходы между игровыми экранами — важная часть UX. Они делают игру визуально цельной и приятной. В этом разделе мы реализуем &lt;strong&gt;двухфазный эффект затемнения и осветления&lt;/strong&gt; — популярный способ перехода между состояниями игры с помощью чёрного оверлея.&lt;/p&gt;
  &lt;h3 id=&quot;6nm8&quot;&gt;&lt;strong&gt;Возможности данного класса:&lt;/strong&gt;&lt;/h3&gt;
  &lt;ul id=&quot;4wqU&quot;&gt;
    &lt;li id=&quot;thQR&quot;&gt;затухание экрана до чёрного (fade-out),&lt;/li&gt;
    &lt;li id=&quot;v8al&quot;&gt;смену состояния игры (например, меню → игра),&lt;/li&gt;
    &lt;li id=&quot;QMMU&quot;&gt;проявление нового состояния (fade-in).&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;EWLu&quot;&gt;&lt;strong&gt;Transition.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;zBmc&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;quot;GameState.h&amp;quot;

class Transition : public sf::Drawable, public sf::Transformable {
public:
    Transition(sf::RenderWindow&amp;amp; window); // Конструктор с окном для динамического размера
    void startTransition(GameState newState);
    bool update(GameState&amp;amp; currentState, float deltaTime); // Добавляем deltaTime

private:
    virtual void draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) const override;

    sf::RectangleShape overlay;
    sf::RenderWindow&amp;amp; window; // Ссылка на окно для получения размера
    GameState targetState;
    float alpha; // Используем float для плавности
    bool transitioning;
    bool fadingIn; // Флаг для фазы fade-in
    float transitionDuration; // Длительность одной фазы
};&lt;/pre&gt;
  &lt;p id=&quot;qOOd&quot;&gt;&lt;strong&gt;Transition.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;Lino&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;Transition.h&amp;quot;

// Конструктор: инициализирует переход с заданным окном отрисовки
Transition::Transition(sf::RenderWindow&amp;amp; window)
    : window(window), alpha(255.f), transitioning(false), fadingIn(false), transitionDuration(0.5f) {

    // Устанавливаем размер затемняющего прямоугольника равным размеру окна
    overlay.setSize(sf::Vector2f(static_cast&amp;lt;float&amp;gt;(window.getSize().x),
        static_cast&amp;lt;float&amp;gt;(window.getSize().y)));

    // Цвет прямоугольника — чёрный с максимальной непрозрачностью (начальное состояние — экран полностью закрыт)
    overlay.setFillColor(sf::Color(0, 0, 0, static_cast&amp;lt;sf::Uint8&amp;gt;(alpha)));
}

// Запускает переход к новому состоянию игры (например, из меню в игру)
void Transition::startTransition(GameState newState) {
    transitioning = true;   // Начинаем переход
    fadingIn = false;       // Сначала будет фейд-аут (затемнение)
    targetState = newState; // Сохраняем целевое состояние игры
    alpha = 255.f;          // Сбрасываем прозрачность

    // Обновляем цвет оверлея, чтобы он был полностью непрозрачным
    overlay.setFillColor(sf::Color(0, 0, 0, static_cast&amp;lt;sf::Uint8&amp;gt;(alpha)));
}

// Обновляет состояние перехода (анимация затухания / появления)
// Возвращает true, если переход всё ещё активен
bool Transition::update(GameState&amp;amp; currentState, float deltaTime) {
    if (transitioning) {
        if (!fadingIn) {
            // Фаза затухания (экран темнеет)
            alpha -= (255.f / transitionDuration) * deltaTime;
            if (alpha &amp;lt;= 0.f) {
                alpha = 0.f;
                fadingIn = true;           // Переходим к фазе проявления
                currentState = targetState; // Меняем текущее состояние игры
            }
        }
        else {
            // Фаза проявления (экран светлеет)
            alpha += (255.f / transitionDuration) * deltaTime;
            if (alpha &amp;gt;= 255.f) {
                alpha = 255.f;
                transitioning = false;     // Переход завершён
            }
        }

        // Обновляем прозрачность оверлея
        // Используется 255.f - alpha, чтобы получить эффект &amp;quot;появления&amp;quot; после затемнения
        overlay.setFillColor(sf::Color(0, 0, 0, static_cast&amp;lt;sf::Uint8&amp;gt;(255.f - alpha)));

        return true; // Переход всё ещё активен
    }
    return false; // Переход завершён
}

// Отрисовывает оверлей (черный прямоугольник) поверх текущего содержимого окна
void Transition::draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) const {
    if (transitioning) {
        target.draw(overlay, states); // Рисуем затемняющий слой, если переход активен
    }
}&lt;/pre&gt;
  &lt;h2 id=&quot;TXEL&quot;&gt;Реализация игры “Пятнашки” в классе FifteenPuzzle, который наследуется от sf::Drawable для отрисовки.&lt;/h2&gt;
  &lt;p id=&quot;29Ez&quot;&gt;&lt;strong&gt;FifteenPuzzle.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;fGZh&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;string&amp;gt;

// Класс, реализующий игру &amp;quot;Пятнашки&amp;quot;, наследуется от 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&amp;amp;) = delete;
    FifteenPuzzle&amp;amp; operator=(const FifteenPuzzle&amp;amp;) = delete;

    // Разрешено перемещение для поддержки семантики перемещения
    FifteenPuzzle(FifteenPuzzle&amp;amp;&amp;amp;) = default;
    FifteenPuzzle&amp;amp; operator=(FifteenPuzzle&amp;amp;&amp;amp;) = default;

    // Установка позиции пазла на экране
    void setPosition(float x, float y);
    // Получение текущей позиции пазла
    sf::Vector2f getPosition() const;
    // Получение границ пазла (для обработки событий)
    sf::FloatRect getBounds() const;

    // Обработка событий (например, кликов мыши)
    void handleEvent(const sf::Event&amp;amp; event, const sf::RenderWindow&amp;amp; window);
    // Обновление состояния игры (заглушка для возможных анимаций)
    void update();
    // Проверка, решён ли пазл
    bool isSolved() const;
    // Перезапуск игры (перемешивание плиток)
    void restart();

private:
    const int m_tileSize; // Размер одной плитки (в пикселях)
    const int m_gridSize; // Размер сетки (например, 4x4)
    std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt; m_board; // Двумерный массив, представляющий игровое поле
    std::shared_ptr&amp;lt;sf::Font&amp;gt; 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&amp;lt;int&amp;gt;&amp;amp; numbers) const;
    // Метод отрисовки пазла (переопределение sf::Drawable)
    virtual void draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) const override;
};&lt;/pre&gt;
  &lt;p id=&quot;dpZt&quot;&gt;Основные моменты:&lt;/p&gt;
  &lt;ul id=&quot;QxXS&quot;&gt;
    &lt;li id=&quot;wPth&quot;&gt;&lt;strong&gt;Константы&lt;/strong&gt;: DEFAULT_TILE_SIZE (100 пикселей) и DEFAULT_GRID_SIZE (4x4) задают размер плиток и сетки по умолчанию.&lt;/li&gt;
    &lt;li id=&quot;AyRv&quot;&gt;&lt;strong&gt;Запрет копирования&lt;/strong&gt;: Конструктор копирования и оператор присваивания отключены, чтобы избежать дублирования ресурсов.&lt;/li&gt;
    &lt;li id=&quot;pp2m&quot;&gt;&lt;strong&gt;Поддержка перемещения&lt;/strong&gt;: Конструктор перемещения и оператор присваивания включены для эффективного управления ресурсами.&lt;/li&gt;
    &lt;li id=&quot;zr1R&quot;&gt;&lt;strong&gt;Основные методы&lt;/strong&gt;: Методы вроде &lt;code&gt;handleEvent&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;isSolved&lt;/code&gt; и &lt;code&gt;restart&lt;/code&gt; управляют игровым процессом, а &lt;code&gt;draw&lt;/code&gt; отвечает за отрисовку.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;y3av&quot;&gt;&lt;strong&gt;FifteenPuzzle.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;OZA2&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;FifteenPuzzle.h&amp;quot;
#include &amp;lt;numeric&amp;gt;
#include &amp;lt;random&amp;gt;
#include &amp;lt;stdexcept&amp;gt;
#include &amp;quot;ResourceManager.h&amp;quot;

// Конструктор: инициализация игры с заданными параметрами
FifteenPuzzle::FifteenPuzzle(int tileSize, int gridSize)
    : m_tileSize(tileSize)
    , m_gridSize(gridSize)
    , m_board(gridSize, std::vector&amp;lt;int&amp;gt;(gridSize, 0))
    , m_font(std::make_shared&amp;lt;sf::Font&amp;gt;())
    , m_emptyX(0)
    , m_emptyY(0)
    , m_position(0.f, 0.f)
    , m_isSolved(false)
{
    // Получение экземпляра менеджера ресурсов (паттерн Singleton)
    ResourceManager&amp;amp; rm = ResourceManager::getInstance();
    // Загрузка шрифта из менеджера ресурсов
    *m_font = rm.getFont(&amp;quot;font&amp;quot;);

    // Проверка успешной загрузки шрифта
    if (m_font-&amp;gt;getInfo().family.empty()) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить шрифт &amp;#x27;font&amp;#x27; из ResourceManager&amp;quot;);
    }

    // Инициализация игрового поля
    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&amp;lt;int&amp;gt;&amp;amp; numbers) const {
    // Подсчёт инверсий в последовательности чисел
    int inversions = 0;
    for (size_t i = 0; i &amp;lt; numbers.size(); ++i) {
        for (size_t j = i + 1; j &amp;lt; numbers.size(); ++j) {
            if (numbers[i] &amp;amp;&amp;amp; numbers[j] &amp;amp;&amp;amp; numbers[i] &amp;gt; numbers[j]) {
                ++inversions;
            }
        }
    }

    // Для нечётного размера сетки: чётное число инверсий =&amp;gt; решаемо
    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&amp;lt;int&amp;gt; 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 &amp;lt; m_gridSize; ++i) {
            for (int j = 0; j &amp;lt; 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) &amp;amp;&amp;amp;
        (x &amp;gt;= 0 &amp;amp;&amp;amp; x &amp;lt; m_gridSize &amp;amp;&amp;amp; y &amp;gt;= 0 &amp;amp;&amp;amp; y &amp;lt; 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 &amp;lt; m_gridSize; ++i) {
        for (int j = 0; j &amp;lt; m_gridSize; ++j) {
            if (i == m_gridSize - 1 &amp;amp;&amp;amp; 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&amp;amp; event, const sf::RenderWindow&amp;amp; window) {
    if (event.type == sf::Event::MouseButtonPressed &amp;amp;&amp;amp;
        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&amp;lt;int&amp;gt;((mousePos.x - m_position.x) / m_tileSize);
            int y = static_cast&amp;lt;int&amp;gt;((mousePos.y - m_position.y) / m_tileSize);
            moveTile(x, y);
        }
    }
}

// Обновление состояния игры (пока не используется)
void FifteenPuzzle::update() {
    // Здесь можно добавить анимации или другие обновления
}

// Отрисовка пазла
void FifteenPuzzle::draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) const {
    // Смещение координат для учёта позиции пазла
    states.transform.translate(m_position);

    // Отрисовка каждой плитки
    for (int i = 0; i &amp;lt; m_gridSize; ++i) {
        for (int j = 0; j &amp;lt; 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();
}&lt;/pre&gt;
  &lt;h3 id=&quot;U65b&quot;&gt;Инициализация&lt;/h3&gt;
  &lt;p id=&quot;88nI&quot;&gt;В конструкторе создаётся игровое поле (m_board) как двумерный вектор размером gridSize x gridSize. Шрифт для номеров плиток загружается через ResourceManager (паттерн Singleton). Затем вызывается shuffleBoard() для создания начальной конфигурации.&lt;/p&gt;
  &lt;h3 id=&quot;r5it&quot;&gt;Перемешивание&lt;/h3&gt;
  &lt;p id=&quot;OdzA&quot;&gt;Метод shuffleBoard() генерирует случайную последовательность чисел от 1 до gridSize*gridSize-1 с 0 в конце (пустая плитка). Используется std::shuffle с генератором случайных чисел std::mt19937. Важно, что метод isSolvable проверяет, является ли конфигурация решаемой, подсчитывая инверсии и учитывая позицию пустой плитки.&lt;/p&gt;
  &lt;h3 id=&quot;nwTZ&quot;&gt;Обработка кликов&lt;/h3&gt;
  &lt;p id=&quot;PzE2&quot;&gt;Метод handleEvent обрабатывает клики мыши. Координаты клика преобразуются в индексы плитки, и если плитка соседствует с пустой, вызывается moveTile для её перемещения.&lt;/p&gt;
  &lt;h3 id=&quot;73kd&quot;&gt;Отрисовка&lt;/h3&gt;
  &lt;p id=&quot;9SLy&quot;&gt;Метод draw отрисовывает каждую плитку как прямоугольник (sf::RectangleShape) бирюзового цвета с номером, отцентрированным с помощью sf::Text. Пустая плитка (0) не отрисовывается.&lt;/p&gt;
  &lt;h3 id=&quot;HTeD&quot;&gt;Проверка решения&lt;/h3&gt;
  &lt;p id=&quot;Mzj6&quot;&gt;Метод isSolved проверяет, расположены ли плитки в порядке от 1 до 15 с пустой плиткой в правом нижнем углу.&lt;/p&gt;
  &lt;h1 id=&quot;NQWr&quot;&gt;Создаём главное меню для игры &amp;quot;Пятнашки&amp;quot;, класс MainMenu&lt;/h1&gt;
  &lt;p id=&quot;vmfQ&quot;&gt;Класс MainMenu отвечает за отображение и обработку главного меню игры. Оно включает заголовок &amp;quot;Пятнашки&amp;quot; и пять кнопок: &amp;quot;Играть&amp;quot;, &amp;quot;Правила&amp;quot;, &amp;quot;Музыка: Вкл/Выкл&amp;quot;, &amp;quot;Рестарт&amp;quot; и &amp;quot;Выход&amp;quot;. Меню использует SFML для отрисовки, поддерживает анимации кнопок и фоновую музыку. Код организован так, чтобы быть понятным и легко расширяемым.&lt;/p&gt;
  &lt;p id=&quot;6gqX&quot;&gt;Класс MainMenu наследуется от базового класса Screen, что позволяет интегрировать его в систему управления состояниями игры. &lt;/p&gt;
  &lt;p id=&quot;4oY3&quot;&gt;&lt;strong&gt;MainMenu.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;MQHk&quot; data-lang=&quot;cpp&quot;&gt;
#pragma once  

#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;SFML/Audio.hpp&amp;gt;
#include &amp;quot;Button.h&amp;quot;
#include &amp;quot;GameState.h&amp;quot;
#include &amp;quot;Transition.h&amp;quot;
#include &amp;quot;ColorfulText.h&amp;quot;
#include &amp;lt;memory&amp;gt;
#include &amp;quot;GameScreen.h&amp;quot;
#include &amp;quot;Screen.h&amp;quot;

class MainMenu : public Screen {
private:
    // Элементы интерфейса главного меню — все они уникальные указатели для удобного управления памятью
    std::unique_ptr&amp;lt;ColorfulText&amp;gt; titleText;   // Заголовок меню с цветной анимацией
    std::unique_ptr&amp;lt;Button&amp;gt; playButton;        // Кнопка &amp;quot;Играть&amp;quot;
    std::unique_ptr&amp;lt;Button&amp;gt; rulesButton;       // Кнопка &amp;quot;Правила&amp;quot;
    std::unique_ptr&amp;lt;Button&amp;gt; musicButton;       // Кнопка включения/выключения музыки
    std::unique_ptr&amp;lt;Button&amp;gt; restartButton;     // Кнопка &amp;quot;Перезапустить&amp;quot; (может использоваться в других состояниях)
    std::unique_ptr&amp;lt;Button&amp;gt; exitButton;        // Кнопка &amp;quot;Выход&amp;quot;

    sf::Music* music;            // Указатель на объект музыки (не владеем им, просто ссылка)
    GameScreen&amp;amp; gameScreen;      // Ссылка на игровой экран для перехода к игре
    bool isMusicOn;              // Флаг: включена ли музыка

    // Метод для инициализации элементов интерфейса меню
    void initialize() override;

public:
    // Конструктор: принимает окно отрисовки, текущее состояние игры, переход и игровой экран
    MainMenu(sf::RenderWindow&amp;amp; window,
        GameState&amp;amp; state,
        Transition&amp;amp; transition,
        GameScreen&amp;amp; gameScreen);

    // Обработка событий (нажатия мыши, клавиш и т.д.)
    void handleEvent(const sf::Event&amp;amp; event) override;

    // Обновление логики меню (анимация, проверка нажатий и т.д.)
    void update(float deltaTime) override;

    // Отрисовка всех элементов меню
    void draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) const override;
};&lt;/pre&gt;
  &lt;p id=&quot;mzXg&quot;&gt;Основные моменты:&lt;/p&gt;
  &lt;ul id=&quot;ag6o&quot;&gt;
    &lt;li id=&quot;UrgP&quot;&gt;&lt;strong&gt;Поля&lt;/strong&gt;: Указатели std::unique_ptr на заголовок (ColorfulText) и кнопки (Button) обеспечивают автоматическое управление памятью. Переменная isMusicOn отслеживает состояние музыки.&lt;/li&gt;
    &lt;li id=&quot;6tYq&quot;&gt;&lt;strong&gt;Зависимости&lt;/strong&gt;: Класс использует sf::Music для фоновой музыки, GameScreen для перезапуска игры и Transition для переключения между экранами.&lt;/li&gt;
    &lt;li id=&quot;4mW8&quot;&gt;&lt;strong&gt;Методы&lt;/strong&gt;: initialize настраивает элементы меню, handleEvent обрабатывает клики, update обновляет анимации, а draw отрисовывает меню.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;qFWB&quot;&gt;&lt;strong&gt;MainMenu.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;D3j4&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;MainMenu.h&amp;quot;
#include &amp;lt;stdexcept&amp;gt;

// Константы для расположения и размеров элементов интерфейса
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&amp;amp; window, GameState&amp;amp; state, Transition&amp;amp; transition, GameScreen&amp;amp; gameScreen)
    : Screen(window, state, transition), gameScreen(gameScreen), isMusicOn(true) {
    initialize();  // Инициализация всех элементов интерфейса
}

// Метод инициализации главного меню
void MainMenu::initialize() {
    ResourceManager&amp;amp; rm = ResourceManager::getInstance();

    // Получаем шрифты из менеджера ресурсов
    sf::Font&amp;amp; font = rm.getFont(&amp;quot;font&amp;quot;);
    sf::Font&amp;amp; font2 = rm.getFont(&amp;quot;font1&amp;quot;);

    // Проверяем, успешно ли загружены шрифты
    if (font.getInfo().family.empty() || font2.getInfo().family.empty()) {
        throw std::runtime_error(&amp;quot;Не удалось получить шрифты из ResourceManager&amp;quot;);
    }

    // Устанавливаем фоновое изображение
    setupBackground(&amp;quot;background&amp;quot;);

    // Создаем анимированный цветной заголовок &amp;quot;Пятнашки&amp;quot;
    titleText = std::make_unique&amp;lt;ColorfulText&amp;gt;(
        &amp;quot;font1&amp;quot;,
        L&amp;quot;Пятнашки&amp;quot;,
        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&amp;lt;Button&amp;gt;(
        L&amp;quot;Играть&amp;quot;,
        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&amp;lt;Button&amp;gt;(
        L&amp;quot;Правила&amp;quot;,
        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&amp;lt;Button&amp;gt;(
        L&amp;quot;Музыка: Вкл&amp;quot;,
        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&amp;lt;Button&amp;gt;(
        L&amp;quot;Рестарт&amp;quot;,
        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&amp;lt;Button&amp;gt;(
        L&amp;quot;Выход&amp;quot;,
        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 = &amp;amp;rm.getMusic(&amp;quot;music&amp;quot;);

    // Проверяем, что музыка загружена и готова к воспроизведению
    if (music-&amp;gt;getStatus() == sf::Music::Stopped &amp;amp;&amp;amp; music-&amp;gt;getDuration() == sf::Time::Zero) {
        throw std::runtime_error(&amp;quot;Музыкальный поток не инициализирован в ResourceManager&amp;quot;);
    }

    // Настраиваем зацикленное воспроизведение и запускаем музыку
    music-&amp;gt;setLoop(true);
    music-&amp;gt;play();
}

// Обновляет логику меню — в данном случае только анимацию заголовка
void MainMenu::update(float deltaTime) {
    titleText-&amp;gt;update(deltaTime);  // Анимация цвета заголовка
}

// Обрабатывает события от пользователя (например, нажатие мыши)
void MainMenu::handleEvent(const sf::Event&amp;amp; event) {
    // Получаем текущую позицию курсора
    sf::Vector2f mousePos(static_cast&amp;lt;float&amp;gt;(sf::Mouse::getPosition(window).x),
        static_cast&amp;lt;float&amp;gt;(sf::Mouse::getPosition(window).y));

    // Если произошло нажатие мыши
    if (event.type == sf::Event::MouseButtonPressed) {
        if (playButton-&amp;gt;isClicked(mousePos)) playButton-&amp;gt;pressAnimation();
        if (rulesButton-&amp;gt;isClicked(mousePos)) rulesButton-&amp;gt;pressAnimation();
        if (musicButton-&amp;gt;isClicked(mousePos)) musicButton-&amp;gt;pressAnimation();
        if (restartButton-&amp;gt;isClicked(mousePos)) restartButton-&amp;gt;pressAnimation();
        if (exitButton-&amp;gt;isClicked(mousePos)) exitButton-&amp;gt;pressAnimation();
    }

    // Если кнопка мыши отпущена
    if (event.type == sf::Event::MouseButtonReleased) {
        if (playButton-&amp;gt;isClicked(mousePos)) {
            playButton-&amp;gt;releaseAnimation();
            transition.startTransition(GameState::GAME);  // Переход к игре
        }
        if (rulesButton-&amp;gt;isClicked(mousePos)) {
            rulesButton-&amp;gt;releaseAnimation();
            transition.startTransition(GameState::RULES);  // Переход к правилам
        }
        if (musicButton-&amp;gt;isClicked(mousePos)) {
            musicButton-&amp;gt;releaseAnimation();
            isMusicOn = !isMusicOn;  // Переключаем состояние музыки
            musicButton-&amp;gt;setText(isMusicOn ? L&amp;quot;Музыка: Вкл&amp;quot; : L&amp;quot;Музыка: Выкл&amp;quot;);  // Обновляем текст кнопки
            if (isMusicOn &amp;amp;&amp;amp; music-&amp;gt;getStatus() != sf::Music::Playing) {
                music-&amp;gt;play();  // Включаем музыку
            }
            else if (!isMusicOn) {
                music-&amp;gt;stop();  // Выключаем музыку
            }
        }
        if (restartButton-&amp;gt;isClicked(mousePos)) {
            restartButton-&amp;gt;releaseAnimation();
            gameScreen.restartGame();  // Перезапускаем игру
            transition.startTransition(GameState::GAME);  // Переходим в игру
        }
        if (exitButton-&amp;gt;isClicked(mousePos)) {
            exitButton-&amp;gt;releaseAnimation();
            window.close();  // Закрываем окно приложения
        }
    }
}

// Отрисовывает всё содержимое главного меню
void MainMenu::draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) const {
    // Если фоновая текстура установлена — рисуем её
    if (useTexture) {
        target.draw(backgroundSprite, states);
    }
    else {
        // Иначе рисуем однотонный фон (в данном случае голубой)
        sf::RectangleShape background(sf::Vector2f(
            static_cast&amp;lt;float&amp;gt;(window.getSize().x),
            static_cast&amp;lt;float&amp;gt;(window.getSize().y)));
        background.setFillColor(sf::Color::Cyan);
        target.draw(background, states);
    }

    // Отрисовываем заголовок
    titleText-&amp;gt;draw(target, states);

    // Отрисовываем все кнопки
    target.draw(*playButton, states);
    target.draw(*rulesButton, states);
    target.draw(*musicButton, states);
    target.draw(*restartButton, states);
    target.draw(*exitButton, states);
}&lt;/pre&gt;
  &lt;h2 id=&quot;cRlU&quot;&gt;&lt;strong&gt;Создание игрового окна, класс  GameScreen&lt;/strong&gt;&lt;/h2&gt;
  &lt;p id=&quot;eJHF&quot;&gt;Игровое окно включает в себя:&lt;/p&gt;
  &lt;ul id=&quot;lD53&quot;&gt;
    &lt;li id=&quot;4WxW&quot;&gt;отрисовку поля игры,&lt;/li&gt;
    &lt;li id=&quot;PEiw&quot;&gt;анимации и интерфейс,&lt;/li&gt;
    &lt;li id=&quot;hfqg&quot;&gt;таймер прохождения,&lt;/li&gt;
    &lt;li id=&quot;9g8d&quot;&gt;обработку событий,&lt;/li&gt;
    &lt;li id=&quot;xBKR&quot;&gt;и даже отображение сообщения о победе.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;y99u&quot;&gt;Класс GameScreen — это наследник базового класса Screen, который управляет визуальными и логическими элементами экрана. Он включает следующие компоненты:&lt;/p&gt;
  &lt;ul id=&quot;Cvd4&quot;&gt;
    &lt;li id=&quot;zNqe&quot;&gt;FifteenPuzzle — основная игровая логика и поле;&lt;/li&gt;
    &lt;li id=&quot;txrt&quot;&gt;Button — кнопка возврата в меню;&lt;/li&gt;
    &lt;li id=&quot;yM8X&quot;&gt;ColorfulText — анимированный заголовок &amp;quot;Пятнашки&amp;quot;;&lt;/li&gt;
    &lt;li id=&quot;L19Q&quot;&gt;sf::Text, sf::RectangleShape — для уведомлений и таймера;&lt;/li&gt;
    &lt;li id=&quot;dL7a&quot;&gt;sf::Clock — измерение времени прохождения;&lt;/li&gt;
    &lt;li id=&quot;kNnv&quot;&gt;флаг isGameStarted — определяет, запущена ли игра.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;5HLD&quot;&gt;&lt;strong&gt;GameScreen.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;Ch5y&quot; data-lang=&quot;cpp&quot;&gt;#pragma once


#include &amp;quot;Screen.h&amp;quot;           
#include &amp;quot;FifteenPuzzle.h&amp;quot;    
#include &amp;quot;Button.h&amp;quot;           
#include &amp;quot;ColorfulText.h&amp;quot;     


class GameScreen : public Screen {
private:
    // Указатель на объект головоломки &amp;quot;Пятнашки&amp;quot;
    std::unique_ptr&amp;lt;FifteenPuzzle&amp;gt; puzzle;

    // Кнопка выхода в меню
    std::unique_ptr&amp;lt;Button&amp;gt; exitButton;

    // Цветной анимированный заголовок &amp;quot;Пятнашки&amp;quot;
    std::unique_ptr&amp;lt;ColorfulText&amp;gt; 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&amp;amp; window, GameState&amp;amp; state, Transition&amp;amp; transition);

    /*
     Обработка событий (нажатия клавиш, клики мыши и др.).
     */
    void handleEvent(const sf::Event&amp;amp; event) override;

    /*
      Отрисовка всех элементов экрана: головоломки, кнопок, текста и фона.
     */
    void draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) const override;

    /*
     Обновление состояния экрана (анимация заголовка, обновление таймера и т.д.).
     */
    void update(float deltaTime) override;

    /*
     Перезапуск игры: сброс позиций плиток, таймера, очистка сообщений.
     */
    void restartGame();
};&lt;/pre&gt;
  &lt;p id=&quot;cN9m&quot;&gt;&lt;strong&gt;GameScreen.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;Co2o&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;GameScreen.h&amp;quot;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;

// Константа для позиции Y заголовка 
constexpr float TITLE_Y_POS = 15.0f;

/*
 Конструктор класса GameScreen.
 Инициализирует игровой экран, связывает его с окном и системой состояний.
 */
GameScreen::GameScreen(sf::RenderWindow&amp;amp; window, GameState&amp;amp; state, Transition&amp;amp; transition)
    : Screen(window, state, transition),
    puzzle(std::make_unique&amp;lt;FifteenPuzzle&amp;gt;(100, 4)), // Создаем головоломку 4x4 с размером плитки 100
    isGameStarted(false) { // Игра ещё не начата
    initialize(); // Вызываем метод инициализации графических элементов
}

/*
 Метод инициализации всех графических элементов интерфейса:
 - Кнопка выхода в меню
 - Текстовые поля: сообщение о победе, таймер
 - Заголовок игры
 */
void GameScreen::initialize() {
    ResourceManager&amp;amp; rm = ResourceManager::getInstance();
    sf::Font&amp;amp; font = rm.getFont(&amp;quot;font&amp;quot;);

    if (font.getInfo().family.empty()) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить шрифт &amp;#x27;font&amp;#x27; из ResourceManager&amp;quot;);
    }

    setupBackground(&amp;quot;background1&amp;quot;); // Устанавливаем фоновое изображение

    // Создаем кнопку &amp;quot;меню&amp;quot; с заданными стилями и размерами
    exitButton = std::make_unique&amp;lt;Button&amp;gt;(
        std::wstring(L&amp;quot;м е н ю&amp;quot;),
        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-&amp;gt;setPosition(
        (window.getSize().x - puzzle-&amp;gt;getBounds().width) / 2.0f,
        150.0f
    );

    // Цветной анимированный заголовок &amp;quot;Пятнашки&amp;quot;
    titleText = std::make_unique&amp;lt;ColorfulText&amp;gt;(&amp;quot;font1&amp;quot;, L&amp;quot;Пятнашки&amp;quot;, 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-&amp;gt;getBounds().left + puzzle-&amp;gt;getBounds().width + 50.0f,
        puzzle-&amp;gt;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&amp;amp; event) {
    // Выход в меню при нажатии Esc
    if (event.type == sf::Event::KeyPressed &amp;amp;&amp;amp; event.key.code == sf::Keyboard::Escape) {
        transition.startTransition(GameState::MENU);
    }

    // Обработка нажатия на кнопку &amp;quot;меню&amp;quot;
    if (event.type == sf::Event::MouseButtonPressed) {
        sf::Vector2f mousePos = window.mapPixelToCoords(sf::Mouse::getPosition(window));
        if (exitButton-&amp;gt;isClicked(mousePos)) {
            exitButton-&amp;gt;pressAnimation(); // Анимация нажатия
        }
    }

    // Обработка отпускания кнопки мыши
    if (event.type == sf::Event::MouseButtonReleased) {
        sf::Vector2f mousePos = window.mapPixelToCoords(sf::Mouse::getPosition(window));
        if (exitButton-&amp;gt;isClicked(mousePos)) {
            exitButton-&amp;gt;releaseAnimation(); // Анимация отпускания
            transition.startTransition(GameState::MENU); // Переход в меню
        }
    }

    // Старт игры при первом взаимодействии (нажатие клавиши или клик)
    if (!isGameStarted &amp;amp;&amp;amp; (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::KeyPressed)) {
        isGameStarted = true;
        gameClock.restart(); // Запускаем таймер
    }

    // Передаем событие головоломке (например, клик на плитку)
    puzzle-&amp;gt;handleEvent(event, window);

    // Проверяем, решена ли головоломка
    if (puzzle-&amp;gt;isSolved() &amp;amp;&amp;amp; isGameStarted) {
        isGameStarted = false;

        // Получаем время прохождения
        sf::Time elapsed = gameClock.getElapsedTime();
        int minutes = static_cast&amp;lt;int&amp;gt;(elapsed.asSeconds()) / 60;
        int seconds = static_cast&amp;lt;int&amp;gt;(elapsed.asSeconds()) % 60;
        int milliseconds = static_cast&amp;lt;int&amp;gt;(elapsed.asMilliseconds()) % 1000;

        // Форматируем строку времени
        std::wstring timeString = std::wstring(L&amp;quot;Время решения: &amp;quot;) +
            (minutes &amp;lt; 10 ? L&amp;quot;0&amp;quot; : L&amp;quot;&amp;quot;) + std::to_wstring(minutes) + L&amp;quot;:&amp;quot; +
            (seconds &amp;lt; 10 ? L&amp;quot;0&amp;quot; : L&amp;quot;&amp;quot;) + std::to_wstring(seconds) + L&amp;quot;:&amp;quot; +
            (milliseconds &amp;lt; 100 ? L&amp;quot;0&amp;quot; : (milliseconds &amp;lt; 10 ? L&amp;quot;00&amp;quot; : L&amp;quot;&amp;quot;)) + std::to_wstring(milliseconds);

        // Отображаем сообщение о победе
        text.setString(L&amp;quot;                   Победа!!!\n&amp;quot; +
            timeString + L&amp;quot;\n&amp;quot; +
            L&amp;quot;Нажмите Esc или кнопку меню!&amp;quot;);
        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&amp;quot;&amp;quot;);
        timerBackground.setSize(sf::Vector2f(0, 0));
    }

    // Перезапуск игры при нажатии R
    if (event.type == sf::Event::KeyPressed &amp;amp;&amp;amp; event.key.code == sf::Keyboard::R) {
        restartGame();
    }
}

/*
 Отрисовка всех элементов экрана:
 - Фон
 - Головоломка
 - Сообщения (победа)
 - Таймер
 - Кнопки
 - Заголовок
 */
void GameScreen::draw(sf::RenderTarget&amp;amp; 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-&amp;gt;draw(target, states);          // Анимированный заголовок
}

/*
 Перезапуск игры:
 - Сброс плиток в случайное положение
 - Сброс таймера
 - Очистка текстовых сообщений
 - Восстановление таймера
 */
void GameScreen::restartGame() {
    puzzle-&amp;gt;restart(); // Перемешиваем головоломку
    isGameStarted = false; // Сбрасываем флаг начала игры
    gameClock.restart(); // Сбрасываем таймер

    text.setString(L&amp;quot;&amp;quot;); // Очищаем текст победы
    text.setCharacterSize(30); // Сбрасываем размер текста

    textBackground.setSize(sf::Vector2f(0, 0)); // Скрываем фон под текстом

    // Восстанавливаем начальное значение таймера
    timerText.setString(L&amp;quot;Время: 00:00:000&amp;quot;);
    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-&amp;gt;update(deltaTime); // Обновляем анимацию цветного заголовка

    if (isGameStarted) {
        sf::Time elapsed = gameClock.getElapsedTime();
        int minutes = static_cast&amp;lt;int&amp;gt;(elapsed.asSeconds()) / 60;
        int seconds = static_cast&amp;lt;int&amp;gt;(elapsed.asSeconds()) % 60;
        int milliseconds = static_cast&amp;lt;int&amp;gt;(elapsed.asMilliseconds()) % 1000;

        // Форматируем строку времени
        std::wstring timerString = std::wstring(L&amp;quot;Время: &amp;quot;) +
            (minutes &amp;lt; 10 ? L&amp;quot;0&amp;quot; : L&amp;quot;&amp;quot;) + std::to_wstring(minutes) + L&amp;quot;:&amp;quot; +
            (seconds &amp;lt; 10 ? L&amp;quot;0&amp;quot; : L&amp;quot;&amp;quot;) + std::to_wstring(seconds) + L&amp;quot;:&amp;quot; +
            (milliseconds &amp;lt; 100 ? L&amp;quot;0&amp;quot; : (milliseconds &amp;lt; 10 ? L&amp;quot;00&amp;quot; : L&amp;quot;&amp;quot;)) + 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
        );
    }
}&lt;/pre&gt;
  &lt;h2 id=&quot;TnAu&quot;&gt;&lt;strong&gt;Перечисление GameState&lt;/strong&gt;&lt;/h2&gt;
  &lt;p id=&quot;JxLZ&quot;&gt;&lt;strong&gt;GameState.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;tijB&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

enum class GameState { MENU, GAME, RULES };&lt;/pre&gt;
  &lt;p id=&quot;hJaW&quot;&gt;Это перечисление описывает &lt;strong&gt;три возможных состояния&lt;/strong&gt; игры:&lt;/p&gt;
  &lt;ul id=&quot;nV8O&quot;&gt;
    &lt;li id=&quot;sRAQ&quot;&gt;&lt;code&gt;MENU&lt;/code&gt; – главное меню.&lt;/li&gt;
    &lt;li id=&quot;KmlO&quot;&gt;&lt;code&gt;GAME&lt;/code&gt; – активный экран игры.&lt;/li&gt;
    &lt;li id=&quot;CqY5&quot;&gt;&lt;code&gt;RULES&lt;/code&gt; – экран с правилами.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;rx5K&quot;&gt;Оно используется для управления переходами между экранами и состояниями.&lt;/p&gt;
  &lt;h2 id=&quot;slrl&quot;&gt;Окно &amp;quot;Правила игры&amp;quot;, класс RulesScreen&lt;/h2&gt;
  &lt;p id=&quot;VBl5&quot;&gt;Класс RulesScreen — это экран с правилами игры. Он помогает игроку быстро понять суть и цель головоломки &lt;strong&gt;перед началом игры&lt;/strong&gt;. &lt;/p&gt;
  &lt;p id=&quot;9BW5&quot;&gt;Включает:&lt;/p&gt;
  &lt;ul id=&quot;4knn&quot;&gt;
    &lt;li id=&quot;6L5n&quot;&gt;анимированный заголовок;&lt;/li&gt;
    &lt;li id=&quot;pD5W&quot;&gt;текст с пошаговыми правилами;&lt;/li&gt;
    &lt;li id=&quot;fv6g&quot;&gt;пример изображения правильно собранного поля;&lt;/li&gt;
    &lt;li id=&quot;Tnnc&quot;&gt;кнопку возврата в главное меню.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;7SKS&quot;&gt;&lt;strong&gt;RulesScreen.h&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;aQTP&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;quot;Screen.h&amp;quot;

#include &amp;quot;Button.h&amp;quot;

#include &amp;quot;ColorfulText.h&amp;quot;

/*
 Класс RulesScreen представляет экран с правилами игры.
 Отображает описание правил игры &amp;quot;Пятнашки&amp;quot;, содержит анимированный заголовок,
 изображение игрового поля и кнопку возврата в главное меню.
 */
class RulesScreen : public Screen {
private:
    // Анимированный цветной заголовок &amp;quot;Правила&amp;quot;
    std::unique_ptr&amp;lt;ColorfulText&amp;gt; titleText;

    // Текстовое поле для отображения правил игры
    sf::Text text;

    // Прямоугольник-фон под текст, чтобы он был лучше виден на фоне
    sf::RectangleShape textBackground;

    // Спрайт с изображением правильно собранного поля (пример)
    sf::Sprite numbersSprite;

    // Кнопка &amp;quot;Назад&amp;quot; для возврата в главное меню
    std::unique_ptr&amp;lt;Button&amp;gt; backButton;

    /**
     Инициализирует графические элементы экрана.
     Метод вызывается один раз при создании экрана.
     Настраивает: текст, фон, спрайт, кнопку и заголовок.
     */
    void initialize() override;

public:
    /**
     Конструктор экрана правил.
     window Ссылка на SFML окно
     state Ссылка на текущее состояние приложения
     transition Объект для управления переходами между экранами
     */
    RulesScreen(sf::RenderWindow&amp;amp; window, GameState&amp;amp; state, Transition&amp;amp; transition);

    /*
     Обрабатывает события пользователя (нажатия клавиш, клики мыши).
     event Обработанное событие SFML
     */
    void handleEvent(const sf::Event&amp;amp; event) override;

    /*
     Обновляет логику экрана (анимации, состояние кнопок и т.д.).
     deltaTime Время, прошедшее с последнего кадра (в секундах)
     */
    void update(float deltaTime) override;

    /*
     Отрисовывает все элементы экрана.
     target Цель отрисовки (обычно SFML окно)
     states Рендер-состояния (не используются по умолчанию)
     */
    void draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) const override;
};&lt;/pre&gt;
  &lt;p id=&quot;UFFo&quot;&gt;&lt;strong&gt;RulesScreen.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;iUoF&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;RulesScreen.h&amp;quot;
#include &amp;lt;stdexcept&amp;gt;
#include &amp;lt;iostream&amp;gt;

// Константа для позиции Y заголовка (чтобы избежать magic numbers)
constexpr float TITLE_Y_POS = 15.0f;

/*
 Конструктор класса RulesScreen.
 Инициализирует экран правил, связывает его с окном и системой состояний.
 */
RulesScreen::RulesScreen(sf::RenderWindow&amp;amp; window, GameState&amp;amp; state, Transition&amp;amp; transition)
    : Screen(window, state, transition) {
    initialize(); // Вызываем метод инициализации графических элементов
}

/*
 Метод инициализации всех графических элементов интерфейса:
 - Заголовок
 - Текст с правилами игры
 - Фон под текст
 - Изображение игрового поля
 - Кнопка возврата в меню
 */
void RulesScreen::initialize() {
    ResourceManager&amp;amp; rm = ResourceManager::getInstance();
    sf::Font&amp;amp; font = rm.getFont(&amp;quot;font&amp;quot;);

    if (font.getInfo().family.empty()) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить шрифт &amp;#x27;font&amp;#x27; из ResourceManager&amp;quot;);
    }

    setupBackground(&amp;quot;background1&amp;quot;); // Устанавливаем фоновое изображение

    // Анимированный цветной заголовок &amp;quot;Пятнашки&amp;quot;
    titleText = std::make_unique&amp;lt;ColorfulText&amp;gt;(&amp;quot;font1&amp;quot;, L&amp;quot;Пятнашки&amp;quot;, 100,
        sf::Vector2f(0, TITLE_Y_POS), window, true, false);

    // Настройка текстового поля с правилами игры
    text.setFont(font);
    text.setString(
        L&amp;quot;   1. Игровое поле состоит из 15 пронумерованных\n&amp;quot;
        L&amp;quot;   плиток и одного пустого места.\n&amp;quot;
        L&amp;quot;   2. Цель игры перемещать плитки, расположить\n&amp;quot;
        L&amp;quot;   их в правильном порядке: (см. изображение справа)\n&amp;quot;
        L&amp;quot;   3. Перемещать можно только плитки,\n&amp;quot;
        L&amp;quot;   соседние с пустым местом.\n&amp;quot;
        L&amp;quot;   4. Используйте для перемещения плиток левую кнопку\n&amp;quot;
        L&amp;quot;   мыши.\n\n&amp;quot;
        L&amp;quot;   Удачи!&amp;quot;
    );
    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&amp;amp; numTexture = rm.getTexture(&amp;quot;numbers&amp;quot;);
    if (numTexture.getSize().x == 0) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Ошибка: Не удалось загрузить изображение &amp;#x27;numbers&amp;#x27; из ResourceManager.\n&amp;quot;;
    }
    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);

    // Создаём кнопку &amp;quot;Назад&amp;quot; внизу экрана
    backButton = std::make_unique&amp;lt;Button&amp;gt;(
        std::wstring(L&amp;quot;м е н ю&amp;quot;),
        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 — возврат в меню
 - Нажатие/отпускание кнопки &amp;quot;Назад&amp;quot;
 */
void RulesScreen::handleEvent(const sf::Event&amp;amp; event) {
    // Выход в меню при нажатии Esc
    if (event.type == sf::Event::KeyPressed &amp;amp;&amp;amp; event.key.code == sf::Keyboard::Escape) {
        transition.startTransition(GameState::MENU);
    }

    // Обработка нажатия на кнопку &amp;quot;Назад&amp;quot;
    if (event.type == sf::Event::MouseButtonPressed) {
        sf::Vector2f mousePos = window.mapPixelToCoords(sf::Mouse::getPosition(window));
        if (backButton-&amp;gt;isClicked(mousePos)) {
            backButton-&amp;gt;pressAnimation(); // Анимация нажатия
        }
    }

    // Обработка отпускания кнопки мыши
    if (event.type == sf::Event::MouseButtonReleased) {
        sf::Vector2f mousePos = window.mapPixelToCoords(sf::Mouse::getPosition(window));
        if (backButton-&amp;gt;isClicked(mousePos)) {
            backButton-&amp;gt;releaseAnimation(); // Анимация отпускания
            transition.startTransition(GameState::MENU); // Переход в меню
        }
    }
}

/*
 Отрисовка всех элементов экрана:
 - Фон
 - Текст с правилами
 - Фон под текстом
 - Изображение игрового поля
 - Кнопка &amp;quot;Назад&amp;quot;
 - Анимированный заголовок
 */
void RulesScreen::draw(sf::RenderTarget&amp;amp; 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);        // Рисуем кнопку &amp;quot;Назад&amp;quot;
    titleText-&amp;gt;draw(target, states);         // Рисуем анимированный заголовок
}

/*
 Обновление состояния экрана:
 - Обновление анимации заголовка
 */
void RulesScreen::update(float deltaTime) {
    titleText-&amp;gt;update(deltaTime); // Обновляем анимацию цветного заголовка
}&lt;/pre&gt;
  &lt;h2 id=&quot;riTd&quot;&gt;main — Сердце игры &amp;quot;Пятнашки&amp;quot;&lt;/h2&gt;
  &lt;p id=&quot;zdvO&quot;&gt;Файл main.cpp — это отправная точка всей игры. Он выполняет ключевые задачи: загружает ресурсы, создаёт окно, переключает экраны, обрабатывает события и управляет рендерингом.&lt;/p&gt;
  &lt;p id=&quot;Yuup&quot;&gt;&lt;strong&gt;main.cpp&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;qLK8&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;SFML/Audio.hpp&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;quot;MainMenu.h&amp;quot;
#include &amp;quot;GameScreen.h&amp;quot;
#include &amp;quot;RulesScreen.h&amp;quot;
#include &amp;quot;ResourceManager.h&amp;quot;
#include &amp;quot;Screen.h&amp;quot;
#include &amp;quot;resource.h&amp;quot; // Подключаем ресурсы из .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&amp;amp; rm = ResourceManager::getInstance();

    if (!rm.loadTexture(&amp;quot;puzzle&amp;quot;, IDR_TEXTURE1)) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить текстуру &amp;#x27;puzzle&amp;#x27;&amp;quot;);
    }
    if (!rm.loadTexture(&amp;quot;background&amp;quot;, IDR_TEXTURE2)) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить текстуру &amp;#x27;background&amp;#x27;&amp;quot;);
    }
    if (!rm.loadTexture(&amp;quot;background1&amp;quot;, IDR_TEXTURE3)) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить текстуру &amp;#x27;background1&amp;#x27;&amp;quot;);
    }
    if (!rm.loadTexture(&amp;quot;background2&amp;quot;, IDR_TEXTURE4)) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить текстуру &amp;#x27;background2&amp;#x27;&amp;quot;);
    }
    if (!rm.loadTexture(&amp;quot;numbers&amp;quot;, IDR_TEXTURE5)) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить текстуру &amp;#x27;numbers&amp;#x27;&amp;quot;);
    }
    if (!rm.loadMusic(&amp;quot;music&amp;quot;, IDR_SOUND1)) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить звук &amp;#x27;music&amp;#x27;&amp;quot;);
    }
    if (!rm.loadFont(&amp;quot;font&amp;quot;, IDR_FONT1)) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить шрифт &amp;#x27;font&amp;#x27;&amp;quot;);
    }
    if (!rm.loadFont(&amp;quot;font1&amp;quot;, IDR_FONT2)) {
        throw std::runtime_error(&amp;quot;Не удалось загрузить шрифт &amp;#x27;font1&amp;#x27;&amp;quot;);
    }
}

/*
 Точка входа в приложение.
 Создаёт окно игры, инициализирует ресурсы, создаёт экраны (меню, игра, правила),
 запускает основной цикл отрисовки и обработки событий.
 */
int main() {
    system(&amp;quot;chcp 1251&amp;quot;); // Устанавливаем кодовую страницу Windows-1251 для корректного вывода в консоль

    try {
        // Загружаем все игровые ресурсы
        initializeResources();

        // Создаем главное окно приложения
        sf::RenderWindow window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), L&amp;quot;Пятнашки&amp;quot;);
        window.setFramerateLimit(60); // Ограничиваем частоту кадров до 60 FPS

        // Загружаем и устанавливаем иконку окна
        ResourceManager&amp;amp; rm = ResourceManager::getInstance();
        sf::Image icon = rm.getImage(&amp;quot;puzzle&amp;quot;);

        if (icon.getSize().x == 0 || icon.getSize().y == 0) {
            throw std::runtime_error(&amp;quot;Ошибка: не удалось загрузить иконку &amp;#x27;puzzle&amp;#x27; из ресурсов!&amp;quot;);
        }

        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 = &amp;amp;mainMenu;

        // Часы для измерения времени между кадрами
        sf::Clock clock;

        while (window.isOpen()) {
            // Вычисляем время с последнего кадра (ограничиваем его)
            float deltaTime = clock.restart().asSeconds();
            if (deltaTime &amp;gt; MAX_DELTA_TIME) deltaTime = MAX_DELTA_TIME;

            // Обработка событий SFML
            sf::Event event;
            while (window.pollEvent(event)) {
                if (event.type == sf::Event::Closed) {
                    window.close(); // Закрытие окна по нажатию крестика
                }
                currentScreen-&amp;gt;handleEvent(event); // Передаем событие текущему экрану
            }

            // Обновляем состояние перехода между экранами
            bool isTransitioning = transition.update(state, deltaTime);

            // Обновляем текущий экран
            currentScreen-&amp;gt;update(deltaTime);

            // Отрисовка
            window.clear(); // Очищаем окно
            window.draw(*currentScreen); // Рисуем текущий экран
            if (isTransitioning) {
                window.draw(transition); // Рисуем анимацию перехода поверх экрана
            }
            window.display(); // Отображаем всё на экране

            // Переключение экранов в зависимости от состояния
            switch (state) {
            case GameState::MENU:
                currentScreen = &amp;amp;mainMenu;
                break;
            case GameState::GAME:
                currentScreen = &amp;amp;gameScreen;
                break;
            case GameState::RULES:
                currentScreen = &amp;amp;rulesScreen;
                break;
            default:
                break;
            }
        }
    }
    catch (const std::exception&amp;amp; e) {
        // Ловим и выводим ошибки
        std::cerr &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}&lt;/pre&gt;
  &lt;p id=&quot;dnOV&quot;&gt;main.cpp — это диспетчер игры. Он не содержит логику самих экранов, а только управляет их жизненным циклом и переключениями. Благодаря такой структуре код легко поддерживать и расширять — можно добавлять новые экраны или менять ресурсы, не затрагивая остальное.&lt;/p&gt;
  &lt;figure id=&quot;jbFJ&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/1a/83/1a831b8d-0eb2-43e0-9733-7052ecd7de3e.gif&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;4lCo&quot;&gt;&lt;a href=&quot;https://github.com/cnet-sudo/fifteen_puzzle&quot; target=&quot;_blank&quot;&gt;Клонировать репозиторий &lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;brRM&quot;&gt;&lt;a href=&quot;https://t.me/ue4_dev_soft/191&quot; target=&quot;_blank&quot;&gt;Скачать файлы&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;h3h3&quot;&gt;&lt;a href=&quot;https://t.me/C_Verhovcevo_NVK&quot; target=&quot;_blank&quot;&gt;Наш телеграмм&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;vvNL&quot;&gt;&lt;a href=&quot;https://www.youtube.com/@%D0%98%D1%81%D0%BA%D1%83%D1%81%D1%81%D1%82%D0%B2%D0%BE%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%D0%BD%D0%B0%D1%8F%D0%B7%D1%8B&quot; target=&quot;_blank&quot;&gt;Наш ютюб &lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>gamedeveloper:CKUfS-Q-mGT</id><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper/CKUfS-Q-mGT?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><title>Менеджер ресурсов и динамический текст на SFML</title><published>2025-04-19T12:37:02.146Z</published><updated>2025-04-19T12:37:02.146Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/90/53/90536cf3-3b26-4433-afcd-3c9347319717.png"></media:thumbnail><category term="mul-timedijnaya-biblioteka-sfml-c" label="Мультимедийная библиотека  SFML C++"></category><summary type="html">&lt;img src=&quot;https://img2.teletype.in/files/d9/cd/d9cdf266-65c4-4689-bb18-a75f575d97cf.jpeg&quot;&gt;Когда Вы только начинаете изучать игровую разработку на C++ с использованием SFML, перед многими из Вас встаёт вопрос: как правильно организовать хранение и загрузку игровых ресурсов? Сначала всё кажется просто - загружаешь текстуру здесь, шрифт там, звук где-то ещё. Но когда Ваш проект начинает расти, Вы сталкиваетесь с реальными проблемами.</summary><content type="html">
  &lt;figure id=&quot;peqr&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/d9/cd/d9cdf266-65c4-4689-bb18-a75f575d97cf.jpeg&quot; width=&quot;1344&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;g4NA&quot;&gt;Когда Вы только начинаете изучать игровую разработку на C++ с использованием SFML, перед многими из Вас встаёт вопрос: как правильно организовать хранение и загрузку игровых ресурсов? Сначала всё кажется просто - загружаешь текстуру здесь, шрифт там, звук где-то ещё. Но когда Ваш проект начинает расти, Вы сталкиваетесь с реальными проблемами.&lt;/p&gt;
  &lt;p id=&quot;Q27P&quot;&gt;Одна и та же текстурная карта может загружаться в память трижды - для главного меню, для уровня и для редактора. В этом случае память расходуется неэффективно, а код превращается  в спагетти из вызовов loadFromFile, разбросанных по всей кодовой базе. Тогда Вы начинаете понимать, что нужен системный подход.&lt;/p&gt;
  &lt;p id=&quot;jp5J&quot;&gt;Какие варианты есть, это: хранение ресурсов в отдельных классах, глобальные переменные (да, я знаю, что это плохо), система с ручным управлением жизненного цикла.&lt;/p&gt;
  &lt;p id=&quot;eYry&quot;&gt;Каждый из этих подходов имеет свои недостатки. Либо ресурсы живут слишком долго, либо их освобождение происходит не в тот момент, либо доступ к ним неудобный.&lt;/p&gt;
  &lt;p id=&quot;7V7d&quot;&gt;Самый оптимальный вариант, это создание класса сингелтона, для управления ресурсами.&lt;/p&gt;
  &lt;p id=&quot;WBkO&quot;&gt;Но почему бы не упаковать все ресурсы прямо в исполняемый файл?&lt;/p&gt;
  &lt;p id=&quot;ijzh&quot;&gt;Мы создадим менеджер ресурсов, который использует встроенные возможности Windows.&lt;/p&gt;
  &lt;p id=&quot;Hn0r&quot;&gt;Стоит сразу оговориться — это решение работает исключительно под Windows. Оно использует системные API-функции для доступа к ресурсам, встроенным в EXE-файл.&lt;/p&gt;
  &lt;p id=&quot;O3nf&quot;&gt;Главное преимущество этой системы — все ресурсы действительно хранятся внутри EXE-файла. Вы не представляете, как приятно, когда твоя игра представляет собой всего один файл, который можно скопировать куда угодно, и он просто работает. Никаких &amp;quot;где-то потерялась текстура&amp;quot;, &amp;quot;не найден шрифт&amp;quot; и прочих проблем с путями.&lt;/p&gt;
  &lt;p id=&quot;vMG3&quot;&gt;Конечно, у этого подхода есть свои ограничения:&lt;/p&gt;
  &lt;p id=&quot;jXH4&quot;&gt;1.    Размер EXE-файла увеличивается (но современные компьютеры с этим легко справляются)&lt;/p&gt;
  &lt;p id=&quot;Yap2&quot;&gt;2.    Невозможно изменить ресурсы без перекомпиляции&lt;/p&gt;
  &lt;p id=&quot;CrMS&quot;&gt;3.    Работает только под Windows&lt;/p&gt;
  &lt;p id=&quot;b1eJ&quot;&gt;Но для многих сценариев разработки игр эти ограничения не критичны. В моём случае плюсы перевешивают:&lt;/p&gt;
  &lt;p id=&quot;JS19&quot;&gt;1.     Простота распространения (один файл!)&lt;/p&gt;
  &lt;p id=&quot;1y6b&quot;&gt;2.     Защита ресурсов от случайного изменения&lt;/p&gt;
  &lt;p id=&quot;rBOY&quot;&gt;3.     Гарантия, что все нужные файлы на месте&lt;/p&gt;
  &lt;p id=&quot;vjXZ&quot;&gt;Если вам нужна кроссплатформенность, этот подход не подойдёт. Но если вы, как и я, разрабатываете игры исключительно под Windows и цените простоту распространения, то встроенные ресурсы Windows — отличное решение. В следующих разделах я подробно расскажу, как реализовать эту систему с нуля.&lt;/p&gt;
  &lt;h3 id=&quot;BFuG&quot;&gt;&lt;strong&gt;Структура проекта в Visual Studio 2022&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;lpMc&quot;&gt;📁 Проект/&lt;/p&gt;
  &lt;p id=&quot;JgeD&quot;&gt;├── 📄 main.cpp                        # Главный файл с основным циклом&lt;/p&gt;
  &lt;p id=&quot;WuY9&quot;&gt;├── 📄 ResourceManager.h      # Заголовочный файл менеджера ресурсов&lt;/p&gt;
  &lt;p id=&quot;pTTZ&quot;&gt;├── 📄 ResourceManager.cpp # Реализация менеджера ресурсов&lt;/p&gt;
  &lt;p id=&quot;Sk5f&quot;&gt;├── 📄 ColorfulText.h               # Заголовочный файл анимированного текста&lt;/p&gt;
  &lt;p id=&quot;x72v&quot;&gt;├── 📄 ColorfulText.cpp          # Реализация анимированного текста&lt;/p&gt;
  &lt;p id=&quot;9J49&quot;&gt;├── 📄 resource.h                    # Идентификаторы ресурсов&lt;/p&gt;
  &lt;p id=&quot;ajUT&quot;&gt;└── 📄 resources.rc                 # Скрипт компиляции ресурсов&lt;/p&gt;
  &lt;h3 id=&quot;Ym3p&quot;&gt;Файлы ресурсов&lt;/h3&gt;
  &lt;figure id=&quot;FeQk&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/a2/2f/a22fb23a-e6e8-4790-8e7f-841342837c93.png&quot; width=&quot;480&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;SPXv&quot;&gt;Ожидаемый результат от программы&lt;/h3&gt;
  &lt;figure id=&quot;pV48&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/0e/55/0e5567a0-6160-409b-b2dc-684bb75f1eb8.gif&quot; width=&quot;800&quot; /&gt;
  &lt;/figure&gt;
  &lt;h2 id=&quot;dYgq&quot;&gt;&lt;strong&gt;Начинаем творить чудеса &lt;/strong&gt;&lt;/h2&gt;
  &lt;p id=&quot;iKK3&quot;&gt;Сначала создаём в ручную файлы ресурсов используя редактор кода С++ нашей IDE. Надеюсь Вы уже справились с установкой библиотеки SFML, если нет почитайте статьи выше. &lt;/p&gt;
  &lt;h3 id=&quot;DM1z&quot;&gt;resource.h&lt;/h3&gt;
  &lt;pre id=&quot;ZQQa&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#define IDR_FONT1      201  // Шрифт
#define IDR_FONT2      202  // Шрифт

#define IDR_TEXTURE1   301  // Текстура&lt;/pre&gt;
  &lt;h3 id=&quot;UW48&quot;&gt;resource.rc&lt;/h3&gt;
  &lt;pre id=&quot;TItM&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;resource.h&amp;quot;

// ресурсы по умолчанию
IDR_TEXTURE1   RCDATA  &amp;quot;assets/puzzle.png&amp;quot;
IDR_FONT1      RCDATA  &amp;quot;assets/arial.ttf&amp;quot;
IDR_FONT2      RCDATA  &amp;quot;assets/glv.ttf&amp;quot;
IDR_ICON1      ICON    &amp;quot;assets/puzzle.ico&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;FtJd&quot;&gt;В нашей программе все ресурсы находятся в отдельной папке  assets, которая в свою очередь находится в корневом каталоге проекта. &lt;/p&gt;
  &lt;p id=&quot;lJo8&quot;&gt;Теперь создадим класс управления ресурсами.&lt;/p&gt;
  &lt;h3 id=&quot;hvX1&quot;&gt;ResourceManager.h&lt;/h3&gt;
  &lt;pre id=&quot;ZtHm&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;SFML/Audio.hpp&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;optional&amp;gt;
#include &amp;lt;source_location&amp;gt;
#include &amp;lt;windows.h&amp;gt;

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

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

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

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

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

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

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

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

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

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

    // Запрещаем копирование и перемещение
    ResourceManager(const ResourceManager&amp;amp;) = delete;
    ResourceManager&amp;amp; operator=(const ResourceManager&amp;amp;) = delete;
    ResourceManager(ResourceManager&amp;amp;&amp;amp;) = delete;
    ResourceManager&amp;amp; operator=(ResourceManager&amp;amp;&amp;amp;) = 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&amp;lt;typename T&amp;gt;
    [[nodiscard]] bool loadResource(int resourceId, T&amp;amp; target,
        const std::source_location&amp;amp; loc = std::source_location::current()) 
        noexcept;

    // Хранилища ресурсов
    std::unordered_map&amp;lt;std::string, sf::Font&amp;gt; fonts_; // Шрифты
    // Звуковые буферы
    std::unordered_map&amp;lt;std::string, sf::SoundBuffer&amp;gt; buffers_; 
    std::unordered_map&amp;lt;std::string, sf::Texture&amp;gt; textures_; // Текстуры
    // Музыкальные потоки
    std::unordered_map&amp;lt;std::string, std::unique_ptr&amp;lt;sf::Music&amp;gt;&amp;gt; 
    musicStreams_; 
    // Кэш изображений
    std::unordered_map&amp;lt;std::string, sf::Image&amp;gt; imageCache_;
};&lt;/pre&gt;
  &lt;h3 id=&quot;rdSd&quot;&gt;ResourceManager.cpp&lt;/h3&gt;
  &lt;pre id=&quot;kafi&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;ResourceManager.h&amp;quot;
#include &amp;lt;iostream&amp;gt;

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

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

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

    // Использование RAII-обертки для управления ресурсом
    ResourceHandle resourceHandle(resource, handle);
    const void* data = resourceHandle.data();
    if (!data) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Ошибка в &amp;quot; &amp;lt;&amp;lt; loc
        .function_name() &amp;lt;&amp;lt; &amp;quot; (строка &amp;quot; &amp;lt;&amp;lt; loc.line()
            &amp;lt;&amp;lt; &amp;quot;): Не удалось заблокиро
            вать ресурс с ID &amp;quot; &amp;lt;&amp;lt; resourceId &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
        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&amp;amp; loc) noexcept {
    sf::Font font;
    if (!loadResource(resourceId, font, loc)) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Оши
        бка в &amp;quot; &amp;lt;&amp;lt; loc.function_name() &amp;lt;&amp;lt; &amp;quot; (строка &amp;quot; &amp;lt;&amp;lt; loc.line()
            &amp;lt;&amp;lt; &amp;quot;): Не удалось загрузить шрифт &amp;#x27;&amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot;&amp;#x27;\n&amp;quot;;
        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&amp;amp; loc) noexcept {
    sf::SoundBuffer buffer;
    if (!loadResource(resourceId, buffer, loc)) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Ошибка в &amp;quot; &amp;lt;&amp;lt; loc.function_name() &amp;lt;&amp;lt; &amp;quot; (строка &amp;quot; 
        &amp;lt;&amp;lt; loc.line()
            &amp;lt;&amp;lt; &amp;quot;): Не удалось загрузить звук &amp;#x27;&amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot;&amp;#x27;\n&amp;quot;;
        return false;
    }
    // Сохранение звукового буфера в словаре
    buffers_[std::string(id)] = std::move(buffer);
    return true;
}

// Загрузка текстуры из ресурсов
bool ResourceManager::loadTexture(std::string_view id, int resourceId, 
const std::source_location&amp;amp; loc) noexcept {
    sf::Texture texture;
    if (!loadResource(resourceId, texture, loc)) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Ошибка в &amp;quot; &amp;lt;&amp;lt; loc.function_name() &amp;lt;&amp;lt; &amp;quot; (строка &amp;quot; 
        &amp;lt;&amp;lt; loc.line()
            &amp;lt;&amp;lt; &amp;quot;): Не удалось загрузить текстуру &amp;#x27;&amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot;&amp;#x27;\n&amp;quot;;
        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&amp;amp; loc) noexcept {
    // Поиск музыкального ресурса
    HRSRC resource = FindResource(NULL, MAKEINTRESOURCE(resourceId), 
    RT_RCDATA);
    if (!resource) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Ошибка в &amp;quot; &amp;lt;&amp;lt; loc.function_name() &amp;lt;&amp;lt; &amp;quot; (строка &amp;quot; 
        &amp;lt;&amp;lt; loc.line()
            &amp;lt;&amp;lt; &amp;quot;): Не удалось найти музыкальный ресурс &amp;#x27;&amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot;&amp;#x27;\n&amp;quot;;
        return false;
    }

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

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

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

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

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

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

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

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

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

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

// Получение музыкального потока по идентификатору
std::optional&amp;lt;sf::Music*&amp;gt; ResourceManager::getMusic(std::string_view id, 
const std::source_location&amp;amp; loc) noexcept {
    auto it = musicStreams_.find(std::string(id));
    if (it == musicStreams_.end()) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Ошибка в &amp;quot; &amp;lt;&amp;lt; loc.function_name() &amp;lt;&amp;lt; &amp;quot; (строка &amp;quot; 
        &amp;lt;&amp;lt; loc.line()
            &amp;lt;&amp;lt; &amp;quot;): Музыкальный поток &amp;#x27;&amp;quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &amp;quot;&amp;#x27; не найден\n&amp;quot;;
        return std::nullopt;
    }
    return it-&amp;gt;second.get();
}&lt;/pre&gt;
  &lt;p id=&quot;R92U&quot;&gt;Теперь мы можем создать класс для создания феерического текста&lt;/p&gt;
  &lt;h3 id=&quot;kpIt&quot;&gt;ColorfulText.h&lt;/h3&gt;
  &lt;pre id=&quot;hayU&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;string_view&amp;gt;
#include &amp;lt;random&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;optional&amp;gt;
#include &amp;lt;ranges&amp;gt;
#include &amp;lt;concepts&amp;gt;
#include &amp;lt;source_location&amp;gt;

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

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

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

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

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

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

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

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

    // Переопределение метода отрисовки из sf::Drawable
    void draw(sf::RenderTarget&amp;amp; 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&amp;lt;sf::Text&amp;gt; letters_;          // Вектор букв текста
    float colorTimer_ = 0.0f;                // Таймер для эффектов
    Effect currentEffect_;                   // Текущий эффект
    float waveOffset_ = 0.0f;           // Смещение для плавных эффектов
    std::vector&amp;lt;float&amp;gt; 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;   // Шанс мерцания
};
&lt;/pre&gt;
  &lt;h3 id=&quot;4Qn2&quot;&gt;ColorfulText.cpp&lt;/h3&gt;
  &lt;pre id=&quot;IaYZ&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;ColorfulText.h&amp;quot;
#include &amp;quot;ResourceManager.h&amp;quot;
#include &amp;lt;cmath&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;iostream&amp;gt;

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

    // Создаем объект с помощью приватного конструктора
    ColorfulText result;
    ResourceManager&amp;amp; 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()), [&amp;amp;](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_ &amp;gt;= 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&amp;lt;int&amp;gt; dis(0, 255);

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

void ColorfulText::updateWave(float deltaTime) {
    // Эффект волны цветов
    std::ranges::for_each(std::views::iota(size_t{ 0 }, 
    letters_.size()), [&amp;amp;](size_t i) {
        float phase = waveOffset_ + i * 0.2f;
        sf::Uint8 value = static_cast&amp;lt;sf::Uint8&amp;gt;(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&amp;amp; letter) {
        sf::Color color = letter.getFillColor();
        color.a = static_cast&amp;lt;sf::Uint8&amp;gt;(alpha);
        letter.setFillColor(color);
        });
}

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

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

    std::ranges::for_each(std::views::iota(size_t{ 0 }, 
    letters_.size()), [&amp;amp;](size_t i) {
        twinkleTimers_[i] += deltaTime;
        if (twinkleTimers_[i] &amp;gt;= COLOR_CHANGE_INTERVAL) {
            if (dis(gen) &amp;lt; 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&amp;amp; target, 
sf::RenderStates states) const {
    // Отрисовка всех букв
    for (const auto&amp;amp; letter : letters_) {
        target.draw(letter, states);
    }
}

void ColorfulText::setPosition(const sf::Vector2f&amp;amp; 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()), [&amp;amp;](size_t i) {
        letters_[i].setPosition(xPos + i * 
        letters_[0].getCharacterSize() * 0.8f, yPos);
        });
}&lt;/pre&gt;
  &lt;p id=&quot;il5Z&quot;&gt;А теперь все объединим в исполняем файле.&lt;/p&gt;
  &lt;h3 id=&quot;xN3j&quot;&gt;Мигающие_буквы.cpp&lt;/h3&gt;
  &lt;pre id=&quot;5d0F&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;quot;ColorfulText.h&amp;quot;
#include &amp;quot;ResourceManager.h&amp;quot;
#include &amp;quot;resource.h&amp;quot;
#include &amp;lt;iostream&amp;gt;

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

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

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

    // Создание текста с юникодом
    auto text = ColorfulText::create(&amp;quot;font&amp;quot;, L&amp;quot;Привет, мир!&amp;quot;, 100,
        sf::Vector2f(0, 0), window, true, true, 
        ColorfulText::Effect::Pulse);
    if (!text) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Не удалось создать ColorfulText\n&amp;quot;;
        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-&amp;gt;setEffect(
                ColorfulText::Effect::RandomColors); break;
                case sf::Keyboard::F2: text-&amp;gt;setEffect(
                ColorfulText::Effect::Wave); break;
                case sf::Keyboard::F3: text-&amp;gt;setEffect(
                ColorfulText::Effect::Blink); break;
                case sf::Keyboard::F4: text-&amp;gt;setEffect(
                ColorfulText::Effect::Rainbow); break;
                case sf::Keyboard::F5: text-&amp;gt;setEffect(
                ColorfulText::Effect::Pulse); break;
                case sf::Keyboard::F6: text-&amp;gt;setEffect(
                ColorfulText::Effect::Fade); break;
                case sf::Keyboard::F7: text-&amp;gt;setEffect(
                ColorfulText::Effect::Twinkle); break;
                }
            }
        }

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

        window.clear(sf::Color::Blue);
        window.draw(*text);
        window.display();
    }
    return 0;
}
&lt;/pre&gt;
  &lt;p id=&quot;T0Gn&quot;&gt;Надеюсь у Вас всё получилось и до новых встреч !!!&lt;/p&gt;
  &lt;p id=&quot;8sFF&quot;&gt;&lt;a href=&quot;https://t.me/C_Verhovcevo_NVK&quot; target=&quot;_blank&quot;&gt;Телеграмм канал - Программирование игр С++&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>gamedeveloper:dpV_zrPwzWf</id><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper/dpV_zrPwzWf?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><title>Пишем калькулятор на C++ с SFML </title><published>2025-03-16T15:03:31.841Z</published><updated>2025-03-16T18:44:28.641Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img1.teletype.in/files/88/66/8866a024-ba41-441e-88ed-f29334327952.png"></media:thumbnail><category term="mul-timedijnaya-biblioteka-sfml-c" label="Мультимедийная библиотека  SFML C++"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/c7/0e/c70e7c09-3a8c-4448-8154-2ed51f44ece7.jpeg&quot;&gt;Привет, коллеги и доброжелательные критики! Сегодня я решил отвлечься от  своей грамозкой работы, чтобы написать что-то простое, но с изюминкой — калькулятор с графическим интерфейсом на C++20 и SFML.  Этот проект — не претензия на что-то грандиозное, а скорее лёгкий эксперимент, чтобы вспомнить, как приятно писать код, который сразу видно на экране. Заодно я поделюсь с вами своими мыслями, подходами и парой советов. Давайте разберём, как я это закрутил и почему выбрал именно SFML.</summary><content type="html">
  &lt;figure id=&quot;7eJt&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/c7/0e/c70e7c09-3a8c-4448-8154-2ed51f44ece7.jpeg&quot; width=&quot;1344&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;JWwB&quot;&gt;Привет, коллеги и доброжелательные критики! Сегодня я решил отвлечься от  своей громоздкой работы, чтобы написать что-то простое, но с изюминкой — калькулятор с графическим интерфейсом на C++20 и SFML.  Этот проект — не претензия на что-то грандиозное, а скорее лёгкий эксперимент, чтобы вспомнить, как приятно писать код, который сразу видно на экране. Заодно я поделюсь с вами своими мыслями, подходами и парой советов. Давайте разберём, как я это закрутил и почему выбрал именно SFML.&lt;/p&gt;
  &lt;h3 id=&quot;WD8J&quot;&gt;Почему калькулятор? И почему SFML?&lt;/h3&gt;
  &lt;p id=&quot;kyIf&quot;&gt;Калькулятор — это классика программирования. Помню, как в начале карьеры, ещё в нулевых, писал такие на Pascal для курсовых, потом переделывал их на C с самописным парсером для зачётов. Это как &amp;quot;Hello, World&amp;quot;, только с кнопками и математикой — отличный способ проверить свои навыки. Сейчас, конечно, можно было бы взять что-то посерьёзнее: Qt для полноценного GUI, SDL для низкоуровневого контроля или даже Unreal Engine, если уж совсем размахнуться. Но я остановился на SFML, а конкретно на версии 2.6.1. Почему именно она? Это последняя стабильная версия на март 2025 года, и она отлично дружит с современными компиляторами — GCC 12, MSVC 2022 и даже Clang 17. В ней есть мелкие улучшения рендеринга, поддержка C++17/20 из коробки и никаких сюрпризов с совместимостью, что для меня как человека без дополнительного запаса времени очень важно — не люблю тратить своё драгоценное время на борьбу с зависимостями.&lt;/p&gt;
  &lt;p id=&quot;mYV6&quot;&gt;SFML я выбрал не просто так. Это лёгкая библиотека для 2D-графики, которая не заставляет тебя писать тонны boilerplate-кода, как Qt, или возиться с низкоуровневыми деталями, как SDL. Сравните с другими системами: Qt — это тяжеловес с кучей возможностей, но для калькулятора его функционал избыточен, как если бы вы использовали танк для поездки в магазин. SDL даёт больше контроля, но требует больше ручной работы — например, самому рисовать текстуры или управлять контекстом OpenGL. SFML же сразу предлагает готовые примитивы вроде RectangleShape, Text и Sprite, что идеально для простого GUI. Её плюсы: минимализм (быстро настраивается), производительность (для 2D почти не жрёт ресурсов), кроссплатформенность (Windows, Linux, macOS без лишних телодвижений). Почему не SFML 3.1, потому - что синтаксис поменялся и нужно потратить время, чтобы вникнуть, а времени увы очень мало. Может в будущем я и буду писать код на SFML 3.1, но точно не сейчас. Да и багов хватает всегда с выходом чего-то нового. &lt;/p&gt;
  &lt;p id=&quot;lVK5&quot;&gt;Я использую SFML для небольших экспериментов, где нужна быстрая визуализация: прототипы интерфейсов, простые инструменты для отладки, визуализации данных или даже мелкие игрушки для души. Например, пару лет назад я делал простой шутер - &amp;quot;Кощей&amp;quot;, рендерил тысячи клеток в реальном времени, и она справилась без лагов. Для больших проектов я бы взял Qt или Unity, но тут задача была другая — сделать что-то рабочее за пару часов, без оверхеда и с удовольствием.&lt;/p&gt;
  &lt;h3 id=&quot;g4DK&quot;&gt;Архитектура: как я это разложил&lt;/h3&gt;
  &lt;p id=&quot;ghx7&quot;&gt;Проект у меня получился из трёх основных классов плюс точка входа. Я старался держать всё просто, но с учётом современных практик — никаких сырых указателей, никаких C-style массивов. Вот что вышло:&lt;/p&gt;
  &lt;p id=&quot;Xox2&quot;&gt;&lt;strong&gt;Button&lt;/strong&gt; — класс для кнопок. Простая обёртка над прямоугольником и текстом, но с умными указателями для управления памятью.&lt;/p&gt;
  &lt;p id=&quot;BBGg&quot;&gt;&lt;strong&gt;Calculator&lt;/strong&gt; — главный класс, который собирает интерфейс, обрабатывает клики и связывает всё с вычислениями.&lt;/p&gt;
  &lt;p id=&quot;axUC&quot;&gt;&lt;strong&gt;ExpressionEvaluator&lt;/strong&gt; — рекурсивный парсер выражений. Без него это был бы просто красивый блокнот с кнопками.&lt;/p&gt;
  &lt;p id=&quot;AqF4&quot;&gt;&lt;strong&gt;main.cpp&lt;/strong&gt; — минимальный код для запуска окна и цикла событий.&lt;/p&gt;
  &lt;p id=&quot;JThC&quot;&gt;Я сразу решил использовать std::unique_ptr вместо сырых указателей — в 2025 году это уже стандарт, и возиться с new/delete нет никакого смысла. Также заменил все массивы на std::vector — меньше багов, больше читаемости. Давайте разберём каждый кусок подробнее.&lt;/p&gt;
  &lt;p id=&quot;ZdI9&quot;&gt;&lt;strong&gt;Кнопки (Button.h)&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;JWqo&quot; data-lang=&quot;cpp&quot;&gt;#pragma once
#include &amp;lt;string&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;SFML/Graphics.hpp&amp;gt;

class Button : public sf::Drawable, public sf::Transformable {
public:
    Button(std::string text, sf::Font&amp;amp; font, unsigned int characterSize, 
           sf::Vector2f position, sf::Vector2f size)
        : m_rect{std::make_unique&amp;lt;sf::RectangleShape&amp;gt;
        (size)}, // Создаем прямоугольник кнопки
          m_text{std::make_unique&amp;lt;sf::Text&amp;gt;(text, font, charac
          terSize)} { // Создаем текст на кнопке
        
        m_rect-&amp;gt;setPosition(position); // Задаем позицию кнопки
        m_rect-&amp;gt;setFillColor(sf::Color(200, 200, 200)); // Серый фон
        m_rect-&amp;gt;setOutlineColor(sf::Color::Black); // Черная обводка
        m_rect-&amp;gt;setOutlineThickness(2); // Толщина обводки

        m_text-&amp;gt;setPosition(position.x + 20, p
        osition.y + 20); // Текст с отступом внутри кнопки
        m_text-&amp;gt;setFillColor(sf::Color::White); // Белый цвет текста
    }

    void pressEffect() { // Эффект нажатия — меняем цвет
        m_rect-&amp;gt;setFillColor(sf::Color(150, 150, 150));
        m_rect-&amp;gt;setOutlineColor(sf::Color(150, 150, 150));
    }

    void releaseEffect() { // Эффект отпускания — возвращаем исходный цвет
        m_rect-&amp;gt;setFillColor(sf::Color(200, 200, 200));
        m_rect-&amp;gt;setOutlineColor(sf::Color::Black);
    }

    std::string getText() const { // Получаем текст кнопки
        return m_text-&amp;gt;getString();
    }

    sf::FloatRect getGlobalBounds() const { 
    // Границы кнопки с учетом трансформаций
        return getTransform().transformRect(m_rect-&amp;gt;getGlobalBounds());
    }

private:
    void draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) 
    const override { // Отрисовка кнопки
        states.transform *= getTransform();
        target.draw(*m_rect, states); // Рисуем прямоугольник
        target.draw(*m_text, states); // Рисуем текст
    }

    std::unique_ptr&amp;lt;sf::RectangleShape&amp;gt; m_rect; 
    
    std::unique_ptr&amp;lt;sf::Text&amp;gt; m_text; // Умный указатель на текст
};f::Text&amp;gt; m_text; // Умный указатель на текст
};&lt;/pre&gt;
  &lt;p id=&quot;vWUM&quot;&gt;Класс кнопки — это мой первый шаг к интерфейсу. Он простой, но функциональный: прямоугольник с текстом, который реагирует на клики. Использовал std::unique_ptr, чтобы памятью управляла сама программа — никаких утечек, никакого ручного delete. Добавил визуальную обратную связь через pressEffect и releaseEffect — кнопка темнеет при нажатии, что делает UI живым. Цвета выбрал на глаз: серый фон и белый текст — классика, но в реальном проекте я бы вынес их в константы или конфиг-файл, чтобы дизайнеры могли играться. SFML тут хорош тем, что сразу даёт RectangleShape и Text — не надо самому писать шейдеры или возиться с текстурами, как в SDL. Ещё я подумал центрировать текст поумнее, но отступ в 20 пикселей для демки сгодился.&lt;/p&gt;
  &lt;p id=&quot;XQ3q&quot;&gt;&lt;strong&gt;Калькулятор (Calculator.h)&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;MlF8&quot; data-lang=&quot;cpp&quot;&gt;#pragma once
#include &amp;lt;string&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;quot;Button.h&amp;quot;
#include &amp;quot;ExpressionEvaluator.h&amp;quot;

class Calculator : public sf::Drawable, public sf::Transformable {
public:
    explicit Calculator(sf::Font&amp;amp; font) { // Конструктор принимает шрифт
        display = std::make_unique&amp;lt;sf::Recta
        ngleShape&amp;gt;(sf::Vector2f(360, 50)); // Создаем дисплей
        display-&amp;gt;setPosition(20, 20); // Позиция дисплея
        display-&amp;gt;setFillColor(sf::Color(173, 216, 230)); // Голубой фон
        display-&amp;gt;setOutlineColor(sf::Color::Black); // Черная обводка
        display-&amp;gt;setOutlineThickness(2); // Толщина обводки

        displayText = std::make_unique&amp;lt;sf::Text&amp;gt;(&amp;quot;&amp;quot;, font, 30); 
        displayText-&amp;gt;setPosition(30, 30); // Позиция текста
        displayText-&amp;gt;setFillColor(sf::Color::Black); // Черный цвет текста

        windowBackground = std::make_unique&amp;lt;sf::RectangleShape&amp;gt;(
        sf::Vector2f(400, 600)); // Фон окна
        windowBackground-&amp;gt;setFillColor(sf::Color::White); 

        const std::vector&amp;lt;std::string&amp;gt; labels = { 
        // Список меток для кнопок
            &amp;quot;7&amp;quot;, &amp;quot;8&amp;quot;, &amp;quot;9&amp;quot;, &amp;quot;/&amp;quot;, &amp;quot;4&amp;quot;, &amp;quot;5&amp;quot;, &amp;quot;6&amp;quot;, &amp;quot;*&amp;quot;, &amp;quot;1&amp;quot;, &amp;quot;2&amp;quot;, &amp;quot;3&amp;quot;, &amp;quot;-&amp;quot;,
            &amp;quot;0&amp;quot;, &amp;quot;C&amp;quot;, &amp;quot;=&amp;quot;, &amp;quot;+&amp;quot;, &amp;quot;(&amp;quot;, &amp;quot;)&amp;quot;, &amp;quot;&amp;lt;&amp;lt;&amp;lt;&amp;quot;
        };

        buttons.reserve(labels.size()); // Резервируем место под кнопки
        for (size_t i = 0; i &amp;lt; labels.size(); ++i) { 
        // Создаем кнопки в цикле
            buttons.emplace_back(std::make_unique&amp;lt;Button&amp;gt;(
                labels[i], font, 24,
                sf::Vector2f(20 + (i % 4) * 90, 100 + (i / 4) * 90),
                sf::Vector2f(80, 80)
            ));
        }
    }

    void handleEvent(const sf::Event&amp;amp; event, sf::RenderWindow&amp;amp; window) { 
    // Обработка событий
        if (event.type == sf::Event::MouseButtonPressed &amp;amp;&amp;amp; 
            event.mouseButton.button == sf::Mouse::Left) {
            for (const auto&amp;amp; button : buttons) {
                if (button-&amp;gt;getGlobalBounds().contains(
                    static_cast&amp;lt;sf::Vector2f&amp;gt;(
                    sf::Mouse::getPosition(window)))) {
                    button-&amp;gt;pressEffect(); // Анимация нажатия
                    processInput(button-&amp;gt;getText()); // Обрабатываем ввод
                    displayText-&amp;gt;setString(input); // Обновляем дисплей
                }
            }
        }
        if (event.type == sf::Event::MouseButtonReleased &amp;amp;&amp;amp; 
            event.mouseButton.button == sf::Mouse::Left) {
            for (const auto&amp;amp; button : buttons) {
                button-&amp;gt;releaseEffect(); // Возвращаем цвет
            }
        }
    }

private:
    void draw(sf::RenderTarget&amp;amp; target, sf::RenderStates states) 
    const override { // Отрисовка калькулятора
        states.transform *= getTransform();
        target.draw(*windowBackground, states); // Рисуем фон
        target.draw(*display, states); // Рисуем дисплей
        target.draw(*displayText, states); // Рисуем текст
        for (const auto&amp;amp; button : buttons) {
            target.draw(*button, states); // Рисуем все кнопки
        }
    }

    void processInput(const std::string&amp;amp; text) { // Логика обработки ввода
        using namespace std::string_literals;
        if (text == &amp;quot;C&amp;quot;s) { // Очистка
            input.clear();
        }
        else if (text == &amp;quot;=&amp;quot;s) { // Вычисление результата
            try {
                double result = ExpressionEvaluator::evaluate(input);
                input = std::to_string(result);
                if (input.ends_with(&amp;quot;.000000&amp;quot;)) { // Убираем лишние нули
                    input = input.substr(0, input.find(&amp;#x27;.&amp;#x27;));
                }
            }
            catch (const std::exception&amp;amp;) {
                input = &amp;quot;Error&amp;quot;s; // Ошибка при вычислении
            }
        }
        else if (text == &amp;quot;&amp;lt;&amp;lt;&amp;lt;&amp;quot;s) { // Удаление последнего символа
            if (!input.empty()) {
                input.pop_back();
            }
        }
        else if (text == &amp;quot;+&amp;quot;s || text == &amp;quot;-&amp;quot;s || text == &amp;quot;*&amp;quot;s || text == 
        &amp;quot;/&amp;quot;s) { // Операторы
            std::string_view ops = &amp;quot;+-*/&amp;quot;;
            if (!input.empty() &amp;amp;&amp;amp; ops.find(input.back()) == 
            std::string_view::npos &amp;amp;&amp;amp; input.length() &amp;lt; 19) {
                input += text;
            }
        }
        else if (input.length() &amp;lt; 19) { 
        // Добавляем символ, если не превышен лимит
            input += text;
        }
        displayText-&amp;gt;setString(input); // Обновляем текст на дисплее
    }

    std::unique_ptr&amp;lt;sf::RectangleShape&amp;gt; windowBackground; // Фон окна
    std::unique_ptr&amp;lt;sf::RectangleShape&amp;gt; display; // Дисплей
    std::unique_ptr&amp;lt;sf::Text&amp;gt; displayText; // Текст дисплея
    std::vector&amp;lt;std::unique_ptr&amp;lt;Button&amp;gt;&amp;gt; buttons; // Вектор кнопок
    std::string input; // Текущий ввод
};&lt;/pre&gt;
  &lt;p id=&quot;5TLi&quot;&gt;Это ядро всего проекта. Я долго думал, как организовать кнопки, и решил остановиться на сетке 4x5 — это стандартная раскладка, как на старых калькуляторах Casio, только с парой дополнительных кнопок вроде скобок и &amp;quot;backspace&amp;quot;. Размеры окна (400x600) и кнопок (80x80) подбирал вручную, чтобы всё аккуратно влезло, а отступы в 20 пикселей между элементами добавил для читаемости. Дисплей сделал голубым — просто захотелось чего-то яркого на белом фоне. В processInput вся логика ввода: защита от двойных операторов (чтобы не вводились &amp;quot;++&amp;quot; или &amp;quot;*/&amp;quot;), лимит в 19 символов (SFML начинает обрезать текст, если больше), плюс обработка ошибок вроде деления на ноль. Использовал ends_with из C++20 — мелочь, но избавляет от ручной проверки концов строки. SFML  тут хорош своей простотой: метод draw сам рендерит всё в нужном порядке, и мне не пришлось писать сложную логику обновления.&lt;/p&gt;
  &lt;p id=&quot;nHPN&quot;&gt;Ещё я заметил, что при быстрых кликах SFML немного подтормаживает — это не баг самой библиотеки, а особенность того, как я обрабатываю события. В реальном проекте я бы добавил дебаунсинг или перерисовывал только изменённые элементы, но для демки оставил как есть.&lt;/p&gt;
  &lt;p id=&quot;tKiZ&quot;&gt;&lt;strong&gt;Парсер (ExpressionEvaluator.h)&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;fQLN&quot; data-lang=&quot;cpp&quot;&gt;#pragma once
#include &amp;lt;string&amp;gt;
#include &amp;lt;string_view&amp;gt;
#include &amp;lt;stdexcept&amp;gt;
#include &amp;lt;charconv&amp;gt;

class ExpressionEvaluator {
public:
    static double evaluate(std::string_view expres
    sion) { // Вычисляем выражение
        size_t pos = 0;
        return parseExpression(expression, pos);
    }

private:
    static double parseExpression(std::string_view expr,
     size_t&amp;amp; pos) { // Разбираем выражение (+, -)
        double result = parseTerm(expr, pos);
        while (pos &amp;lt; expr.length()) {
            char op = expr[pos];
            if (op != &amp;#x27;+&amp;#x27; &amp;amp;&amp;amp; op != &amp;#x27;-&amp;#x27;) break;
            pos++;
            double term = parseTerm(expr, pos);
            result = (op == &amp;#x27;+&amp;#x27;) ? result + term : result - term;
        }
        return result;
    }

    static double parseTerm(std::strin
    g_view expr, size_t&amp;amp; pos) { // Разбираем члены (*, /)
        double result = parseFactor(expr, pos);
        while (pos &amp;lt; expr.length()) {
            char op = expr[pos];
            if (op != &amp;#x27;*&amp;#x27; &amp;amp;&amp;amp; op != &amp;#x27;/&amp;#x27;) break;
            pos++;
            double factor = parseFactor(expr, pos);
            if (op == &amp;#x27;*&amp;#x27;) result *= factor;
            else if (factor
             == 0) throw std::invalid_argument(&amp;quot;Деление на ноль!&amp;quot;);
            else result /= factor;
        }
        return result;
    }

    static double parseFactor(std::string_view expr, size_t&amp;amp; pos) 
    { // Разбираем множители (числа, скобки)
        skipWhitespace(expr, pos);
        if (pos &amp;gt;= expr.length()) throw std::invalid_argument(
        &amp;quot;Некорректное выражение&amp;quot;);

        if (expr[pos] == &amp;#x27;(&amp;#x27;) { // Обработка скобок
            pos++;
            double result = parseExpression(expr, pos);
            skipWhitespace(expr, pos);
            if (pos &amp;gt;= expr.length() || expr[pos] != &amp;#x27;)&amp;#x27;) 
                throw std::invalid_argument(&amp;quot;Нет закрывающей скобки&amp;quot;);
            pos++;
            return result;
        }

        double result{};
        auto [ptr, ec] = std::from_chars(expr.data() + pos, 
        // Парсим число
                                       expr.data() + expr.length(), 
                                       result);
        if (ec != std::errc()) throw std::invalid_argument(
        &amp;quot;Некорректное число&amp;quot;);
        pos = ptr - expr.data();
        return result;
    }

    static void skipWhitespace(std::string_view expr, size_t&amp;amp; pos) { 
    // Пропускаем пробелы
        while (pos &amp;lt; expr.length() &amp;amp;&amp;amp; std::isspace(expr[pos])) pos++;
    }
};&lt;/pre&gt;
  &lt;p id=&quot;LnRn&quot;&gt;Парсер — это, пожалуй, самая интересная часть. Я решил сделать рекурсивный спуск. Алгоритм простой, но правильный: сначала парсим скобки и числа (через parseFactor), потом умножение и деление (в parseTerm), и только потом сложение с вычитанием (в parseExpression). Это автоматически учитывает приоритет операций, так что &amp;quot;2 + 3 * 4&amp;quot; даст 14, а не 20. Использовал std::from_chars вместо stringstream — это быстрее и меньше тянет за собой STL-оверхеда. Плюс, std::string_view экономит копирования строк, что для парсера мелочь, но приятная.&lt;/p&gt;
  &lt;p id=&quot;GIIf&quot;&gt;Я добавил поддержку пробелов через skipWhitespace, хотя в этом интерфейсе она не особо нужна — просто привычка писать код с запасом на будущее. Ещё парсер выбрасывает исключения при ошибках вроде деления на ноль или незакрытых скобок — в реальном проекте я бы добавил нормальное логирование, но тут просто вывожу &amp;quot;Error&amp;quot; на дисплей. SFML тут не участвует, но без парсера калькулятор был бы просто красивой оболочкой.&lt;/p&gt;
  &lt;p id=&quot;0i7s&quot;&gt;&lt;strong&gt;Точка входа (main.cpp)&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;8r8G&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;quot;Calculator.h&amp;quot;

int main() {
    sf::RenderWindow window(sf::VideoMode(400, 600), L&amp;quot;SFML Калькулятор&amp;quot;); 
    // Создаем окно
    auto font = std::make_unique&amp;lt;sf::Font&amp;gt;(); // Загружаем шрифт
    if (!font-&amp;gt;loadFromFile(&amp;quot;arialmt.ttf&amp;quot;)) {
        std::cerr &amp;lt;&amp;lt; &amp;quot;Не удалось загрузить шрифт!\n&amp;quot;;
        return -1;
    }

    Calculator calculator(*font); // Создаем калькулятор

    while (window.isOpen()) { // Главный цикл
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) window.close(); 
            // Закрытие окна
            calculator.handleEvent(event, window); // Обработка событий
        }

        window.clear(); // Очистка экрана
        window.draw(calculator); // Отрисовка калькулятора
        window.display(); // Показываем результат
    }
    return 0;
}&lt;/pre&gt;
  &lt;p id=&quot;9AHv&quot;&gt;Точка входа — это стандартный цикл SFML. Окно 400x600 выбрал как компромисс между компактностью и читаемостью. Шрифт взял Arial, потому что он универсален и не требует возни с лицензиями — в продакшене я бы добавил fallback на системный шрифт через sf::Font::loadFromMemory или проверку через std::filesystem. SFML  тут стабильно рендерит всё без сюрпризов, хотя я заметил, что при частых кликах FPS может проседать — это не баг библиотеки, а моя реализация без оптимизаций.&lt;/p&gt;
  &lt;figure id=&quot;dWa7&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/e9/5f/e95f4f61-d58b-42bf-9c69-a97861786aa6.jpeg&quot; width=&quot;388&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;SqQX&quot;&gt;Рефлексия: что получилось и что можно лучше&lt;/h3&gt;
  &lt;p id=&quot;HvWC&quot;&gt;Проект занял у меня пару часов в субботу вечером, и результат меня приятно удивил. Калькулятор считает выражения вроде &amp;quot;2 + 3 * (4 - 1)&amp;quot; (правильно выдаёт 11), ловит ошибки вроде деления на ноль или незакрытых скобок, и выглядит аккуратно. SFML показала себя с лучшей стороны: рендеринг быстрый, API интуитивный, никаких глюков с памятью или шрифтами. C++20 тоже не подвёл: std::string_view экономит копирования, ends_with упрощает обработку строк, а std::from_chars делает парсинг чисел шустрым.&lt;/p&gt;
  &lt;p id=&quot;lYIj&quot;&gt;Нет предела совершенству, что можно улучшить:&lt;/p&gt;
  &lt;p id=&quot;Du76&quot;&gt;&lt;strong&gt;Десятичные числа&lt;/strong&gt;. Сейчас парсер понимает только целые — надо добавить поддержку точек и, возможно, научные форматы вроде &amp;quot;1.23e-4&amp;quot;.&lt;/p&gt;
  &lt;p id=&quot;tVS2&quot;&gt;&lt;strong&gt;Производительность&lt;/strong&gt;. Для коротких выражений мой рекурсивный спуск норм, но на длинных строках (например, 100 операторов) лучше взять стековый алгоритм или подключить Boost.Spirit. Я даже прикинул, как это сделать, но для демки оставил как есть.&lt;/p&gt;
  &lt;p id=&quot;Umxa&quot;&gt;&lt;strong&gt;UI/UX&lt;/strong&gt;. Клавиатурный ввод был бы логичным дополнением — сейчас только мышью тыкать. Ещё можно добавить масштабируемость окна, тёмную тему или анимации переходов между состояниями.&lt;/p&gt;
  &lt;p id=&quot;jJmS&quot;&gt;&lt;strong&gt;Тестирование&lt;/strong&gt;. Я писал на коленке, но в реальном проекте нужны юнит-тесты для парсера — хотя бы на базовые случаи вроде &amp;quot;2+2&amp;quot;, &amp;quot;1/0&amp;quot; и &amp;quot;(2+3)*4&amp;quot;.&lt;/p&gt;
  &lt;p id=&quot;XuN1&quot;&gt;&lt;strong&gt;Логирование&lt;/strong&gt;. Ошибки сейчас просто пишут &amp;quot;Error&amp;quot; на дисплей. В продакшене я бы вывел их в файл или консоль с деталями: где упало, что ввели.&lt;/p&gt;
  &lt;p id=&quot;I05a&quot;&gt;&lt;strong&gt;Оптимизация рендеринга&lt;/strong&gt;. SFML немного подтормаживает при частых кликах — можно добавить дебаунсинг на события или перерисовывать только изменённые элементы через dirty rectangles.&lt;/p&gt;
  &lt;p id=&quot;qGqR&quot;&gt;Ещё я подумал про локализацию — например, заменить точку на запятую для регионов, где так принято, но это уже избыточно для такого проекта. В целом, SFML 2.6.1 дала мне ровно то, что я хотел: быстрый старт и минимум головной боли.&lt;/p&gt;
  &lt;h3 id=&quot;90h9&quot;&gt;&lt;strong&gt;Рекомендации новичкам и не только&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;djlj&quot;&gt;Если вы только начинаете, вот что я бы посоветовал:&lt;/p&gt;
  &lt;ol id=&quot;HEWB&quot;&gt;
    &lt;li id=&quot;gq0c&quot;&gt;Не бойтесь библиотек вроде SFML — это не так страшно, как кажется. Она проще, чем Qt, и учит основам работы с графикой.&lt;/li&gt;
    &lt;li id=&quot;PfET&quot;&gt;Осваивайте современный C++ — умные указатели вроде unique_ptr и контейнеры вроде vector спасут вас от кучи багов с памятью.&lt;/li&gt;
    &lt;li id=&quot;Jqc1&quot;&gt;Попробуйте написать парсер вручную хотя бы раз — это отличное упражнение для понимания алгоритмов и структур данных.&lt;/li&gt;
    &lt;li id=&quot;Mtur&quot;&gt;Делайте визуальную обратную связь — даже простая смена цвета кнопки делает UI живым и дружелюбным.&lt;/li&gt;
    &lt;li id=&quot;qyMp&quot;&gt;Экспериментируйте с версиями — SFML 2.6.1 стабильна, но если вам нужны новые фичи, следите за веткой разработки на GitHub, уже доступна версия 3.1 .&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;3HfZ&quot;&gt;Для проффи  совет другой: возьмите такую простую задачу и доведите её до идеала.  Добавьте тесты, профилирование, конфиги, обработку граничных случаев. Это хороший способ не закиснуть на рабочих рутинах и вспомнить, почему мы вообще любим кодить. Я, например, после этого проекта задумался, как бы переписать парсер на концепты C++20 или прикрутить многопоточность для рендеринга — просто ради интереса. Спасибо за внимание и всем хорошего времени суток. &lt;/p&gt;
  &lt;p id=&quot;EJDK&quot;&gt;Творите и любите !!! Это то для чего создан человек !!!&lt;/p&gt;
  &lt;p id=&quot;fOhQ&quot;&gt;&lt;a href=&quot;https://t.me/C_Verhovcevo_NVK&quot; target=&quot;_blank&quot;&gt;Телеграмм канал - Программирование игр С++&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>gamedeveloper:KO2svQewVZP</id><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper/KO2svQewVZP?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><title>Чистая архитектура в играх на SFML С++: как разложить код по полочкам</title><published>2025-03-15T14:05:15.145Z</published><updated>2025-03-15T14:24:14.791Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/d2/c0/d2c0210a-c844-47cb-bee4-4b1e420089b0.png"></media:thumbnail><category term="mul-timedijnaya-biblioteka-sfml-c" label="Мультимедийная библиотека  SFML C++"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/46/15/46158a2a-9cbc-491b-9ba2-61503b6265ce.png&quot;&gt;Привет, разработчики! Сегодня поговорим о том, как применить чистую архитектуру (Clean Architecture) в разработке игр с использованием библиотеки SFML. Если вы хотите, чтобы ваш код был модульным, тестируемым и не привязанным к конкретной библиотеке, этот подход для вас. Давайте разберем, как разделить код на слои и сделать вашу игру структурированной и гибкой.</summary><content type="html">
  &lt;figure id=&quot;LO0W&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/46/15/46158a2a-9cbc-491b-9ba2-61503b6265ce.png&quot; width=&quot;1197&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Ct6K&quot;&gt;Привет, разработчики! Сегодня поговорим о том, как применить &lt;strong&gt;чистую архитектуру&lt;/strong&gt; (Clean Architecture) в разработке игр с использованием библиотеки SFML. Если вы хотите, чтобы ваш код был модульным, тестируемым и не привязанным к конкретной библиотеке, этот подход для вас. Давайте разберем, как разделить код на слои и сделать вашу игру структурированной и гибкой.&lt;/p&gt;
  &lt;h2 id=&quot;RzFb&quot;&gt;Что такое чистая архитектура?&lt;/h2&gt;
  &lt;p id=&quot;hnro&quot;&gt;Чистая архитектура — это концепция, предложенная Робертом Мартином (дядей Бобом), которая помогает организовать код так, чтобы он был:&lt;/p&gt;
  &lt;ul id=&quot;DmqK&quot;&gt;
    &lt;li id=&quot;Wkjl&quot;&gt;&lt;strong&gt;Независимым&lt;/strong&gt; от фреймворков (в нашем случае — SFML).&lt;/li&gt;
    &lt;li id=&quot;x2Tl&quot;&gt;&lt;strong&gt;Тестируемым&lt;/strong&gt; без запуска всей игры.&lt;/li&gt;
    &lt;li id=&quot;XKWf&quot;&gt;&lt;strong&gt;Четко разделенным&lt;/strong&gt; по зонам ответственности.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;mk1T&quot;&gt;Идея в том, чтобы игровая логика не знала о том, как рисуются спрайты или как обрабатываются нажатия клавиш. Вместо этого мы разбиваем код на слои, где каждый слой делает свою работу.&lt;/p&gt;
  &lt;h2 id=&quot;W4iR&quot;&gt;Слои чистой архитектуры в игре&lt;/h2&gt;
  &lt;p id=&quot;uBCr&quot;&gt;Представьте игру как торт: каждый слой — это отдельный уровень со своей задачей. Вот как это может выглядеть в контексте SFML:&lt;/p&gt;
  &lt;p id=&quot;g6uG&quot;&gt;&lt;strong&gt;Структура проекта&lt;/strong&gt;&lt;/p&gt;
  &lt;figure id=&quot;yOWz&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/07/f2/07f2f545-dc28-41fe-b33e-bff65d990183.png&quot; width=&quot;820&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;yQAp&quot;&gt;1. Сущности (Entities)&lt;/h3&gt;
  &lt;p id=&quot;mVND&quot;&gt;Это сердце игры — основные объекты, такие как игрок, враги или пули. Они содержат данные (позицию, здоровье, скорость) и базовую логику, но не знают ничего о SFML.&lt;/p&gt;
  &lt;p id=&quot;41g1&quot;&gt;Пример:&lt;/p&gt;
  &lt;pre id=&quot;zLTN&quot; data-lang=&quot;cpp&quot;&gt;class Player {
public:
    float x, y;
    float speed;
    int health;

    void move(float dx, float dy) {
        x += dx * speed;
        y += dy * speed;
    }
};&lt;/pre&gt;
  &lt;h3 id=&quot;WZrQ&quot;&gt;2. Бизнес-логика (Use Cases / Interactors)&lt;/h3&gt;
  &lt;p id=&quot;WlEf&quot;&gt;Здесь живут правила игры. Это слой, который отвечает за &amp;quot;что происходит&amp;quot;. Например, движение игрока, проверка столкновений или условия победы.&lt;/p&gt;
  &lt;p id=&quot;eXXU&quot;&gt;Пример:&lt;/p&gt;
  &lt;pre id=&quot;RtqQ&quot; data-lang=&quot;cpp&quot;&gt;class MovePlayerUseCase {
public:
    void execute(Player&amp;amp; player, float dx, float dy) {
        player.move(dx, dy);
    }
};&lt;/pre&gt;
  &lt;h3 id=&quot;5zYX&quot;&gt;3. Интерфейсы (Controllers / Presenters)&lt;/h3&gt;
  &lt;p id=&quot;YTR7&quot;&gt;Этот слой связывает бизнес-логику с внешним миром. Например:&lt;/p&gt;
  &lt;ul id=&quot;jmol&quot;&gt;
    &lt;li id=&quot;emno&quot;&gt;InputController — превращает нажатия клавиш в команды для игры.&lt;/li&gt;
    &lt;li id=&quot;mCLt&quot;&gt;Renderer — отвечает за отрисовку.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;9f6w&quot;&gt;Пример обработки ввода:&lt;/p&gt;
  &lt;pre id=&quot;cFYw&quot; data-lang=&quot;cpp&quot;&gt;class InputController {
public:
    void handleInput(sf::RenderWindow&amp;amp; window, 
    MovePlayerUseCase&amp;amp; moveUseCase, Player&amp;amp; player) {
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::W)) {
            moveUseCase.execute(player, 0, -1);
        }
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::S)) {
            moveUseCase.execute(player, 0, 1);
        }
    }
};&lt;/pre&gt;
  &lt;h3 id=&quot;NlhB&quot;&gt;4. Инфраструктура (Frameworks &amp;amp; Drivers)&lt;/h3&gt;
  &lt;p id=&quot;bXqI&quot;&gt;Тут живет код, который зависит от SFML: рендеринг, обработка событий, создание окон. Этот слой изолирован от остальной логики.&lt;/p&gt;
  &lt;p id=&quot;2FPA&quot;&gt;Пример рендеринга:&lt;/p&gt;
  &lt;pre id=&quot;tmL3&quot; data-lang=&quot;cpp&quot;&gt;class SfmlRenderer {
public:
    void render(sf::RenderWindow&amp;amp; window, const Player&amp;amp; player) {
        sf::CircleShape shape(20.f);
        shape.setPosition(player.x, player.y);
        shape.setFillColor(sf::Color::Green);
        window.draw(shape);
    }
};&lt;/pre&gt;
  &lt;h3 id=&quot;9pze&quot;&gt;Собираем игру: главный цикл&lt;/h3&gt;
  &lt;p id=&quot;9XTF&quot;&gt;Теперь соединим всё в главном цикле игры. &lt;/p&gt;
  &lt;p id=&quot;Pmg8&quot;&gt;Вот пример:&lt;/p&gt;
  &lt;pre id=&quot;FUEi&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;quot;Entities/Player.hpp&amp;quot;
#include &amp;quot;UseCases/MovePlayerUseCase.hpp&amp;quot;
#include &amp;quot;Interfaces/InputController.hpp&amp;quot;
#include &amp;quot;Infrastructure/SfmlRenderer.hpp&amp;quot;

int main() {
    sf::RenderWindow window(sf::VideoMode(800, 600), &amp;quot;Clean SFML Game&amp;quot;);

    Player player{400.f, 300.f, 5.f, 100}; // Начальная позиция и параметры
    MovePlayerUseCase moveUseCase;
    InputController inputController;
    SfmlRenderer renderer;

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) {
                window.close();
            }
        }

        // Ввод
        inputController.handleInput(window, moveUseCase, player);

        // Рендеринг
        window.clear();
        renderer.render(window, player);
        window.display();
    }

    return 0;
}&lt;/pre&gt;
  &lt;h3 id=&quot;Rk26&quot;&gt;Почему это круто?&lt;/h3&gt;
  &lt;ol id=&quot;KGz8&quot;&gt;
    &lt;li id=&quot;AfvF&quot;&gt;&lt;strong&gt;Гибкость&lt;/strong&gt;: Хотите заменить SFML на SDL? Просто перепишите слой инфраструктуры — остальной код не тронется.&lt;/li&gt;
    &lt;li id=&quot;5KoJ&quot;&gt;&lt;strong&gt;Тестируемость&lt;/strong&gt;: Можно проверять логику движения или столкновений без запуска окна.&lt;/li&gt;
    &lt;li id=&quot;gqsU&quot;&gt;&lt;strong&gt;Порядок&lt;/strong&gt;: Каждый слой отвечает за своё, и код становится читаемым.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h3 id=&quot;vaF2&quot;&gt;Полезные советы&lt;/h3&gt;
  &lt;p id=&quot;wa7k&quot;&gt;Используйте абстрактные классы или интерфейсы, чтобы слои общались через четкие границы.&lt;/p&gt;
  &lt;p id=&quot;C3sP&quot;&gt;Не храните состояние игры (например, координаты) в объектах SFML — держите их в сущностях.&lt;/p&gt;
  &lt;p id=&quot;Bruf&quot;&gt;Избегайте соблазна смешивать логику с рендерингом — это путь к хаосу.&lt;/p&gt;
  &lt;p id=&quot;lBB3&quot;&gt;Чистая архитектура в играх на SFML — это не просто модный подход, а способ сделать ваш проект масштабируемым и удобным для поддержки. Да, на старте придется потратить чуть больше времени на разделение кода, но в долгосрочной перспективе это окупается.&lt;/p&gt;
  &lt;p id=&quot;VHNG&quot;&gt;Если у вас есть вопросы или вы хотите разобрать конкретный пример — пишите в комментариях! &lt;/p&gt;
  &lt;p id=&quot;lJMm&quot;&gt;А как вы организуете код в своих играх? Делитесь опытом!&lt;/p&gt;
  &lt;p id=&quot;DCYb&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;FygN&quot;&gt;&lt;a href=&quot;https://t.me/C_Verhovcevo_NVK&quot; target=&quot;_blank&quot;&gt;Телеграмм канал - Программирование игр С++&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>gamedeveloper:eapvb5LgYef</id><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper/eapvb5LgYef?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><title>Визуализация быстрой сортировки с SFML и C++20</title><published>2025-03-08T15:04:05.096Z</published><updated>2025-03-08T15:04:25.579Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img3.teletype.in/files/a8/8d/a88d6c97-e663-4a0d-9d47-42a7765616f3.png"></media:thumbnail><category term="mul-timedijnaya-biblioteka-sfml-c" label="Мультимедийная библиотека  SFML C++"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/8e/ec/8eeca781-1042-4130-84ef-18bf7ca2693e.png&quot;&gt;В этой статье мы создадим интерактивную визуализацию алгоритма быстрой сортировки (QuickSort) с использованием библиотеки SFML и современных возможностей C++20. Этот проект поможет вам лучше понять, как работает один из самых популярных алгоритмов сортировки, и покажет, как можно комбинировать графику и алгоритмы для создания образовательных инструментов.</summary><content type="html">
  &lt;figure id=&quot;sfMs&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/8e/ec/8eeca781-1042-4130-84ef-18bf7ca2693e.png&quot; width=&quot;1186&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Y3GO&quot;&gt;В этой статье мы создадим интерактивную визуализацию алгоритма быстрой сортировки (QuickSort) с использованием библиотеки SFML и современных возможностей C++20. Этот проект поможет вам лучше понять, как работает один из самых популярных алгоритмов сортировки, и покажет, как можно комбинировать графику и алгоритмы для создания образовательных инструментов.&lt;/p&gt;
  &lt;h2 id=&quot;4Yky&quot;&gt;Зачем визуализировать алгоритмы?&lt;/h2&gt;
  &lt;p id=&quot;McPi&quot;&gt;Алгоритмы сортировки, такие как QuickSort, могут быть сложными для понимания, особенно для новичков. &lt;/p&gt;
  &lt;p id=&quot;GcwK&quot;&gt;Визуализация помогает:&lt;/p&gt;
  &lt;ul id=&quot;5Nu7&quot;&gt;
    &lt;li id=&quot;ZqZD&quot;&gt;Увидеть, как данные перемещаются и сравниваются на каждом шаге.&lt;/li&gt;
    &lt;li id=&quot;oFUH&quot;&gt;Понять концепции разбиения и рекурсии.&lt;/li&gt;
    &lt;li id=&quot;CL8k&quot;&gt;Сделать обучение более интерактивным и увлекательным.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;bZni&quot;&gt;Если вы хотите не только изучить алгоритмы, но и создать что-то наглядное и красивое, этот проект для вас!&lt;/p&gt;
  &lt;h2 id=&quot;ugdB&quot;&gt;Что нам понадобится?&lt;/h2&gt;
  &lt;p id=&quot;xH4Y&quot;&gt;&lt;strong&gt;SFML&lt;/strong&gt;: Библиотека для работы с графикой, событиями и окнами. &lt;a href=&quot;https://www.sfml-dev.org/&quot; target=&quot;_blank&quot;&gt;Скачать и установить&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;qVyn&quot;&gt;&lt;strong&gt;Компилятор с поддержкой C++20&lt;/strong&gt;: Например, GCC или Clang.&lt;/p&gt;
  &lt;p id=&quot;s66r&quot;&gt;&lt;strong&gt;Базовые знания C++&lt;/strong&gt;: Работа с векторами, лямбда-функциями и рекурсией.&lt;/p&gt;
  &lt;h2 id=&quot;5PAu&quot;&gt;Описание проекта&lt;/h2&gt;
  &lt;h3 id=&quot;OWzD&quot;&gt;Цель&lt;/h3&gt;
  &lt;p id=&quot;u42w&quot;&gt;Создать программу, которая:&lt;/p&gt;
  &lt;ul id=&quot;1hWH&quot;&gt;
    &lt;li id=&quot;pQwr&quot;&gt;Генерирует случайный массив чисел.&lt;/li&gt;
    &lt;li id=&quot;IfZt&quot;&gt;Визуализирует его как набор столбцов.&lt;/li&gt;
    &lt;li id=&quot;HmQb&quot;&gt;Анимирует процесс сортировки с помощью QuickSort.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h3 id=&quot;eF53&quot;&gt;Особенности&lt;/h3&gt;
  &lt;p id=&quot;BhmI&quot;&gt;Использование std::ranges из C++20 для удобной работы с диапазонами.&lt;/p&gt;
  &lt;p id=&quot;6kCd&quot;&gt;Рекурсивная реализация QuickSort с визуализацией на каждом шаге.&lt;/p&gt;
  &lt;p id=&quot;PSay&quot;&gt;Простая, но эффективная анимация с задержкой для наглядности.&lt;/p&gt;
  &lt;h2 id=&quot;eEcA&quot;&gt;Код проекта&lt;/h2&gt;
  &lt;h3 id=&quot;HuHz&quot;&gt;Подготовка&lt;/h3&gt;
  &lt;ol id=&quot;8YLp&quot;&gt;
    &lt;li id=&quot;yuZB&quot;&gt;Установите SFML и настройте ваш проект (например, через CMake или вручную подключите библиотеки).&lt;/li&gt;
    &lt;li id=&quot;iAWh&quot;&gt;Убедитесь, что ваш компилятор поддерживает C++20 (флаг -std=c++20).&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h3 id=&quot;rDhC&quot;&gt;&lt;strong&gt;Полный код&lt;/strong&gt;&lt;/h3&gt;
  &lt;pre id=&quot;7FtS&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;ranges&amp;gt;
#include &amp;lt;random&amp;gt;
#include &amp;lt;chrono&amp;gt;
#include &amp;lt;thread&amp;gt;

int main() {
    // Настройки окна
    sf::RenderWindow window(sf::VideoMode(1280, 720), &amp;quot;QuickSort Visualization&amp;quot;);
    window.setFramerateLimit(60);

    // Генерация случайного массива чисел
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution&amp;lt;&amp;gt; dis(10, 700);

    std::vector&amp;lt;int&amp;gt; arr(150);
    for (auto&amp;amp; x : arr) x = dis(gen);

    // Функция быстрой сортировки с визуализацией процесса
    auto quickSort = [&amp;amp;](auto&amp;amp; range, int low, int high, auto&amp;amp; recur) mutable {
        if (low &amp;gt;= high) return; // Базовый случай рекурсии

        int pivot = range[high]; // Выбираем опорный элемент (последний в диапазоне)
        int i = low - 1;

        // Разделение элементов: меньшие идут влево, большие вправо
        for (int j = low; j &amp;lt; high; ++j) {
            if (range[j] &amp;lt;= pivot) {
                ++i;
                std::swap(range[i], range[j]);
            }
        }
        std::swap(range[i + 1], range[high]); // Устанавливаем опорный элемент на своё место
        int pi = i + 1; // Индекс опорного элемента

        // Рекурсивная сортировка левой и правой частей
        recur(range, low, pi - 1, recur);
        recur(range, pi + 1, high, recur);

        // Визуализация текущего состояния массива
        window.clear();
        float barWidth = static_cast&amp;lt;float&amp;gt;(window.getSize().x) / arr.size();
        for (int k = 0; k &amp;lt; arr.size(); ++k) {
            sf::RectangleShape bar(sf::Vector2f(barWidth - 1, arr[k]));
            bar.setPosition(k * barWidth, window.getSize().y - arr[k]);
            bar.setFillColor(sf::Color::White);
            window.draw(bar);
        }
        window.display();
        std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Задержка для анимации
        };

    // Используем std::ranges для удобной работы с массивом
    auto range = std::ranges::subrange(arr.begin(), arr.end());
    bool sorting = true; // Флаг, чтобы сортировка выполнялась только один раз

    // Основной цикл программы
    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) window.close(); // Закрытие окна
        }

        if (sorting) {
            quickSort(range, 0, arr.size() - 1, quickSort); // Запуск сортировки
            sorting = false; // Отключаем повторную сортировку
        }

        // Отрисовка финального состояния массива
        window.clear();
        float barWidth = static_cast&amp;lt;float&amp;gt;(window.getSize().x) / arr.size();
        for (int i = 0; i &amp;lt; arr.size(); ++i) {
            sf::RectangleShape bar(sf::Vector2f(barWidth - 1, arr[i]));
            bar.setPosition(i * barWidth, window.getSize().y - arr[i]);
            bar.setFillColor(sf::Color::White);
            window.draw(bar);
        }
        window.display();
    }

    return 0;
}
&lt;/pre&gt;
  &lt;h3 id=&quot;AHDg&quot;&gt;Пояснения к коду&lt;/h3&gt;
  &lt;p id=&quot;v34i&quot;&gt;&lt;strong&gt;Генерация массива&lt;/strong&gt;: Создаем вектор из 150 случайных чисел от 10 до 700 с помощью std::random_device и std::mt19937.&lt;/p&gt;
  &lt;p id=&quot;Cd12&quot;&gt;&lt;strong&gt;QuickSort&lt;/strong&gt;: Реализован как лямбда-функция с рекурсией. На каждом шаге разбиения массива мы визуализируем текущее состояние.&lt;/p&gt;
  &lt;p id=&quot;3zbT&quot;&gt;&lt;strong&gt;Визуализация&lt;/strong&gt;: Каждый элемент массива отображается как прямоугольник (sf::RectangleShape), высота которого пропорциональна значению элемента.&lt;/p&gt;
  &lt;p id=&quot;nwGk&quot;&gt;&lt;strong&gt;Анимация&lt;/strong&gt;: Задержка в 50 миллисекунд (std::this_thread::sleep_for) между шагами сортировки создает эффект плавной анимации.&lt;/p&gt;
  &lt;h3 id=&quot;PVcA&quot;&gt;Как это работает?&lt;/h3&gt;
  &lt;ol id=&quot;HA0h&quot;&gt;
    &lt;li id=&quot;UQIw&quot;&gt;При запуске программы генерируется случайный массив, который отображается как набор столбцов в окне SFML.&lt;/li&gt;
    &lt;li id=&quot;RqZy&quot;&gt;Алгоритм QuickSort начинает сортировку, и на каждом шаге разбиения массива окно обновляется, показывая текущее состояние.&lt;/li&gt;
    &lt;li id=&quot;z8p5&quot;&gt;После завершения сортировки вы видите отсортированный массив.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;OwG1&quot;&gt;Попробуйте запустить код и понаблюдать за процессом — это действительно увлекательно!&lt;/p&gt;
  &lt;figure id=&quot;M5a4&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/bc/ad/bcadbba7-8495-40ff-8ba2-8e3a0eba132d.gif&quot; width=&quot;1280&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;ntLz&quot;&gt;Расширяемость проекта&lt;/h3&gt;
  &lt;p id=&quot;meql&quot;&gt;Хотите сделать проект еще интереснее? Вот несколько идей:&lt;/p&gt;
  &lt;ul id=&quot;IBcy&quot;&gt;
    &lt;li id=&quot;Y1io&quot;&gt;Добавьте другие алгоритмы сортировки (например, пузырьковую или сортировку слиянием) и сравните их визуально.&lt;/li&gt;
    &lt;li id=&quot;We7e&quot;&gt;Включите интерактивные элементы: кнопки для запуска сортировки или изменения скорости анимации.&lt;/li&gt;
    &lt;li id=&quot;IixR&quot;&gt;Экспериментируйте с цветами: выделяйте опорный элемент (pivot) другим цветом для наглядности.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 id=&quot;mxGh&quot;&gt;Заключение&lt;/h2&gt;
  &lt;p id=&quot;8LtC&quot;&gt;Визуализация алгоритмов — это мощный инструмент для обучения и отладки. С помощью SFML и C++20 вы можете создавать не только полезные, но и красивые образовательные проекты. Этот пример демонстрирует, как комбинировать графику, алгоритмы и современные возможности языка для создания интерактивных приложений.&lt;/p&gt;
  &lt;p id=&quot;xU9M&quot;&gt;Попробуйте улучшить этот проект: добавьте звуковые эффекты, измените стиль визуализации или реализуйте другой алгоритм. Делитесь своими идеями в комментариях — мне будет интересно узнать, что вы придумали!&lt;/p&gt;
  &lt;p id=&quot;yQ2f&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;0EWI&quot;&gt;&lt;a href=&quot;https://t.me/C_Verhovcevo_NVK&quot; target=&quot;_blank&quot;&gt;Телеграмм канал - Программирование игр С++&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>gamedeveloper:BQuEWTLbgaU</id><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper/BQuEWTLbgaU?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><title>CRTP в игровых механиках</title><published>2025-03-02T19:10:20.797Z</published><updated>2025-03-02T19:10:20.797Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/5d/7f/5d7f84df-442f-4a6b-9038-f8164b51f67c.png"></media:thumbnail><category term="prostye-prilozheniya-na-s" label="Простые приложения на С++"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/04/09/040987a7-245e-4c93-9f8a-627113387944.jpeg&quot;&gt;В мире разработки игр часто возникает необходимость создавать гибкие и производительные системы. Одним из мощных инструментов в C++ для достижения этой цели является CRTP — Curiously Recurring Template Pattern. Сегодня мы разберём, как CRTP может помочь в создании игровых механик, и реализуем простой пример с использованием библиотеки SFML.</summary><content type="html">
  &lt;figure id=&quot;xuib&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/04/09/040987a7-245e-4c93-9f8a-627113387944.jpeg&quot; width=&quot;1280&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;nPXr&quot;&gt;В мире разработки игр часто возникает необходимость создавать гибкие и производительные системы. Одним из мощных инструментов в C++ для достижения этой цели является &lt;strong&gt;CRTP&lt;/strong&gt; — Curiously Recurring Template Pattern. Сегодня мы разберём, как CRTP может помочь в создании игровых механик, и реализуем простой пример с использованием библиотеки &lt;strong&gt;SFML&lt;/strong&gt;.&lt;/p&gt;
  &lt;h2 id=&quot;EHrw&quot;&gt;Что такое CRTP?&lt;/h2&gt;
  &lt;p id=&quot;Gsx9&quot;&gt;CRTP — это шаблонный паттерн в C++, где базовый класс является шаблоном, а производный класс передаётся в качестве параметра шаблона самому себе. Это позволяет базовому классу вызывать методы производного класса без использования виртуальных функций, что даёт выигрыш в производительности за счёт статической диспетчеризации.&lt;/p&gt;
  &lt;p id=&quot;1rG6&quot;&gt;Пример структуры CRTP:&lt;/p&gt;
  &lt;pre id=&quot;Vepp&quot; data-lang=&quot;cpp&quot;&gt;template &amp;lt;typename Derived&amp;gt;
class Base {
public:
    void interface() {
        static_cast&amp;lt;Derived*&amp;gt;(this)-&amp;gt;implementation();
    }
};

class Derived : public Base&amp;lt;Derived&amp;gt; {
public:
    void implementation() {
        // Реализация в производном классе
    }
};&lt;/pre&gt;
  &lt;h2 id=&quot;Xt7T&quot;&gt;Зачем использовать CRTP в играх?&lt;/h2&gt;
  &lt;ol id=&quot;cZqj&quot;&gt;
    &lt;li id=&quot;39DS&quot;&gt;&lt;strong&gt;Производительность&lt;/strong&gt;: Виртуальные функции добавляют накладные расходы из-за таблицы vtable. CRTP позволяет обойтись без них.&lt;/li&gt;
    &lt;li id=&quot;mQdC&quot;&gt;&lt;strong&gt;Гибкость&lt;/strong&gt;: Легко добавлять новые типы объектов с общей логикой.&lt;/li&gt;
    &lt;li id=&quot;RVlf&quot;&gt;&lt;strong&gt;Типобезопасность&lt;/strong&gt;: Компилятор проверяет корректность типов на этапе компиляции.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;tK7J&quot;&gt;В игровых механиках это особенно полезно для систем вроде управления объектами (игроки, враги, снаряды), где нужно минимизировать задержки и упростить расширение.&lt;/p&gt;
  &lt;h2 id=&quot;yMts&quot;&gt;Cистема игровых объектов с SFML&lt;/h2&gt;
  &lt;p id=&quot;VmvT&quot;&gt;Давайте создадим простую систему игровых сущностей с использованием CRTP и SFML. Мы реализуем базовый класс GameObject, от которого будут наследоваться Player и Enemy. Каждый объект будет обновляться и отрисовываться через SFML.&lt;/p&gt;
  &lt;p id=&quot;TCOW&quot;&gt;Перед началом убедитесь, что у вас установлена библиотека SFML. &lt;/p&gt;
  &lt;p id=&quot;rX1X&quot;&gt;Минимальные зависимости:&lt;/p&gt;
  &lt;ul id=&quot;fEho&quot;&gt;
    &lt;li id=&quot;XAjG&quot;&gt;sfml-graphics&lt;/li&gt;
    &lt;li id=&quot;Pyxz&quot;&gt;sfml-window&lt;/li&gt;
    &lt;li id=&quot;jXWB&quot;&gt;sfml-system&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;u5ni&quot;&gt;Создадим абстрактный базовый класс IGameObject и шаблонный класс с CRTP для реализации общей логики игровых объектов GameObject.&lt;/p&gt;
  &lt;pre id=&quot;7Vjh&quot; data-lang=&quot;cpp&quot;&gt;#pragma once
#include &amp;lt;SFML/Graphics.hpp&amp;gt;

// Абстрактный базовый класс для всех игровых объектов
class IGameObject {
public:
    // Виртуальный деструктор для корректного удаления объектов
    virtual ~IGameObject() = default; 
    // Чисто виртуальный метод для обновления состояния объекта
    virtual void update(float deltaTime) = 0; 
    // Чисто виртуальный метод для отрисовки объекта
    virtual void draw(sf::RenderWindow&amp;amp; window) = 0; 
    // Чисто виртуальный метод для получения позиции объекта
    virtual sf::Vector2f getPosition() const = 0; 
};&lt;/pre&gt;
  &lt;pre id=&quot;EIiV&quot; data-lang=&quot;cpp&quot;&gt;#pragma once

#include &amp;lt;SFML/Graphics.hpp&amp;gt;
// Шаблонный класс с CRTP для реализации общей логики игровых объектов
template &amp;lt;typename Derived&amp;gt;
class GameObject : public IGameObject {
public:
    // Переопределяем метод update, вызывая реализацию из производного класса
    void update(float deltaTime) override {
        static_cast&amp;lt;Derived*&amp;gt;(this)-&amp;gt;updateImpl(deltaTime);
    }

    // Переопределяем метод draw, вызывая реализацию из производного класса
    void draw(sf::RenderWindow&amp;amp; window) override {
        static_cast&amp;lt;Derived*&amp;gt;(this)-&amp;gt;drawImpl(window);
    }

    // Переопределяем метод getPosition, вызывая реализацию из производного класса
    sf::Vector2f getPosition() const override {
        return static_cast&amp;lt;const Derived*&amp;gt;(this)-&amp;gt;getPositionImpl();
    }
};&lt;/pre&gt;
  &lt;p id=&quot;wwgM&quot;&gt;В этом коде:&lt;/p&gt;
  &lt;ul id=&quot;JPDW&quot;&gt;
    &lt;li id=&quot;hWDS&quot;&gt;update обновляет состояние объекта.&lt;/li&gt;
    &lt;li id=&quot;cibD&quot;&gt;draw отвечает за отрисовку.&lt;/li&gt;
    &lt;li id=&quot;Irgz&quot;&gt;getPosition возвращает позицию объекта.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;JZ2I&quot;&gt;Методы вызывают соответствующие реализации в производных классах через static_cast.&lt;/p&gt;
  &lt;p id=&quot;44kx&quot;&gt;Теперь определим два игровых объекта: Player (игрок) и Enemy (враг).&lt;/p&gt;
  &lt;pre id=&quot;qhkK&quot; data-lang=&quot;cpp&quot;&gt;#pragma once
#include &amp;quot;GameObject.h&amp;quot;
// Класс игрока, наследующийся от GameObject с использованием CRTP
class Player : public GameObject&amp;lt;Player&amp;gt; {
private:
    // Графический объект — круг для представления игрока
    sf::CircleShape shape; 
    // Позиция игрока на экране
    sf::Vector2f position = sf::Vector2f(200.0f, 200.0f); 
    // Скорость перемещения игрока
    float speed = 200.0f;  

public:
    // Конструктор игрока
    Player() { 
    // Устанавливаем радиус круга
        shape.setRadius(20.0f); 
    // Задаём зелёный цвет
        shape.setFillColor(sf::Color::Green);
    // Устанавливаем позицию круга
        shape.setPosition(position);     
    }

    // Реализация обновления состояния игрока
    void updateImpl(float deltaTime) {
        // Движение влево
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::A)) position.x -= speed * deltaTime; 
        // Движение вправо
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::D)) position.x += speed * deltaTime; 
        // Движение вверх
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::W)) position.y -= speed * deltaTime;
        // Движение вниз
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::S)) position.y += speed * deltaTime; 
        // Обновляем позицию графического объекта
        shape.setPosition(position); 
    }

    // Реализация отрисовки игрока
    void drawImpl(sf::RenderWindow&amp;amp; window) const {
    // Отрисовываем круг в окне
        window.draw(shape); 
    }

    // Реализация получения позиции игрока
    sf::Vector2f getPositionImpl() const {
    // Возвращаем текущую позицию
        return position; 
    }
};&lt;/pre&gt;
  &lt;pre id=&quot;M80H&quot; data-lang=&quot;cpp&quot;&gt;#pragma once  
#include &amp;quot;GameObject.h&amp;quot;  
// Класс врага, наследующийся от GameObject с использованием CRTP  
class Enemy : public GameObject&amp;lt;Enemy&amp;gt; {  
private:  
// Графический объект — прямоугольник для врага
    sf::RectangleShape shape;
// Позиция врага на экране
   sf::Vector2f position = {400.0f, 300.0f}; 
// Скорость перемещения врага  
   float speed = 100.0f;     

public:
// Конструктор врага
   Enemy() { 
// Устанавливаем размер прямоугольника 
       shape.setSize({ 30.0f, 30.0f });
// Задаём красный цвет 
       shape.setFillColor(sf::Color::Red); 
// Устанавливаем позицию прямоугольника
       shape.setPosition(position);           
   }  

   // Реализация обновления состояния врага  
   void updateImpl(float deltaTime) {  
// Движение врага по горизонтали  
       position.x += speed * deltaTime;
// Разворот при достижении границ  
       if (position.x &amp;gt; 700 || position.x &amp;lt; 100) speed = -speed; 
// Обновляем позицию графического объекта 
       shape.setPosition(position);  
   }  

   // Реализация отрисовки врага  
   void drawImpl(sf::RenderWindow&amp;amp; window) const { 
// Отрисовываем прямоугольник в окне
       window.draw(shape);   
   }  

   // Реализация получения позиции врага  
   sf::Vector2f getPositionImpl() const { 
// Возвращаем текущую позицию 
       return position;  
   }  
};&lt;/pre&gt;
  &lt;p id=&quot;l5XL&quot;&gt;Теперь соберём всё вместе в главном файле.&lt;/p&gt;
  &lt;pre id=&quot;7zAx&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;SFML/Graphics.hpp&amp;gt; 
#include &amp;lt;vector&amp;gt;            
#include &amp;lt;memory&amp;gt;            
#include &amp;quot;IGameObject.h&amp;quot;     
#include &amp;quot;GameObject.h&amp;quot;      
#include &amp;quot;Player.h&amp;quot;          
#include &amp;quot;Enemy.h&amp;quot;           

// Основная функция программы
int main() {
// Создаём окно 800x600
    sf::RenderWindow window(sf::VideoMode(800, 600), &amp;quot;CRTP Game Example&amp;quot;); 
// Ограничиваем частоту кадров до 60 FPS
    window.setFramerateLimit(60); 

    // Вектор умных указателей для хранения всех игровых объектов
    std::vector&amp;lt;std::unique_ptr&amp;lt;IGameObject&amp;gt;&amp;gt; objects;
// Добавляем игрока в вектор
    objects.push_back(std::make_unique&amp;lt;Player&amp;gt;()); 
// Добавляем врага в вектор
    objects.push_back(std::make_unique&amp;lt;Enemy&amp;gt;());  
// Часы для расчёта времени между кадрами
    sf::Clock clock; 
// Главный игровой цикл
    while (window.isOpen()) { 
        sf::Event event;
// Обрабатываем события
        while (window.pollEvent(event)) { 
// Закрываем окно при нажатии на крестик
            if (event.type == sf::Event::Closed) window.close(); 
        }
// Вычисляем время с последнего кадра
        float deltaTime = clock.restart().asSeconds(); 

        // Обновляем состояние всех объектов
        for (auto const&amp;amp; obj : objects) {
            obj-&amp;gt;update(deltaTime);
        }
// Очищаем окно перед отрисовкой
        window.clear(); 
        // Отрисовываем все объекты
        for (auto const&amp;amp; obj : objects) {
            obj-&amp;gt;draw(window);
        }
        // Отображаем нарисованное на экране
        window.display(); 
    }

    return 0; 
}&lt;/pre&gt;
  &lt;h3 id=&quot;exkd&quot;&gt;Как это работает?&lt;/h3&gt;
  &lt;p id=&quot;K5J3&quot;&gt;&lt;strong&gt;Player&lt;/strong&gt; управляется клавишами W, A, S, D и отображается как зелёный круг.&lt;/p&gt;
  &lt;p id=&quot;7iOZ&quot;&gt;&lt;strong&gt;Enemy&lt;/strong&gt; движется автоматически влево-вправо и отображается как красный квадрат.&lt;/p&gt;
  &lt;p id=&quot;uupv&quot;&gt;Базовый класс GameObject через CRTP вызывает методы конкретных классов (Player или Enemy) без виртуальных функций.&lt;/p&gt;
  &lt;h3 id=&quot;aua5&quot;&gt;Преимущества подхода&lt;/h3&gt;
  &lt;p id=&quot;1vSf&quot;&gt;&lt;strong&gt;Скорость&lt;/strong&gt;: Отсутствие &lt;strong&gt;vtable&lt;/strong&gt; ускоряет вызовы методов.&lt;/p&gt;
  &lt;p id=&quot;XGt4&quot;&gt;&lt;strong&gt;Расширяемость&lt;/strong&gt;: Чтобы добавить новый тип объекта (например, Projectile), достаточно унаследовать его от GameObject&amp;lt;Projectile&amp;gt; и реализовать нужные методы.&lt;/p&gt;
  &lt;p id=&quot;t9Gw&quot;&gt;&lt;strong&gt;Простота&lt;/strong&gt;: Логика обновления и отрисовки сосредоточена в одном месте.&lt;/p&gt;
  &lt;h3 id=&quot;GNGR&quot;&gt;Итог&lt;/h3&gt;
  &lt;p id=&quot;ypoH&quot;&gt;CRTP — это мощный инструмент для игровых движков, где важны производительность и гибкость. В примере с SFML мы создали систему объектов, которую легко расширить без потери скорости. Попробуйте добавить новые сущности вроде снарядов или бонусов — это займёт всего несколько строк кода!&lt;/p&gt;
  &lt;p id=&quot;ZPiB&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;0Y2C&quot;&gt;&lt;strong&gt;vtable&lt;/strong&gt; (от англ. &lt;em&gt;virtual table&lt;/em&gt;) — это механизм в C++, который используется для реализации &lt;strong&gt;динамического полиморфизма&lt;/strong&gt; через виртуальные функции. Когда вы объявляете функцию в базовом классе как virtual, компилятор создаёт специальную таблицу указателей (vtable) для каждого класса, который содержит виртуальные функции. Эта таблица хранит адреса реализаций виртуальных методов для конкретного класса.&lt;/p&gt;
  &lt;p id=&quot;5Xwa&quot;&gt;&lt;a href=&quot;https://t.me/C_Verhovcevo_NVK&quot; target=&quot;_blank&quot;&gt;Телеграмм канал - Программирование игр С+&lt;/a&gt;+&lt;/p&gt;

</content></entry><entry><id>gamedeveloper:R118GOjqWi_</id><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper/R118GOjqWi_?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><title>Разработка игр на C++</title><published>2025-02-26T22:33:55.612Z</published><updated>2025-02-26T22:33:55.612Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img1.teletype.in/files/05/6e/056efce9-d723-4ee4-a613-df0d57956f11.png"></media:thumbnail><category term="prostye-prilozheniya-na-s" label="Простые приложения на С++"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/4a/f2/4af24d53-0567-494c-a0b1-23419c0abce3.jpeg&quot;&gt;Разработка игр на C++ — это увлекательный и сложный процесс, который требует понимания как самого языка программирования, так и основ игрового дизайна, математики и компьютерной графики. C++ является одним из самых популярных языков для создания игр благодаря своей производительности и гибкости. В этом введении мы рассмотрим основные этапы и концепции, которые помогут вам начать путь в разработке игр на C++.</summary><content type="html">
  &lt;figure id=&quot;67Df&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/4a/f2/4af24d53-0567-494c-a0b1-23419c0abce3.jpeg&quot; width=&quot;1344&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;m8V5&quot;&gt;Разработка игр на C++ — это увлекательный и сложный процесс, который требует понимания как самого языка программирования, так и основ игрового дизайна, математики и компьютерной графики. C++ является одним из самых популярных языков для создания игр благодаря своей производительности и гибкости. В этом введении мы рассмотрим основные этапы и концепции, которые помогут вам начать путь в разработке игр на C++.&lt;/p&gt;
  &lt;h3 id=&quot;bXG3&quot;&gt;1. &lt;strong&gt;Основы C++&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;sHXZ&quot;&gt;Прежде чем приступать к созданию игр, важно освоить базовые концепции языка C++:&lt;/p&gt;
  &lt;p id=&quot;C7u8&quot;&gt;&lt;strong&gt;Типы данных и переменные&lt;/strong&gt;: &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;, &lt;code&gt;char&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt; и т.д.&lt;/p&gt;
  &lt;p id=&quot;uBEU&quot;&gt;&lt;strong&gt;Управляющие конструкции&lt;/strong&gt;: &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;else&lt;/code&gt;, &lt;code&gt;switch&lt;/code&gt;, циклы (&lt;code&gt;for&lt;/code&gt;, &lt;code&gt;while&lt;/code&gt;, &lt;code&gt;do-while&lt;/code&gt;).&lt;/p&gt;
  &lt;p id=&quot;8i6T&quot;&gt;&lt;strong&gt;Функции&lt;/strong&gt;: создание и использование функций, передача параметров, возврат значений.&lt;/p&gt;
  &lt;p id=&quot;7Vjw&quot;&gt;&lt;strong&gt;Классы и объекты&lt;/strong&gt;: основы объектно-ориентированного программирования (ООП).&lt;/p&gt;
  &lt;p id=&quot;370j&quot;&gt;&lt;strong&gt;Указатели и ссылки&lt;/strong&gt;: работа с динамической памятью.&lt;/p&gt;
  &lt;p id=&quot;JBWm&quot;&gt;&lt;strong&gt;STL (Standard Template Library)&lt;/strong&gt;: использование контейнеров (&lt;code&gt;vector&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;set&lt;/code&gt;) и алгоритмов.&lt;/p&gt;
  &lt;h3 id=&quot;QZgP&quot;&gt;2. &lt;strong&gt;Основы разработки игр&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;9NB2&quot;&gt;Разработка игр включает в себя несколько ключевых аспектов:&lt;/p&gt;
  &lt;p id=&quot;qixH&quot;&gt;&lt;strong&gt;Игровой цикл&lt;/strong&gt;: основной цикл игры, который обновляет состояние игры и отрисовывает кадры.&lt;/p&gt;
  &lt;p id=&quot;dX6V&quot;&gt;&lt;strong&gt;Графика&lt;/strong&gt;: работа с 2D и 3D графикой, использование библиотек для рендеринга.&lt;/p&gt;
  &lt;p id=&quot;FMyO&quot;&gt;&lt;strong&gt;Физика&lt;/strong&gt;: реализация физических взаимодействий (гравитация, столкновения и т.д.).&lt;/p&gt;
  &lt;p id=&quot;GawS&quot;&gt;&lt;strong&gt;Звук&lt;/strong&gt;: добавление звуковых эффектов и музыки.&lt;/p&gt;
  &lt;p id=&quot;oiSp&quot;&gt;&lt;strong&gt;Управление&lt;/strong&gt;: обработка ввода от пользователя (клавиатура, мышь, геймпад).&lt;/p&gt;
  &lt;p id=&quot;BppI&quot;&gt;&lt;strong&gt;Искусственный интеллект (ИИ)&lt;/strong&gt;: создание поведения для NPC (неигровых персонажей).&lt;/p&gt;
  &lt;h3 id=&quot;wgdG&quot;&gt;3. &lt;strong&gt;Библиотеки и фреймворки для разработки игр&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;aL1X&quot;&gt;Для упрощения разработки игр на C++ используются различные библиотеки и фреймворки. Вот некоторые из них:&lt;/p&gt;
  &lt;p id=&quot;brri&quot;&gt;&lt;strong&gt;SFML (Simple and Fast Multimedia Library)&lt;/strong&gt;: библиотека для работы с графикой, звуком и вводом. Отлично подходит для 2D-игр.&lt;/p&gt;
  &lt;p id=&quot;mNSj&quot;&gt;&lt;strong&gt;SDL (Simple DirectMedia Layer)&lt;/strong&gt;: кросс-платформенная библиотека для работы с графикой, звуком и вводом.&lt;/p&gt;
  &lt;p id=&quot;IQxM&quot;&gt;&lt;strong&gt;OpenGL&lt;/strong&gt;: низкоуровневая библиотека для работы с 3D-графикой.&lt;/p&gt;
  &lt;p id=&quot;6i0t&quot;&gt;&lt;strong&gt;Unreal Engine&lt;/strong&gt;: мощный игровой движок, который использует C++ для написания логики игр.&lt;/p&gt;
  &lt;p id=&quot;xzWm&quot;&gt;&lt;strong&gt;Unity (с использованием C++ через плагины)&lt;/strong&gt;: популярный игровой движок, который поддерживает C++ для расширения функциональности.&lt;/p&gt;
  &lt;p id=&quot;vbuV&quot;&gt;&lt;strong&gt;Godot&lt;/strong&gt;: открытый игровой движок, который также поддерживает C++.&lt;/p&gt;
  &lt;h3 id=&quot;0Awn&quot;&gt;4. &lt;strong&gt;Создание простой игры&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;s0E9&quot;&gt;Давайте рассмотрим основные шаги для создания простой 2D-игры на C++ с использованием SFML:&lt;/p&gt;
  &lt;h4 id=&quot;6Wzp&quot;&gt;Шаг 1: Установка SFML&lt;/h4&gt;
  &lt;ul id=&quot;31g2&quot;&gt;
    &lt;li id=&quot;t28K&quot;&gt;Скачайте и установите SFML с официального сайта: &lt;a href=&quot;https://www.sfml-dev.org/&quot; target=&quot;_blank&quot;&gt;https://www.sfml-dev.org/&lt;/a&gt;.&lt;/li&gt;
    &lt;li id=&quot;MfA3&quot;&gt;Настройте проект в вашей IDE (например, Visual Studio, Code::Blocks или CLion).&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h4 id=&quot;ecOC&quot;&gt;Шаг 2: Создание окна игры&lt;/h4&gt;
  &lt;pre id=&quot;uNqy&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;SFML/Graphics.hpp&amp;gt;

int main() {
    sf::RenderWindow window(sf::VideoMode(800, 600), &amp;quot;My Game&amp;quot;);

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed)
                window.close();
        }

        window.clear();
        // Здесь будет отрисовка объектов
        window.display();
    }

    return 0;
}&lt;/pre&gt;
  &lt;h4 id=&quot;W2lH&quot;&gt;Шаг 3: Добавление спрайта&lt;/h4&gt;
  &lt;pre id=&quot;mYB0&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;SFML/Graphics.hpp&amp;gt;

int main() {
    sf::RenderWindow window(sf::VideoMode(800, 600), &amp;quot;My Game&amp;quot;);

    // Загрузка текстуры и создание спрайта
    sf::Texture texture;
    if (!texture.loadFromFile(&amp;quot;player.png&amp;quot;)) {
        return -1;
    }
    sf::Sprite player(texture);
    player.setPosition(400, 300);

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed)
                window.close();
        }

        window.clear();
        window.draw(player); // Отрисовка спрайта
        window.display();
    }

    return 0;
}&lt;/pre&gt;
  &lt;h4 id=&quot;Scdp&quot;&gt;Шаг 4: Обработка ввода&lt;/h4&gt;
  &lt;pre id=&quot;HkIS&quot; data-lang=&quot;cpp&quot;&gt;if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) {
    player.move(-0.1f, 0.0f);
}
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) {
    player.move(0.1f, 0.0f);
}&lt;/pre&gt;
  &lt;h3 id=&quot;eLN4&quot;&gt;Заключение&lt;/h3&gt;
  &lt;p id=&quot;Wznm&quot;&gt;Разработка игр на C++ — это сложный, но очень интересный процесс. Начните с простых проектов, постепенно углубляя свои знания в области графики, физики и ИИ. Используйте доступные библиотеки и движки, чтобы ускорить разработку. Удачи в создании ваших игр!&lt;/p&gt;
  &lt;p id=&quot;WGIi&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;42tf&quot;&gt;&lt;a href=&quot;https://t.me/C_Verhovcevo_NVK&quot; target=&quot;_blank&quot;&gt;Телеграмм канал - Программирование игр С++&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>gamedeveloper:dgq4fXaTw3O</id><link rel="alternate" type="text/html" href="https://teletype.in/@gamedeveloper/dgq4fXaTw3O?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gamedeveloper"></link><title>Построение минимального дерева Штейнера в евклидовой плоскости</title><published>2025-02-02T17:38:43.736Z</published><updated>2025-02-02T17:38:43.736Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/5a/3c/5a3cbc99-28f1-43de-8f02-25c49361d72f.png"></media:thumbnail><category term="prostye-prilozheniya-na-s" label="Простые приложения на С++"></category><summary type="html">&lt;img src=&quot;https://img3.teletype.in/files/20/0f/200f6354-24e5-4fd0-989d-c7e695637521.jpeg&quot;&gt;Даны три точки в евклидовой плоскости с координатами A(x1,y1), B(x2,y2) и C(x3​,y3​). Необходимо построить минимальное дерево, соединяющее эти точки с минимальной общей длиной рёбер.</summary><content type="html">
  &lt;figure id=&quot;9KuY&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/20/0f/200f6354-24e5-4fd0-989d-c7e695637521.jpeg&quot; width=&quot;1280&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;o5Jt&quot;&gt;&lt;strong&gt;Условие:&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;s17Y&quot;&gt;Даны три точки в евклидовой плоскости с координатами A(x1,y1), B(x2,y2) и C(x3​,y3​). Необходимо построить минимальное дерево, соединяющее эти точки с минимальной общей длиной рёбер.&lt;/p&gt;
  &lt;p id=&quot;Iiss&quot;&gt;Разрешается добавление одной дополнительной точки S(xs,ys)(точки Штейнера), которая может уменьшить суммарную длину соединяющих отрезков.&lt;/p&gt;
  &lt;h3 id=&quot;aVkp&quot;&gt;&lt;strong&gt;Требования:&lt;/strong&gt;&lt;/h3&gt;
  &lt;ol id=&quot;41qu&quot;&gt;
    &lt;li id=&quot;AQqv&quot;&gt;Найти координаты точки S, которая минимизирует сумму расстояний от неё до заданных точек.&lt;/li&gt;
    &lt;li id=&quot;AawZ&quot;&gt;Визуализировать полученное дерево с помощью библиотеки &lt;strong&gt;SFML&lt;/strong&gt;.&lt;/li&gt;
    &lt;li id=&quot;55V0&quot;&gt;Вывести терминальные точки (красным), точку Штейнера (синим) и соединяющие рёбра (жёлтым).&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h3 id=&quot;BHTP&quot;&gt;&lt;strong&gt;Формат входных данных:&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;9mHw&quot;&gt;На вход подаются три пары вещественных чисел (x1,y1), (x2,y2), (x3,y3) — координаты заданных точек.&lt;/p&gt;
  &lt;h3 id=&quot;1RXX&quot;&gt;&lt;strong&gt;Формат выходных данных:&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;PrNW&quot;&gt;Программа должна:&lt;/p&gt;
  &lt;ul id=&quot;ZMjt&quot;&gt;
    &lt;li id=&quot;rDFp&quot;&gt;Вычислить координаты точки Штейнера S(xs,ys).&lt;/li&gt;
    &lt;li id=&quot;6qAD&quot;&gt;Вывести общую длину построенного дерева.&lt;/li&gt;
    &lt;li id=&quot;6TUX&quot;&gt;Открыть графическое окно с визуализацией результата.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h3 id=&quot;MaWR&quot;&gt;&lt;strong&gt;Пример работы программы:&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;3ZEL&quot;&gt;Входные данные:&lt;/p&gt;
  &lt;figure id=&quot;QCvz&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/0e/cd/0ecdd76b-a764-4cdd-b660-49681b48e5b9.png&quot; width=&quot;730&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;JkEX&quot;&gt;Выходные данные:&lt;/p&gt;
  &lt;figure id=&quot;3aNE&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/2b/18/2b180899-767e-4b6c-9f66-3e4277135995.png&quot; width=&quot;734&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;15v6&quot;&gt;&lt;strong&gt;Порядок решения задачи о минимальном дереве Штейнера&lt;/strong&gt;&lt;/h3&gt;
  &lt;h3 id=&quot;aOO0&quot;&gt;&lt;strong&gt;Шаг 1: Определение входных данных&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;hzC4&quot;&gt;Заданы три точки A(x1,y1), B(x2,y2) и C(x3,y3) в евклидовой плоскости. Нужно найти &lt;strong&gt;минимальное дерево&lt;/strong&gt;, соединяющее их, с возможностью добавления &lt;strong&gt;одной точки Штейнера&lt;/strong&gt; S(xs,ys), которая минимизирует сумму расстояний.&lt;/p&gt;
  &lt;h3 id=&quot;KtPV&quot;&gt;&lt;strong&gt;Шаг 2: Поиск точки Штейнера&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;PK2k&quot;&gt;Метод &lt;strong&gt;градиентного спуска&lt;/strong&gt; применяется для нахождения оптимальной точки S.&lt;/p&gt;
  &lt;ol id=&quot;62Kj&quot;&gt;
    &lt;li id=&quot;jW30&quot;&gt;&lt;strong&gt;Начальная точка&lt;/strong&gt;&lt;/li&gt;
    &lt;ul id=&quot;n6aJ&quot;&gt;
      &lt;li id=&quot;niD9&quot;&gt;Используем &lt;strong&gt;центр масс&lt;/strong&gt; терминальных точек в качестве начальной точки&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/ol&gt;
  &lt;figure id=&quot;COq3&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/36/50/36508e09-c1fd-4230-9be5-5bce65042eb6.png&quot; width=&quot;506&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;zR2K&quot;&gt;    2.&lt;strong&gt; Минимизация суммы расстояний&lt;/strong&gt;&lt;/p&gt;
  &lt;ul id=&quot;yRcq&quot;&gt;
    &lt;li id=&quot;vFly&quot;&gt;Вычисляем сумму расстояний от текущего положения S до всех терминалов:&lt;/li&gt;
  &lt;/ul&gt;
  &lt;figure id=&quot;3EdF&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/48/7c/487ce0e7-302a-4c0a-86aa-50f893a6d77b.png&quot; width=&quot;448&quot; /&gt;
  &lt;/figure&gt;
  &lt;ul id=&quot;muMh&quot;&gt;
    &lt;li id=&quot;Vkt0&quot;&gt;Двигаемся в направлениях &lt;strong&gt;вверх, вниз, влево и вправо&lt;/strong&gt; с некоторым шагом Δ, уменьшая его, когда не находим лучшего положения. &lt;/li&gt;
    &lt;li id=&quot;jMI4&quot;&gt;Останавливаемся, когда шаг становится меньше заданной точности.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h3 id=&quot;UfbA&quot;&gt;&lt;strong&gt;Шаг 3: Визуализация графа (SFML)&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;41S1&quot;&gt;После нахождения S(xs,ys), строим дерево:&lt;/p&gt;
  &lt;ol id=&quot;qDJD&quot;&gt;
    &lt;li id=&quot;sPzc&quot;&gt;&lt;strong&gt;Рисуем точки:&lt;/strong&gt;&lt;/li&gt;
    &lt;ul id=&quot;QbmI&quot;&gt;
      &lt;li id=&quot;aOsO&quot;&gt;&lt;strong&gt;Красные круги&lt;/strong&gt; для терминальных точек A,B,C.&lt;/li&gt;
      &lt;li id=&quot;h6t5&quot;&gt;&lt;strong&gt;Синяя точка&lt;/strong&gt; для найденной точки Штейнера.&lt;/li&gt;
    &lt;/ul&gt;
    &lt;li id=&quot;G35G&quot;&gt;&lt;strong&gt;Рисуем рёбра:&lt;/strong&gt;&lt;/li&gt;
    &lt;ul id=&quot;ADY3&quot;&gt;
      &lt;li id=&quot;kVYb&quot;&gt;&lt;strong&gt;Жёлтые линии&lt;/strong&gt; от точки S к каждой из терминальных точек.&lt;/li&gt;
    &lt;/ul&gt;
    &lt;li id=&quot;20kU&quot;&gt;&lt;strong&gt;Отображение в окне SFML&lt;/strong&gt;&lt;/li&gt;
    &lt;ul id=&quot;1v2k&quot;&gt;
      &lt;li id=&quot;qX2I&quot;&gt;Создаём окно SFML.&lt;/li&gt;
      &lt;li id=&quot;pZYi&quot;&gt;Отображаем точки и соединяющие линии.&lt;/li&gt;
      &lt;li id=&quot;zxUB&quot;&gt;Обновляем экран.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/ol&gt;
  &lt;h3 id=&quot;NXeI&quot;&gt;&lt;strong&gt;Шаг 4: Вывод результатов&lt;/strong&gt;&lt;/h3&gt;
  &lt;ol id=&quot;X0wI&quot;&gt;
    &lt;li id=&quot;IivD&quot;&gt;Вывод координат точки Штейнера&lt;/li&gt;
    &lt;li id=&quot;vtkm&quot;&gt;Вывод длинны минимального дерева&lt;/li&gt;
    &lt;li id=&quot;IQH4&quot;&gt;Графическое представление : &lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;tKei&quot;&gt;- Красные точки — исходные точки&lt;/p&gt;
  &lt;p id=&quot;IlzL&quot;&gt;- Синяя точка — точка Штейнера&lt;/p&gt;
  &lt;p id=&quot;XxSK&quot;&gt;- Жёлтые линии — минимальное дерево&lt;/p&gt;
  &lt;h3 id=&quot;GeKO&quot;&gt;Код программы&lt;/h3&gt;
  &lt;pre id=&quot;e1r0&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;SFML/Graphics.hpp&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cmath&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;limits&amp;gt;

using namespace std;
using namespace sf;

struct Point {
    double x, y;
};

// Вычисление расстояния между двумя точками
double distance(Point a, Point b) {
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

// Сумма расстояний от точки p до всех остальных
double totalDistance(Point p, const vector&amp;lt;Point&amp;gt;&amp;amp; terminals) {
    double sum = 0;
    for (const auto&amp;amp; t : terminals) {
        sum += distance(p, t);
    }
    return sum;
}

// Метод градиентного спуска для поиска точки Штейнера
Point steinerPoint(const vector&amp;lt;Point&amp;gt;&amp;amp; terminals) {
    double step = 0.1;
    double precision = 1e-6;
    Point steiner = { 0, 0 };

    for (const auto&amp;amp; t : terminals) {
        steiner.x += t.x;
        steiner.y += t.y;
    }
    steiner.x /= terminals.size();
    steiner.y /= terminals.size();

    double prevDist = totalDistance(steiner, terminals);

    while (step &amp;gt; precision) {
        bool improved = false;

        for (double dx : {-step, step}) {
            for (double dy : {-step, step}) {
                Point newPoint = { steiner.x + dx, steiner.y + dy };
                double newDist = totalDistance(newPoint, terminals);

                if (newDist &amp;lt; prevDist) {
                    steiner = newPoint;
                    prevDist = newDist;
                    improved = true;
                }
            }
        }

        if (!improved) {
            step /= 2;
        }
    }

    return steiner;
}

// Основная функция
int main() {
    // Размер окна
    const int WIDTH = 600, HEIGHT = 600;

    // Исходные точки
    vector&amp;lt;Point&amp;gt; terminals = { {100, 500}, {500, 500}, {300, 100}};

    // Найдём точку Штейнера
    Point s = steinerPoint(terminals);

    // Создание окна SFML
    RenderWindow window(VideoMode(WIDTH, HEIGHT), &amp;quot;Steiner Tree Visualization&amp;quot;);

    while (window.isOpen()) {
        Event event;
        while (window.pollEvent(event)) {
            if (event.type == Event::Closed)
                window.close();
        }

        // Очистка экрана
        window.clear(Color::Black);

        // Рисуем рёбра (жёлтые линии)
        for (const auto&amp;amp; t : terminals) {
            Vertex line[] = {
                Vertex(Vector2f(s.x, s.y), Color::Yellow),
                Vertex(Vector2f(t.x, t.y), Color::Yellow)
            };
            window.draw(line, 2, Lines);
        }

        // Рисуем терминальные точки (красные)
        for (const auto&amp;amp; t : terminals) {
            CircleShape terminal(5);
            terminal.setPosition(t.x - 5, t.y - 5);
            terminal.setFillColor(Color::Red);
            window.draw(terminal);
        }

        // Рисуем точку Штейнера (синяя)
        CircleShape steiner(5);
        steiner.setPosition(s.x - 5, s.y - 5);
        steiner.setFillColor(Color::Blue);
        window.draw(steiner);

        // Отображение кадра
        window.display();
    }

    return 0;
}&lt;/pre&gt;
  &lt;p id=&quot;ggSv&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;PzBh&quot;&gt;&lt;strong&gt;Итог&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;NRFH&quot;&gt;Метод использует &lt;strong&gt;градиентный спуск&lt;/strong&gt; для поиска точки Штейнера и &lt;strong&gt;SFML&lt;/strong&gt; для визуализации. Это позволяет эффективно решать задачу и демонстрировать результат в графическом окне.&lt;/p&gt;
  &lt;p id=&quot;Wk7u&quot;&gt;&lt;a href=&quot;https://t.me/C_Verhovcevo_NVK&quot; target=&quot;_blank&quot;&gt;Телеграмм канал - Программирование игр С++/С#&lt;/a&gt;&lt;/p&gt;

</content></entry></feed>