Архитектура C++ проектов: Создание редактора уровней на Raylib

Курс посвящен правильной организации кода и файловой структуры в C++ проектах. Вы научитесь разделять логику и представление, настраивать CMake и применять паттерны проектирования для создания масштабируемых инструментов разработки игр.

1. Основы организации: файловая структура, CMake и разделение на модули

Основы организации: файловая структура, CMake и разделение на модули

Добро пожаловать в курс «Архитектура C++ проектов: Создание редактора уровней на Raylib». Вы здесь, потому что хотите перестать писать «спагетти-код» и научиться создавать инструменты, которые не стыдно показать коллегам и удобно поддерживать самому.

Многие начинающие разработчики сталкиваются с одной и той же проблемой: проект начинается с одного файла main.cpp. Затем появляется player.cpp, потом enemy.cpp, и внезапно папка проекта превращается в свалку из десятков файлов, в которых невозможно ориентироваться. Когда вы пытаетесь написать редактор уровней — инструмент сложный и требующий строгой логики — такой хаос становится фатальным.

В этой статье мы заложим фундамент вашего будущего редактора. Мы не напишем ни строчки игровой логики сегодня, но мы сделаем кое-что более важное: создадим профессиональный каркас проекта.

Почему структура важна?

Представьте, что вы строите дом. Если вы свалите кирпичи, доски и цемент в одну кучу, дом построить не получится. В программировании то же самое. Хорошая структура проекта обеспечивает:

  • Масштабируемость: Добавление новых фич не ломает старые.
  • Читаемость: Вы (и другие) знаете, где искать конкретный код.
  • Переносимость: Проект легко собрать на Windows, Linux или macOS.
  • Стандартная файловая структура C++ проекта

    В индустрии сложился определенный стандарт организации папок. Мы будем придерживаться современной структуры, разделяющей исходный код, заголовки и сторонние библиотеки.

    !Структура папок нашего будущего проекта

    Разберем назначение каждой директории:

    * src/ (Source): Здесь лежат файлы реализации с расширением .cpp. Это «мясо» вашего проекта. * include/ (Include): Здесь хранятся заголовочные файлы .h или .hpp. Это публичный интерфейс ваших модулей. * external/ или libs/: Место для сторонних библиотек (в нашем случае здесь будет жить Raylib). * assets/: Ресурсы игры — текстуры, звуки, шрифты и файлы конфигурации. * build/: Папка для артефактов сборки. Мы никогда не коммитим её в git, так как она содержит временные файлы компилятора.

    > Хорошая архитектура — это не когда нечего добавить, а когда нечего убрать, и при этом всё лежит на своих местах.

    Система сборки: Почему CMake?

    Вы упомянули, что хотите «нормальную структуру». В мире C++ это практически синоним использования CMake. Это мета-система сборки, которая генерирует инструкции для нативной системы сборки вашей ОС (Makefile для Linux, Solution для Visual Studio, Project для Xcode).

    Базовый CMakeLists.txt

    В корне вашего проекта всегда должен лежать файл CMakeLists.txt. Вот минимальный шаблон для нашего редактора:

    Однако, просто создать файл недостаточно. Нам нужно подключить библиотеку Raylib.

    Интеграция Raylib через CMake

    Есть много способов подключить библиотеку: скачать бинарники, использовать git submodules или пакетные менеджеры (Conan, Vcpkg). Для учебного проекта самым надежным и простым способом является FetchContent. Это модуль CMake, который сам скачает исходники Raylib и подключит их к проекту при первой сборке.

    Обновим наш CMakeLists.txt:

    Что здесь происходит?

  • FetchContent_Declare: Мы говорим CMake, откуда скачать Raylib.
  • FetchContent_MakeAvailable: CMake скачивает, настраивает и готовит библиотеку к использованию.
  • target_link_libraries: Мы говорим компилятору: «Когда будешь собирать наш редактор, используй код из Raylib».
  • Разделение на модули: Отказ от монолита

    Главная ошибка при создании редакторов — писать код игры и код редактора вперемешку. Редактор уровней — это программа, которая использует движок вашей игры, но также имеет свой собственный интерфейс (UI).

    Мы разделим проект на три логических слоя:

  • Core (Ядро): Обертки над Raylib, управление окном, входными данными (Input), ресурсами. Этот код ничего не знает об игре.
  • Game (Игра): Логика сущностей, физика, рендеринг игрового мира. Этот код использует Core.
  • Editor (Редактор): UI инструменты, панели выбора тайлов, сетка, сохранение/загрузка. Этот код управляет Game и использует Core.
  • !Архитектурные слои редактора уровней

    Пример организации кода

    Вместо того чтобы писать всё в main.cpp, мы создадим класс приложения.

    Файл include/app.h:

    Файл src/app.cpp:

    И, наконец, наш src/main.cpp становится предельно чистым:

    Зачем нужен #pragma once?

    В примере выше вы видели директиву #pragma once. Это страж включения (header guard). Если вы случайно подключите один и тот же .h файл в нескольких местах (что неизбежно в больших проектах), компилятор без этой директивы попытается объявить одни и те же классы дважды и выдаст ошибку. #pragma once гарантирует, что файл будет обработан компилятором только один раз.

    Практическое задание: Сборка проекта

    Теперь ваша задача — воспроизвести эту структуру на своем компьютере.

  • Создайте папку RaylibEditor.
  • Внутри создайте подпапки src, include, assets.
  • Создайте файл CMakeLists.txt с кодом, приведенным выше (версия с FetchContent).
  • Создайте src/main.cpp, src/app.cpp и include/app.h.
  • Запустите конфигурацию CMake:
  • * Создайте папку build. * Откройте терминал в папке build. * Выполните cmake ... * После завершения выполните cmake --build . (или откройте сгенерированный проект в IDE).

    Если вы увидели окно с надписью "Editor Initialized!" — поздравляю! Вы создали профессиональную заготовку для вашего будущего редактора уровней.

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

    2. Архитектура приложения: отделение движка Raylib от логики редактора

    Архитектура приложения: отделение движка Raylib от логики редактора

    В предыдущей статье мы настроили систему сборки CMake и создали базовую структуру папок. Теперь у нас есть фундамент, но сам код всё ещё уязвим. Если мы начнем писать логику редактора, напрямую вызывая функции Raylib (например, InitWindow или IsKeyDown) внутри классов игровых сущностей, мы создадим жесткую зависимость (tight coupling).

    Сегодня мы займемся архитектурной гигиеной. Мы создадим слой абстракции Core, который отделит «железо» и библиотеку Raylib от высокоуровневой логики вашего редактора. Это ключевой шаг, отличающий любительскую поделку от профессионального инструмента.

    Проблема прямых зависимостей

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

    Почему это плохо?

  • Загрязнение глобального пространства: Подключая raylib.h в заголовочные файлы игры, вы тянете за собой сотни констант и функций во все файлы, которые включают player.h.
  • Невозможность подмены: Если завтра вы захотите портировать редактор на SDL2 или SFML, вам придется переписывать каждый файл, где упоминается Raylib.
  • Сложность тестирования: Вы не сможете протестировать логику игрока без создания реального окна и контекста OpenGL.
  • Решение: Слой Core

    Мы внедрим промежуточный слой. Наша архитектура будет выглядеть как слоеный пирог, где зависимости идут только сверху вниз.

    !Диаграмма слоев: Логика редактора общается только с Core, а Core инкапсулирует работу с Raylib.

    Слой Core будет предоставлять интерфейсы для: * Управления окном (Window) * Обработки ввода (Input) * Работы с ресурсами (ResourceManager)

    Абстракция окна (Window Wrapper)

    Начнем с создания класса, который будет отвечать за жизненный цикл окна. Вместо того чтобы вызывать InitWindow в main.cpp, мы делегируем это классу Core::Window.

    Создайте файл include/core/window.h:

    И реализацию в src/core/window.cpp:

    Что мы получили? Теперь наш main.cpp или класс App не знают, что под капотом используется Raylib. Мы используем идиому RAII (Resource Acquisition Is Initialization): окно создается в конструкторе и уничтожается в деструкторе.

    Абстракция ввода (Input System)

    Это самая важная часть. Мы не хотим использовать KEY_SPACE (константу Raylib) в коде редактора. Мы создадим свой собственный перечень клавиш.

    Файл include/core/input_keys.h:

    Теперь создадим класс Input, который будет транслировать наши клавиши в клавиши Raylib. Файл include/core/input.h:

    В реализации src/core/input.cpp нам придется сделать маппинг (сопоставление). Это немного рутинная работа, но её нужно сделать один раз.

    Интеграция в приложение

    Теперь обновим наш класс App, который мы создали в прошлой статье. Теперь он будет владеть объектом Window и использовать Input.

    Файл include/app.h:

    Файл src/app.cpp:

    Преимущества такого подхода

  • Чистота кода: В App и будущих классах редактора нет ни одного #include "raylib.h". Логика чиста от деталей реализации графики.
  • Безопасность: Мы не можем случайно вызвать EndDrawing() дважды или забыть CloseWindow(), так как это контролируется классом Window.
  • Расширяемость: Если нам понадобится логирование всех нажатий клавиш, мы добавим это в одно место — в метод Core::Input::IsKeyPressed, и это заработает во всем проекте сразу.
  • Итог

    Мы успешно изолировали библиотеку Raylib внутри слоя Core. Наш редактор теперь общается с внешним миром через строго определенные интерфейсы. Это может показаться излишним усложнением на старте, но когда ваш проект вырастет до 50+ файлов, эта структура спасет вас от хаоса.

    В следующей статье мы займемся реализацией игрового цикла (Game Loop) и созданием базовой системы сцен, чтобы переключаться между режимом редактирования и режимом игры.

    3. Управление сущностями: менеджеры объектов и базовый полиморфизм

    Управление сущностями: менеджеры объектов и базовый полиморфизм

    В предыдущих статьях мы заложили фундамент нашего редактора: настроили систему сборки CMake и отделили низкоуровневый код работы с окном (Core) от остальной программы. Теперь у нас есть чистое окно и система ввода, но наш мир пуст. В нём нет ни игрока, ни врагов, ни платформ.

    Сегодня мы займемся наполнением этого мира. Мы спроектируем архитектуру системы сущностей (Entity System). Это сердце любого игрового движка или редактора уровней. Мы научимся хранить, обновлять и отрисовывать сотни объектов, не зная заранее, чем именно они являются.

    Проблема разнородных объектов

    Представьте, что вы пишете игру без продуманной архитектуры. Скорее всего, вы начнете с создания списков для каждого типа объектов:

    Для игры это может сработать (хотя и с трудом), но для редактора уровней такой подход фатален. Редактор должен уметь загружать, перемещать и сохранять любой объект, который вы поместите на сцену. Если вы добавите новый тип врага, вам не захочется переписывать код редактора, добавляя новый std::vector.

    Нам нужен способ хранить всё в одном контейнере:

    Здесь нам на помощь приходит полиморфизм.

    Базовый класс Entity

    Все объекты на сцене имеют общие черты: у них есть позиция, их нужно обновлять каждый кадр, и их нужно рисовать. Вынесем это в абстрактный базовый класс Entity.

    Создайте файл include/game/entity.h:

    Почему деструктор виртуальный?

    Это критически важный момент в C++. Если вы удаляете объект производного класса через указатель на базовый класс (а мы будем делать именно так), и деструктор базового класса не виртуальный, то вызовется только деструктор Entity. Деструктор Player или Enemy вызван не будет. Это приведет к утечке памяти и ресурсов.

    Реализация конкретных сущностей

    Теперь мы можем создать классы, которые наследуются от Entity. Например, создадим простого игрока.

    Файл include/game/player.h:

    Обратите внимание: класс Player реализует методы Update и Render по-своему. Это и есть суть полиморфизма.

    Менеджер сущностей (EntityManager)

    Теперь нам нужен «дирижер», который будет управлять этим оркестром объектов. Класс EntityManager будет владеть всеми сущностями, следить за их жизненным циклом и обновлять их.

    !Схема архитектуры: Менеджер управляет абстрактными сущностями, которые реализуются в конкретных классах.

    Выбор контейнера

    Какой контейнер использовать? std::vector<Entity> не сработает из-за срезки объектов (object slicing) и невозможности хранить абстрактные классы. Нам нужны указатели.

    Лучший выбор для современного C++ — std::vector<std::unique_ptr<Entity>>.

    * std::vector: Динамический массив, быстрый доступ. * std::unique_ptr: Умный указатель, который гарантирует, что у объекта есть только один владелец (наш менеджер). Когда вектор очищается, unique_ptr автоматически удаляет сущности. Никаких delete вручную!

    Файл include/game/entity_manager.h:

    Файл src/game/entity_manager.cpp:

    Математика движения: Delta Time

    В коде Player вы видели переменную deltaTime. Это фундаментальное понятие в разработке игр. Компьютеры имеют разную производительность. У кого-то игра выдает 60 кадров в секунду (FPS), у кого-то 144.

    Если мы будем просто прибавлять position.x += 5 каждый кадр, то на быстром компьютере игрок будет двигаться в 2-3 раза быстрее, чем на медленном. Чтобы скорость была постоянной в мире, а не в кадрах, мы используем формулу движения:

    Где: * — новая позиция объекта. * — текущая позиция объекта. * — скорость объекта (в пикселях или метрах в секунду). * — время, прошедшее с последнего кадра (в секундах).

    Если пикселей/сек, а кадр рисовался 0.016 секунды (60 FPS), то объект сдвинется на пикселя. Если кадр рисовался 0.1 секунды (10 FPS), сдвиг составит пикселей. В итоге за одну секунду объект пройдет ровно 100 пикселей на любом компьютере.

    Интеграция в App

    Осталось собрать всё вместе в нашем главном классе App.

    Обновим include/app.h:

    И src/app.cpp:

    Итоги

    Что мы получили благодаря этой архитектуре?

  • Расширяемость: Чтобы добавить врага, платформу или ловушку, нам нужно просто создать новый класс, наследуемый от Entity. Нам не нужно менять код App или EntityManager.
  • Управление памятью: EntityManager и unique_ptr берут на себя всю грязную работу по удалению объектов.
  • Чистота: Главный цикл приложения (Run) остается простым и понятным. Он просто делегирует задачи менеджеру.
  • В следующей статье мы сделаем наш редактор по-настоящему интерактивным: научимся выделять объекты мышью и перемещать их по сцене, используя простую физику столкновений AABB.

    4. Интеграция пользовательского интерфейса и паттерн Команда для отмены действий

    Интеграция пользовательского интерфейса и паттерн Команда для отмены действий

    В предыдущих частях курса мы проделали большую работу: настроили профессиональную структуру проекта на CMake, отделили движок от логики и создали систему управления сущностями. Теперь наш редактор умеет отображать объекты, но взаимодействовать с ними пока сложно. Мы всё ещё хардкодим создание объектов в коде.

    Настало время превратить нашу программу в настоящий инструмент. Для этого нам понадобятся две вещи:

  • Пользовательский интерфейс (UI): Кнопки и панели для выбора объектов.
  • Система отмены действий (Undo/Redo): Возможность исправить ошибку, нажав Ctrl+Z.
  • Сегодня мы внедрим библиотеку raygui и реализуем один из самых важных паттернов проектирования для редакторов — паттерн Команда (Command).

    Выбор UI библиотеки: Почему Immediate Mode?

    Для C++ существует множество GUI-библиотек: Qt, wxWidgets, ImGui. Для игровых инструментов стандартом де-факто стал подход Immediate Mode GUI (IMGUI). В отличие от классического (Retained Mode) подхода, где вы создаете объекты кнопок и храните их в памяти, в IMGUI интерфейс перерисовывается и обрабатывается каждый кадр заново.

    Для Raylib идеальным компаньоном является библиотека raygui. Это легковесная библиотека, состоящая из одного заголовочного файла, которая работает поверх Raylib.

    Подключение raygui

    Так как raygui — это header-only библиотека, её интеграция предельно проста. Скачайте файл raygui.h из официального репозитория и положите его в папку include/external/ или src/vendor/.

    В одном (и только одном!) .cpp файле (например, в src/core/gui_impl.cpp) вы должны реализовать библиотеку:

    Теперь в любом месте кода вы можете просто подключить заголовок и нарисовать кнопку:

    Это невероятно удобно для редактора: не нужно создавать сложные иерархии классов для UI, вы просто пишете if (Button...) там, где это нужно.

    Архитектурная проблема: Спагетти из событий

    Самый простой способ обработать нажатие кнопки — написать логику прямо внутри if:

    Это работает, но создает тупик. Что если пользователь поставил врага не туда? Ему придется удалять его вручную. А если он случайно удалил пол-уровня? Без функции «Отмена» (Undo) ваш редактор будет вызывать только раздражение.

    Чтобы реализовать Undo/Redo, нам нужно перестать воспринимать действия как функции и начать воспринимать их как объекты.

    Паттерн Команда (Command)

    Паттерн Команда инкапсулирует запрос в виде объекта. Это позволяет параметризовать клиенты с разными запросами, ставить запросы в очередь и, самое главное, поддерживать отмену операций.

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

    !Диаграмма классов UML, показывающая структуру паттерна Команда для редактора уровней.

    Интерфейс Команды

    Создадим абстрактный базовый класс. Файл include/commands/command.h:

    Конкретная команда: Размещение сущности

    Давайте реализуем команду добавления объекта. Чтобы отменить добавление, нужно удалить объект. Чтобы повторить (Redo) — добавить снова.

    Файл include/commands/place_entity_command.h:

    Обратите внимание на красоту решения: команда владеет данными, необходимыми для восстановления состояния. Когда мы делаем Undo, сущность не уничтожается, а перемещается из EntityManager обратно в PlaceEntityCommand.

    История команд (Command History)

    Нам нужен менеджер, который будет хранить стопку выполненных команд. Это классический стек, но с возможностью движения вперед-назад.

    Математически состояние редактора в момент времени можно описать как последовательное применение команд к начальному состоянию :

    Где: * — состояние системы в момент времени . * — -я команда в истории. * — пустое (начальное) состояние сцены.

    Операция Undo — это применение обратной функции (метода Undo):

    Где: * — предыдущее состояние системы. * — отмена последней команды.

    Реализуем этот механизм в классе CommandProcessor.

    Файл include/commands/command_processor.h:

    Интеграция всего вместе

    Теперь обновим наш класс Editor (или App), чтобы связать UI, ввод и команды.

    Важные нюансы реализации

    1. Состояние "грязного" файла

    Паттерн Команда отлично подходит для отслеживания изменений. Вы можете запомнить индекс команды, на котором было последнее сохранение (m_savedIndex).

    Где: * — флаг, показывающий, есть ли несохраненные изменения. * — текущая позиция в истории команд. * — позиция в истории, когда файл был сохранен на диск.

    Если индексы не равны, значит, пользователь что-то изменил, и нужно предложить сохранение при выходе.

    2. Лимит истории

    Бесконечная история команд может съесть всю оперативную память, если редактор работает долго. Обычно реализуют кольцевой буфер или просто удаляют старые команды из начала вектора, если их количество превышает, например, 100 штук.

    Заключение

    Сегодня мы сделали огромный шаг вперед. Наш редактор перестал быть просто «рисовалкой» и стал приложением с состоянием. Мы:

  • Подключили raygui для быстрого создания интерфейса.
  • Изолировали логику изменений в классы Command.
  • Реализовали Undo/Redo, что критически важно для любого творческого инструмента.
  • Теперь вы можете добавлять новые типы команд: MoveCommand (перемещение), DeleteCommand (удаление), ChangeColorCommand (изменение свойств). Архитектура готова к масштабированию.

    В следующей статье мы займемся сериализацией: научимся сохранять наш уровень в файл (JSON или бинарный формат) и загружать его обратно, чтобы ваш труд не пропадал после закрытия программы.

    5. Сериализация данных: грамотное сохранение и загрузка уровней

    Сериализация данных: грамотное сохранение и загрузка уровней

    Приветствую, коллеги. Мы проделали долгий путь. У нас есть архитектура на базе CMake, разделение на ядро и игру, система сущностей и даже возможность отменять действия через паттерн Команда. Но у нашего редактора есть один фатальный недостаток.

    Как только вы закрываете программу, ваш шедевральный уровень исчезает. Вся работа пропадает в небытие, так как данные живут только в оперативной памяти (RAM). Чтобы превратить нашу программу в полноценный инструмент, нам нужно научить её сериализации.

    Сегодня мы разберем, как сохранить состояние игрового мира в файл и восстановить его обратно, не нарушая принципов ООП и архитектуры, которую мы так старательно выстраивали.

    Что такое сериализация?

    Сериализация — это процесс перевода структуры данных или объекта в формат, который можно сохранить (в файл, в базу данных) или передать по сети. Обратный процесс называется десериализацией.

    Представьте, что ваш уровень — это собранный из LEGO замок. Чтобы отправить его другу по почте в маленьком конверте, вам нужно разобрать его на кирпичики и написать инструкцию: «Красный кубик 2x4 стоит на координатах (10, 5)». Друг, получив письмо, читает инструкцию и собирает замок заново.

    В нашем случае: * Замок — это список сущностей std::vector<std::unique_ptr<Entity>>. * Инструкция — это файл сохранения. * Друг — это функция загрузки уровня.

    !Процесс преобразования объекта из оперативной памяти в текстовый формат для сохранения на диск.

    Выбор формата: Бинарный или Текстовый?

    Перед тем как писать код, нужно решить, как мы будем хранить данные.

    1. Бинарный формат

    Вы просто копируете байты из памяти в файл. * Плюсы: Максимальная скорость, минимальный размер файла. * Минусы: Нечитаемо для человека. Если вы измените класс (добавите поле), старые сохранения перестанут работать без сложной конвертации. Сложно отлаживать.

    2. Текстовый формат (JSON, XML, YAML)

    Данные сохраняются в структурированном тексте. * Плюсы: Человекочитаемость (можно править уровень в Блокноте), легкость отладки, совместимость с системами контроля версий (git). * Минусы: Файл весит больше, парсинг (чтение) занимает время.

    > Для редакторов уровней и инструментов разработки JSON является золотым стандартом. Удобство разработки и поддержки важнее, чем экономия пары килобайт на диске.

    Мы выберем JSON (JavaScript Object Notation).

    Подключение библиотеки nlohmann/json

    В C++ нет встроенной поддержки JSON. Писать свой парсер — плохая идея (это долго и чревато ошибками). Мы воспользуемся стандартом де-факто в мире современного C++ — библиотекой nlohmann/json.

    Добавим её в наш проект через CMake, используя уже знакомый нам FetchContent. Откройте ваш корневой CMakeLists.txt:

    Теперь в коде мы можем использовать заголовок:

    Архитектура сохранения: Интерфейс Serializable

    Главная сложность — полиморфизм. У нас есть список указателей на Entity, но там могут лежать Player, Enemy или Platform. Каждый из них имеет свои уникальные поля.

    Нам нужно научить каждую сущность сохранять себя. Добавим чисто виртуальные методы в базовый класс Entity.

    Файл include/game/entity.h:

    Теперь реализуем это в классе Player. Файл include/game/player.h:

    Класс LevelSerializer

    Теперь создадим класс, который будет отвечать за запись всего уровня в файл и чтение из него. Это отдельный модуль, который не должен быть частью EntityManager (принцип единственной ответственности).

    Файл include/game/level_serializer.h:

    Реализация сохранения (Save)

    Здесь всё просто: мы проходим по всем сущностям, просим их превратиться в JSON и складываем в массив.

    Файл src/game/level_serializer.cpp:

    Реализация загрузки (Load) и проблема Фабрики

    С загрузкой сложнее. Мы читаем файл, видим объект {"type": "Player", ...}. Как нам создать именно Player? C++ не умеет создавать классы по строковому названию "из коробки" (в отличие от C# или Java, где есть рефлексия).

    Нам нужен паттерн Фабрика или простой реестр. В простейшем случае это серия if-else.

    Интеграция в UI

    Осталось добавить кнопки в наш редактор. Вернемся в App::Update или туда, где вы рисуете GUI.

    Теперь, запустив редактор, вы можете расставить объекты, нажать "Save", закрыть программу, открыть её снова, нажать "Load" — и вуаля! Ваш мир восстановлен.

    Продвинутые техники (на будущее)

    Решение с if (type == "Player") работает для малых проектов. Но если у вас 100 типов врагов, этот if станет гигантским. В профессиональных движках используют автоматическую регистрацию типов.

    Идея в том, чтобы создать std::map, где ключом является строка (имя типа), а значением — функция, создающая этот тип.

    Это позволяет добавлять новые классы врагов, не меняя код LevelSerializer.

    Заключение

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

    Мы использовали:

  • JSON для хранения данных (читаемость и удобство).
  • Библиотеку nlohmann/json для парсинга.
  • Виртуальные методы для полиморфного сохранения.
  • Паттерн Фабрика (в упрощенном виде) для восстановления объектов.
  • В следующей статье мы займемся оптимизацией рендеринга. Когда вы нарисуете на уровне тысячи тайлов, ваш FPS может упасть. Мы узнаем, что такое Batch Rendering и как заставить Raylib рисовать миллионы объектов без тормозов.