/
technologic
/
3d-adventure_next
Обзор
Документация
Войти
/
technologic
/
3d-adventure_next
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
Source/ChunkManager.h
860 строк
49 KB
roboco
Big VoxelRaycast & Highlight FIX
27 май 2026, 23:10
27 май 2026, 23:10
8139419
Код
Авторство
О чём код?
/** * @file ChunkManager.h * @brief Менеджер чанков для воксельного мира в стиле Minecraft * * Отвечает за жизненный цикл чанков: генерацию, загрузку, визуализацию и выгрузку. * Использует многопоточную архитектуру для асинхронной генерации воксельных данных * и потокобезопасную передачу результатов в главный поток для отрисовки. * * @par Принцип работы: * - Игрок перемещается по миру → вычисляются координаты чанков в радиусе видимости * - Новые чанки ставятся в очередь генерации (_genQueue) * - Фоновые воркеры извлекают задачи, генерируют данные через _onGenerate коллбек * - Готовые чанки передаются в _readyChunks и визуализируются в главном потоке * - Чанки за пределами радиуса выгрузки помечаются и удаляются с освобождением ресурсов * * @par Потокобезопасность: * - _genQueue: защищён мьютексом, используется condition_variable для ожидания задач * - _readyChunks: защищён _readyMtx, запись из воркеров, чтение из главного потока * - _chunks: доступ только из главного потока (игровой цикл) * - _genPendingSet: доступ только из главного потока для отслеживания ожидающих генерацию * * @note Для паузы/возобновления используйте pause()/resume() — они сохраняют состояние мира. * shutdown() вызывается только в деструкторе для полной очистки ресурсов. */ #pragma once #include "axmol.h" #include <condition_variable> #include <deque> #include <functional> #include <mutex> #include <thread> #include <unordered_map> #include <unordered_set> #include <vector> #include <memory> #include <algorithm> #include <cmath> #include <cstdint> // ============================================================================ /** @name Базовые типы и константы * @{ */ // ============================================================================ /** * @brief Уникальный идентификатор чанка по его координатам в сетке мира * * Используется как ключ в хеш-таблицах для быстрого доступа к данным чанка. * Координаты (x, y, z) представляют индекс чанка в глобальной сетке, * а не мировые координаты в единицах игры. */ struct ChunkKey { int32_t x, y, z; ///< Координаты чанка в сетке мира /// @brief Сравнение на равенство для использования в контейнерах bool operator==(const ChunkKey& o) const noexcept { return x == o.x && y == o.y && z == o.z; } /// @brief Сравнение на неравенство (инверсия operator==) bool operator!=(const ChunkKey& o) const noexcept { return !(*this == o); } /** * @brief Функтор хеширования для использования в std::unordered_map * * Реализует FNV-1a 64-bit алгоритм с побайтовым смешиванием координат. * Обеспечивает равномерное распределение хешей для пространственных данных, * минимизируя коллизии при последовательных координатах чанков. */ struct Hash { size_t operator()(const ChunkKey& k) const noexcept { // FNV-1a 64-bit → size_t. Быстрый и устойчивый к коллизиям хеш для координат uint64_t h = 14695981039346656037ULL; auto mix = [&](int32_t v) { uint64_t u = static_cast<uint64_t>(static_cast<uint32_t>(v)); for (int i = 0; i < 4; ++i) { h ^= (u >> (i * 8)) & 0xFF; h *= 1099511628211ULL; } }; mix(k.x); mix(k.y); mix(k.z); return static_cast<size_t>(h); } }; }; /// @brief Идентификатор блока — компактное хранение типа вокселя using BlockId = uint16_t; static constexpr BlockId BLOCK_AIR = 0; ///< Пустой блок (воздух) static constexpr BlockId BLOCK_GRASS = 1; ///< Блок травы (верхний слой земли) static constexpr BlockId BLOCK_STONE = 2; ///< Каменный блок static constexpr BlockId BLOCK_DIRT = 3; ///< Блок земли /// @name Размеры чанка в блоках /// @{ static constexpr int CHUNK_SIZE_X = 16; ///< Ширина чанка по оси X static constexpr int CHUNK_SIZE_Y = 256; ///< Высота чанка по оси Y (вертикаль) static constexpr int CHUNK_SIZE_Z = 16; ///< Глубина чанка по оси Z /// @} // ============================================================================ /** @name Режимы фильтрации текстур * @{ */ // ============================================================================ /** * @brief Режим фильтрации текстур для воксельного атласа */ enum class TextureFilterMode { NEAREST, ///< Ближайший сосед, без mipmaps (чёткие пиксели) NEAREST_MIPMAP_NEAREST ///< Ближайший сосед + mipmaps (убирает мерцание) }; /// @} // конец группы "Режимы фильтрации текстур" // ============================================================================ /** @name Данные чанка * @{ */ // ============================================================================ /** * @brief Контейнер воксельных данных одного чанка * * Хранит линейный массив блоков размером CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z. * Использует Y-major layout для оптимизации кэш-локальности при проходе по высоте. * * @par Память: ~128 КБ на чанк (65536 блоков × 2 байта) * @par Индексация: lx + ly * CHUNK_SIZE_X + lz * CHUNK_SIZE_X * CHUNK_SIZE_Y */ class ChunkData { public: /// @brief Конструктор с инициализацией массива блоков значением BLOCK_AIR explicit ChunkData(const ChunkKey& key) : _key(key) { _blocks.resize(CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z, BLOCK_AIR); } /// @brief Получение ключа чанка (константная ссылка) const ChunkKey& getKey() const noexcept { return _key; } /** * @brief Чтение типа блока по локальным координатам * @param lx Локальная координата X [0; CHUNK_SIZE_X) * @param ly Локальная координата Y [0; CHUNK_SIZE_Y) * @param lz Локальная координата Z [0; CHUNK_SIZE_Z) * @return BlockId типа блока или BLOCK_AIR при выходе за границы */ BlockId getBlock(int lx, int ly, int lz) const noexcept { return _blocks[index(lx, ly, lz)]; } /** * @brief Запись типа блока по локальным координатам * @param lx Локальная координата X [0; CHUNK_SIZE_X) * @param ly Локальная координата Y [0; CHUNK_SIZE_Y) * @param lz Локальная координата Z [0; CHUNK_SIZE_Z) * @param id Идентификатор блока для записи */ void setBlock(int lx, int ly, int lz, BlockId id) noexcept { _blocks[index(lx, ly, lz)] = id; } /// @brief Проверка: состоит ли чанк полностью из воздуха (оптимизация рендера) bool isAllAir() const noexcept { return std::all_of(_blocks.begin(), _blocks.end(), [](BlockId b) { return b == BLOCK_AIR; }); } /// @brief Доступ к сырому массиву блоков для прямого доступа (осторожно!) std::vector<BlockId>& rawBlocks() noexcept { return _blocks; } private: /** * @brief Расчёт линейного индекса в массиве блоков * @par Формула: lx + ly * CHUNK_SIZE_X + lz * CHUNK_SIZE_X * CHUNK_SIZE_Y * @note Y-major layout: изменение Y даёт последовательный доступ к памяти */ static size_t index(int lx, int ly, int lz) noexcept { // [FIX #5] Исправлена индексация на Y-major (ly меняется быстрее всего) return static_cast<size_t>(ly + lx * CHUNK_SIZE_Y + lz * CHUNK_SIZE_X * CHUNK_SIZE_Y); } ChunkKey _key; ///< Ключ чанка для идентификации std::vector<BlockId> _blocks; ///< Линейный массив воксельных данных }; /// @brief Статус жизненного цикла чанка в менеджере enum class ChunkStatus { None, ///< Чанк не инициализирован / удалён QueuedForGen, ///< Ожидает генерации в очереди воркеров Active, ///< Сгенерирован и отображается в сцене QueuedForUnload ///< Помечен на выгрузку, ожидает обработки }; /// @} // конец группы "Данные чанка" // ============================================================================ /** @name Менеджер чанков — основной класс * @{ */ // ============================================================================ /** * @brief Центральный компонент управления воксельным миром * * Координирует асинхронную генерацию, кэширование и визуализацию чанков. * Реализует паттерн Producer-Consumer: главный поток ставит задачи генерации, * фоновые воркеры выполняют тяжёлые вычисления, результаты возвращаются * через потокобезопасные очереди для отрисовки в главном потоке. * * @par Ключевые механизмы: * - **Face Culling**: не генерирует грани между сплошными блоками (экономия >90% вершин) * - **Cross-chunk culling**: учитывает соседей для устранения щелей на границах чанков * - **Dirty-флаг**: перестраивает меш чанка при загрузке соседей для корректного culling * - **Пауза/возобновление**: сохраняет состояние мира при сворачивании приложения * - **Ограничение очереди**: предотвращает утечки памяти при быстром перемещении игрока * * @threadsafe Частично: публичный API вызывается из главного потока, * внутренняя синхронизация защищает доступ к общим данным. */ class ChunkManager { public: // ======================================================================== /** @name Конфигурация * @{ */ // ======================================================================== /** * @brief Параметры настройки поведения менеджера * * @note Все изменения конфигурации применяются только при вызове init(). * Динамическое изменение параметров во время работы не поддерживается. */ struct Config { int renderDistance = 8; ///< Радиус видимости чанков от игрока (в чанках) int unloadMargin = 2; ///< Дополнительный буфер перед выгрузкой (рендер + margin) int workerThreadCount = 2; ///< Количество фоновых потоков генерации int maxGenerationsPerFrame = 2; ///< Макс. чанков для визуализации за один кадр int maxUnloadsPerFrame = 4; ///< Макс. чанков для выгрузки за один кадр /// @brief [FIX #15] Лимит очереди генерации для защиты от переполнения памяти int maxQueueSize = 128; /// @brief [FIX #10] Лимит перестроений мешей dirty-чанков за кадр int maxDirtyRebuildsPerFrame = 2; /// @brief Режим фильтрации текстур атласа TextureFilterMode textureFilter = TextureFilterMode::NEAREST; }; /// @} // конец группы "Конфигурация" // ======================================================================== /** @name Типы коллбеков (межпоточные границы) * @{ */ // ======================================================================== /** * @brief Коллбек генерации данных чанка * @threadsafe Вызывается в ФОНОВОМ потоке воркера * @param chunk Ссылка на объект ChunkData для заполнения воксельными данными * @note Не должен обращаться к графическому API или сцене — только вычисления */ using GenerateCallback = std::function<void(ChunkData&)>; /** * @brief Коллбек визуализации готового чанка * @threadsafe Вызывается в ГЛАВНОМ потоке (игровой цикл) * @param node Указатель на созданный ax::Node с мешем чанка * @param key Ключ чанка для идентификации * @note Здесь безопасно добавлять нод в сцену, настраивать материалы, анимации */ using VisualizeCallback = std::function<void(ax::Node*, const ChunkKey&)>; /** * @brief Коллбек выгрузки чанка * @threadsafe Вызывается в ГЛАВНОМ потоке (игровой цикл) * @param node Указатель на визуальный нод для удаления из сцены * @param key Ключ чанка для идентификации * @note Ответственность за очистку ресурсов (removeFromParent, delete) лежит на коллбеке */ using UnloadCallback = std::function<void(ax::Node*, const ChunkKey&)>; /// @} // конец группы "Типы коллбеков" // ======================================================================== /** @name Конструкторы и управление жизненным циклом * @{ */ // ======================================================================== ChunkManager(); ///< Конструктор по умолчанию ~ChunkManager(); ///< Деструктор: гарантирует вызов shutdown() для очистки /// @name Правило пяти: объект не копируемый (владеет потоками и ресурсами) /// @{ ChunkManager(const ChunkManager&) = delete; ///< Запрет копирования ChunkManager& operator=(const ChunkManager&) = delete; ///< Запрет присваивания /// @} /** * @brief Инициализация менеджера с заданной конфигурацией * @param cfg Параметры работы: радиус видимости, потоки, лимиты * @note Вызывать один раз после создания объекта, до первого update() * @pre Не должен вызываться повторно без предварительного shutdown() */ void init(const Config& cfg); /** * @brief Полная остановка и очистка ресурсов (только для деструктора) * * @par Механизм: * 1. Сбрасывает флаг _running → воркеры завершают текущие задачи и выходят * 2. Ожидает join() всех потоков генерации * 3. Очищает все очереди и хеш-таблицы чанков * 4. Вызывает _onUnload для всех активных визуальных нод * 5. Сбрасывает текстуры и внутренние флаги * * @warning Не использовать для временной паузы! Только при уничтожении объекта. * Для pause/resume используйте соответствующие методы. */ void shutdown(); /** * @brief Временная остановка генерации с сохранением состояния мира * * @par Механизм: * 1. Устанавливает флаг _paused = true → update() становится no-op * 2. Сигнализирует воркерам остановиться после завершения текущей задачи * 3. Ожидает завершения всех потоков (blocking, но безопасно при сворачивании) * 4. Обрабатывает чанки в _readyChunks чтобы не потерять готовые данные * 5. Очищает очередь генерации и сбрасывает флаг stop для будущего resume() * * @note Все данные чанков (_chunks) сохраняются в памяти. * Визуальные ноды остаются в сцене, но не обновляются. * @see resume() для возобновления работы */ void pause(); /** * @brief Возобновление работы после pause() * * @par Механизм: * 1. Сбрасывает флаг _paused = false → update() снова обрабатывает мир * 2. Перезапускает воркеры через startWorkers() с сохранённой конфигурацией * 3. Сбрасывает _lastPlayerChunk для принудительного пересчёта видимых чанков * 4. Проверяет валидность текстуры (защита от context loss на мобильных) * 5. Помечает активные чанки как dirty если текстура была пересоздана * * @note Если вызван без предварительного pause() или после shutdown() — no-op. * @see pause() для временной остановки */ void resume(); /// @brief Проверка: находится ли менеджер в состоянии паузы bool isPaused() const { return _paused; } /** * @brief Принудительный сброс кэша позиции игрока для немедленного обновления чанков * * Вызывается после init() чтобы чанки начали грузиться сразу, * а не после первого движения игрока (нажатия клавиши). * Сбрасывает _lastPlayerChunk в специальное значение, которое гарантированно * не совпадёт с реальными координатами, forcing collectChunksToLoad() на первом update(). * * @note Безопасно вызывать только после init() и до первого update() */ void forceUpdate() { _lastPlayerChunk = ChunkKey{INT32_MIN, INT32_MIN, INT32_MIN}; } /// @} // конец группы "Конструкторы и управление жизненным циклом" // ======================================================================== /** @name Основной игровой цикл * @{ */ // ======================================================================== /** * @brief Обновление состояния мира за кадр * * @par Алгоритм работы: * 1. Если _paused → немедленный возврат (без вычислений) * 2. Вычисляет координаты чанка игрока через worldToChunk() * 3. Если игрок перешёл в новый чанк: * - collectChunksToLoad(): ставит в очередь новые чанки в радиусе renderDistance * - collectChunksToUnload(): помечает далёкие чанки на выгрузку * 4. processReadyChunks(): визуализирует готовые чанки из _readyChunks (лимит per-frame) * 5. processDirtyChunks(): перестраивает меши чанков при загрузке соседей (лимит per-frame) * 6. processUnloadQueue(): удаляет чанки за пределами unloadMargin (лимит per-frame) * * @param playerWorldPos Текущая позиция игрока в мировых координатах * @note Вызывать каждый кадр из update() сцены. Все лимиты предотвращают просадки FPS. */ void update(const ax::Vec3& playerWorldPos); /// @} // конец группы "Основной игровой цикл" // ======================================================================== /** @name Установка коллбеков * @{ */ // ======================================================================== /// @brief Установка коллбека генерации (выполняется в фоновом потоке) void setOnGenerate(GenerateCallback cb) { _onGenerate = std::move(cb); } /// @brief Установка коллбека визуализации (выполняется в главном потоке) void setOnVisualize(VisualizeCallback cb) { _onVisualize = std::move(cb); } /// @brief Установка коллбека выгрузки (выполняется в главном потоке) void setOnUnload(UnloadCallback cb) { _onUnload = std::move(cb); } /// @} // конец группы "Установка коллбеков" // ======================================================================== /** @name Публичный API доступа к чанкам * @{ */ // ======================================================================== /** * @brief Получение визуального нода чанка по ключу * @param key Ключ искомого чанка * @return Указатель на ax::Node если чанк активен, nullptr иначе * @note Используется внешними системами для привязки эффектов, маркеров и т.п. */ ax::Node* getChunkNode(const ChunkKey& key) const; /// @brief Проверка: активен ли чанк (сгенерирован и отображается) bool isChunkActive(const ChunkKey& key) const; /** * @brief Получение типа блока по мировым координатам * * @par Механизм: * 1. Вычисляет ключ чанка через worldToChunk(worldPos) * 2. Ищет чанк в _chunks, проверяет статус Active и наличие chunkData * 3. Конвертирует мировые координаты в локальные внутри чанка * 4. Возвращает BlockId через getBlock() или BLOCK_AIR при ошибке * * @param worldPos Мировые координаты точки запроса * @return BlockId типа блока или BLOCK_AIR если чанк не загружен / координаты вне границ * @note Использует chunkData из ChunkEntry для потокобезопасного доступа * без race conditions с генерацией/выгрузкой. */ BlockId getBlockAtWorldPos(const ax::Vec3& worldPos) const; /** * @brief Конвертация координат чанка в мировые координаты (нижний угол) * @param key Ключ чанка * @return ax::Vec3 мировых координат точки (x*CHUNK_SIZE_X, y*CHUNK_SIZE_Y, z*CHUNK_SIZE_Z) * @note Используется для позиционирования визуального нода чанка в сцене */ static ax::Vec3 chunkToWorld(const ChunkKey& key) noexcept { return {(float)(key.x * CHUNK_SIZE_X), (float)(key.y * CHUNK_SIZE_Y), (float)(key.z * CHUNK_SIZE_Z)}; } /** * @brief Конвертация мировых координат в ключ чанка * @param pos Мировые координаты точки * @return ChunkKey чанка, содержащего данную точку * @note Использует std::floor для корректной работы с отрицательными координатами */ static ChunkKey worldToChunk(const ax::Vec3& pos) noexcept { return {static_cast<int32_t>(std::floor(pos.x / CHUNK_SIZE_X)), static_cast<int32_t>(std::floor(pos.y / CHUNK_SIZE_Y)), static_cast<int32_t>(std::floor(pos.z / CHUNK_SIZE_Z))}; } /** * @brief Установка типа блока по мировым координатам * @param worldPos Мировые координаты (центр блока: floor + 0.5) * @param id Новый тип блока (BLOCK_AIR = удалить) * @return true если блок изменён, false если чанк не загружен */ bool setBlockAtWorldPos(const ax::Vec3& worldPos, BlockId id); /// @} // конец группы "Публичный API доступа к чанкам" /// @} // конец основной группы "Менеджер чанков" private: // ======================================================================== /** @name Внутренние структуры данных * @{ */ // ======================================================================== /** * @brief Запись о состоянии чанка в хеш-таблице _chunks * * Объединяет метаданные жизненного цикла, визуальное представление * и воксельные данные в единой структуре для эффективного доступа. */ struct ChunkEntry { ChunkStatus status = ChunkStatus::None; ///< Текущий статус в жизненном цикле ax::Node* visualNode = nullptr; ///< Указатель на отрисовываемый нод (если активен) /// @brief Хранение данных чанка до выгрузки для: /// - Потокобезопасного запроса блоков (VoxelCollisionResolver) /// - Кросс-чанкового face culling при генерации соседей std::unique_ptr<ChunkData> chunkData; /// @brief Флаг необходимости перестроения меша ///< Устанавливается при загрузке соседей: грани на стыке могут стать невидимыми bool dirty = false; }; /** * @brief Потокобезопасная очередь задач генерации * * Реализует паттерн Producer-Consumer с condition_variable * для эффективного ожидания задач без активного polling. */ struct TaskQueue { std::mutex mtx; ///< Мьютекс для синхронизации доступа к очереди std::condition_variable cv; ///< Условие для блокирующего ожидания задач std::deque<ChunkKey> queue; ///< FIFO очередь ключей чанков на генерацию bool stop = false; ///< Флаг остановки для корректного завершения воркеров /// @brief Максимальный размер очереди для защиты памяти size_t maxSize = 128; /** * @brief Добавление задачи в очередь (потокобезопасно) * @param k Ключ чанка для генерации * @note Если очередь переполнена (size >= maxSize) — задача отбрасывается */ void push(const ChunkKey& k) { std::lock_guard<std::mutex> lk(mtx); if (queue.size() >= maxSize) return; ///< Защита от переполнения queue.push_back(k); cv.notify_one(); ///< Пробуждает один ожидающий воркер } /** * @brief Извлечение задачи из очереди (блокирующее, потокобезопасно) * @param out Выходной параметр для полученного ключа * @return true если задача получена, false если stop=true и очередь пуста * @note Блокирует поток до появления задачи или сигнала остановки */ bool pop(ChunkKey& out) { std::unique_lock<std::mutex> lk(mtx); cv.wait(lk, [&] { return stop || !queue.empty(); }); if (stop && queue.empty()) return false; out = queue.front(); queue.pop_front(); return true; } /// @brief Сигнал остановки всем ожидающим воркерам void signalStop() { { std::lock_guard<std::mutex> lk(mtx); stop = true; } cv.notify_all(); ///< Пробуждает все воркеры для проверки stop } /** * @brief Очистка очереди и сброс флага остановки для resume() * * @par Зачем нужно: * После pause() флаг stop остаётся true. Без его сброса при resume() * воркеры в pop() сразу вернут false, не дождавшись новых задач. */ void clearAndReset() { std::lock_guard<std::mutex> lk(mtx); queue.clear(); stop = false; ///< Критично для работы resume() } }; /// @} // конец группы "Внутренние структуры данных" /** * @brief Применяет настройки фильтрации текстур к атласу _terrainAtlas. * * Конфигурирует параметры сэмплирования (Sampler/Texture Parameters) * согласно выбранному режиму в * _cfg.textureFilter. Поддерживает * NEAREST (классический * pixel-art) и NEAREST_MIPMAP_NEAREST (pixel-art * mipmaps для устранения мерцания на дальних дистанциях). * * @details * Вызывается в init() при первичной загрузке атласа и в resume() после * context loss. Автоматически генерирует mipmaps при необходимости. * * @warning Не потокобезопасен! Только главный поток. * * @see * TextureFilterMode */ void applyTextureFilter(); // ======================================================================== /** @name Приватные методы: управление потоками * @{ */ // ======================================================================== /** * @brief Запуск фоновых воркеров генерации * * @par Механизм: * - Проверяет что _workers пуст (защита от дублирования) * - Устанавливает _running = true (atomic release для видимости в воркерах) * - Создаёт _cfg.workerThreadCount потоков, выполняющих workerLoop() * * @note Выносится в отдельный метод для повторного использования в resume() * без дублирования кода запуска потоков. */ void startWorkers(); /** * @brief Основной цикл фонового воркера * * @par Алгоритм: * 1. Бесконечный цикл: извлекает ключ чанка из _genQueue.pop() * 2. При stop=true и пустой очереди — выходит из цикла (завершение потока) * 3. Создаёт ChunkData для полученного ключа * 4. Вызывает _onGenerate(*chunk) для заполнения воксельными данными * 5. Помещает готовый chunk в _readyChunks под защитой _readyMtx * * @threadsafe Вызывается только в фоновых потоках. * _onGenerate выполняется вне блокировок — пользователь отвечает за потокобезопасность. */ void workerLoop(); /// @} // конец группы "Управление потоками" // ======================================================================== /** @name Приватные методы: сборка задач * @{ */ // ======================================================================== /** * @brief Сбор чанков для загрузки в радиусе видимости игрока * * @par Механизм: * 1. Перебирает чанки в квадрате [playerChunk ± renderDistance] по X/Z * 2. Фильтрует: пропускает уже загруженные, ожидающие генерации, за пределами радиуса * 3. Сортирует кандидаты по расстоянию до игрока (ближние в приоритете) * 4. Для каждого кандидата: * - try_emplace в _chunks (один lookup вместо find+operator[]) * - устанавливает статус QueuedForGen, сбрасывает visualNode/chunkData * - добавляет в _genPendingSet для отслеживания "в работе" * - ставит в _genQueue для обработки воркерами * * @param playerChunk Ключ чанка, в котором находится игрок * @note Использует try_emplace для оптимизации хеш-поиска */ void collectChunksToLoad(const ChunkKey& playerChunk); /** * @brief Сбор чанков для выгрузки за пределами буферной зоны * * @par Механизм: * 1. Проходит по всем записям в _chunks * 2. Удаляет записи со статусом None (неинициализированные) * 3. Пропускает уже помеченные на выгрузку (QueuedForUnload) * 4. Если расстояние до игрока > (renderDistance + unloadMargin): * - устанавливает статус QueuedForUnload * - добавляет ключ в _unloadQueue для последующей обработки * * @param playerChunk Ключ чанка, в котором находится игрок * @note Выгрузка происходит асинхронно в processUnloadQueue() с лимитом per-frame */ void collectChunksToUnload(const ChunkKey& playerChunk); /// @} // конец группы "Сборка задач" // ======================================================================== /** @name Приватные методы: обработка очередей * @{ */ // ======================================================================== /** * @brief Визуализация готовых чанков из _readyChunks * * @par Механизм: * 1. Атомарно извлекает все элементы из _readyChunks в локальный вектор * 2. Обрабатывает максимум _cfg.maxGenerationsPerFrame чанков за кадр: * - Проверяет что чанк всё ещё актуален (не помечен на выгрузку) * - Перемещает chunkData в соответствующий ChunkEntry * - Если чанк полностью из воздуха: пропускает создание меша (оптимизация) * - Иначе: вызывает buildChunkVisualNode() для создания графического представления * - Вызывает _onVisualize() для добавления нода в сцену * - Помечает 4 соседей как dirty (их грани на стыке могут измениться) * 3. Непроцессированные чанки возвращаются в _readyChunks для следующего кадра * * @note Лимит per-frame предотвращает просадки FPS при массовой загрузке */ // [FIX #3] Добавлен флаг force для возможности обработки очереди при паузе void processReadyChunks(bool force = false); /** * @brief Выгрузка чанков из _unloadQueue * * @par Механизм: * 1. Обрабатывает максимум _cfg.maxUnloadsPerFrame записей за кадр * 2. Для каждого ключа: * - Находит запись в _chunks, проверяет статус QueuedForUnload * - Вызывает _onUnload() для удаления визуального нода из сцены * - Удаляет запись из _chunks и _genPendingSet * - Удаляет ключ из _unloadQueue * 3. Непроцессированные остаются в очереди для следующего кадра * * @note Постепенная выгрузка распределяет нагрузку на память и CPU */ void processUnloadQueue(); /** * @brief Перестроение мешей dirty-чанков при загрузке соседей * * @par Зачем нужно: * При загрузке нового чанка-соседа грани на общей границе могут стать * невидимыми (если соседний блок сплошной). Без перестроения меша * на стыке чанков остаются лишние грани → визуальные артефакты. * * @par Механизм: * 1. Собирает до _cfg.maxDirtyRebuildsPerFrame активных чанков с флагом dirty * 2. Для каждого: * - Удаляет старый визуальный нод через _onUnload() * - Перестраивает меш через buildChunkVisualNode() с учётом новых соседей * - Устанавливает новый visualNode и сбрасывает dirty * - Вызывает _onVisualize() для добавления в сцену * * @note Лимит per-frame предотвращает фризы при одновременной * загрузке множества соседей (например, при быстром полёте) */ void processDirtyChunks(); /// @} // конец группы "Обработка очередей" // ======================================================================== /** @name Приватные методы: построение визуализации * @{ */ // ======================================================================== /** * @brief Создание визуального нода чанка с мешем и материалами * * @par Механизм: * 1. Получает данные соседей (X-1, X+1, Z-1, Z+1) для кросс-чанкового face culling * 2. Вызывает chunk_mesh_builder::buildChunkMesh() для генерации вершин/индексов: * - Проходит по всем блокам чанка, пропускает BLOCK_AIR * - Для каждого блока проверяет 6 направлений через isSolid() * - Генерирует грань только если сосед — воздух или отсутствует * - isSolid() проверяет как локальные блоки, так и данные соседей * 3. Создаёт ax::Mesh через chunk_mesh_builder::createMesh(): * - Деинтерливинг данных (позиции, нормали, UV в отдельные массивы) * - Создание VBO/IBO, расчёт AABB, привязка текстуры * 4. Создаёт ax::MeshRenderer, добавляет меш и материал (UNLIT с диффузной текстурой) * 5. Позиционирует нод в мировых координатах через chunkToWorld() * * @param key Ключ чанка для позиционирования * @param data Ссылка на воксельные данные чанка * @return Указатель на созданный ax::Node или nullptr при ошибке * @note Вынесена в отдельный метод для переиспользования * при первичной генерации и при перестроении dirty-чанков */ ax::Node* buildChunkVisualNode(const ChunkKey& key, ChunkData& data); /// @} // конец группы "Построение визуализации" // ======================================================================== /** @name Приватные утилиты * @{ */ // ======================================================================== /** * @brief Расчёт расстояния между чанками по метрике Чебышёва * @param a Первый ключ чанка * @param b Второй ключ чанка * @return Максимум из модулей разностей по осям: max(|dx|, |dy|, |dz|) * @note Идеально для сетки чанков: расстояние 1 = 26 соседей (включая диагонали) */ static int chunkDistance(const ChunkKey& a, const ChunkKey& b) noexcept { return std::max({std::abs(a.x - b.x), std::abs(a.y - b.y), std::abs(a.z - b.z)}); } /// @} // конец группы "Приватные утилиты" // ======================================================================== /** @name Члены данных: конфигурация и состояние * @{ */ // ======================================================================== Config _cfg{}; ///< Активная конфигурация менеджера /// @brief Хеш-таблица всех известных чанков: ключ → ChunkEntry ///< Доступ только из главного потока, не требует синхронизации std::unordered_map<ChunkKey, ChunkEntry, ChunkKey::Hash> _chunks; TaskQueue _genQueue; ///< Потокобезопасная очередь задач генерации std::vector<ChunkKey> _unloadQueue; ///< Очередь ключей на выгрузку (главный поток) std::mutex _readyMtx; ///< Мьютекс для синхронизации _readyChunks /// @brief Очередь готовых чанков от воркеров к главному потоку ///< Запись: воркеры под _readyMtx, чтение: главный поток под _readyMtx std::vector<std::unique_ptr<ChunkData>> _readyChunks; /// @brief Множество чанков, ожидающих генерации (отслеживание "в работе") ///< Доступ только из главного потока для предотвращения дублирования задач std::unordered_set<ChunkKey, ChunkKey::Hash> _genPendingSet; std::vector<std::thread> _workers; ///< Пул фоновых потоков генерации std::atomic<bool> _running{false}; ///< Флаг работы воркеров (atomic для межпоточной видимости) /// @} // конец группы "Конфигурация и состояние" // ======================================================================== /** @name Члены данных: коллбеки и кэш * @{ */ // ======================================================================== GenerateCallback _onGenerate; ///< Коллбек генерации (выполняется в воркере) VisualizeCallback _onVisualize; ///< Коллбек визуализации (главный поток) UnloadCallback _onUnload; ///< Коллбек выгрузки (главный поток) ChunkKey _lastPlayerChunk{0, 0, 0}; ///< Кэшированный чанк игрока для детектирования перемещения ax::Texture2D* _terrainAtlas = nullptr; ///< Кэшированная текстура-атлас тайлов (загружается один раз) /// @} // конец группы "Коллбеки и кэш" // ======================================================================== /** @name Члены данных: управление состоянием паузы * @{ */ // ======================================================================== /// @brief Флаг паузы: отличает "приложение свёрнуто" от "сцена уничтожена" ///< _paused = false + shutdown() → финальная остановка, потоки уничтожены ///< _paused = true + pause() → временная остановка, данные сохранены для resume() bool _paused = false; /// @brief Флаг инициализации для безопасного resume() после shutdown() ///< Гарантирует что resume() знает, был ли успешно вызван init() ранее bool _initialized = false; /// @} // конец группы "Управление состоянием паузы" }; /// @} // конец основной группы "Менеджер чанков"