/
technologic
/
3d-adventure_next
Обзор
Документация
Войти
/
technologic
/
3d-adventure_next
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
Source/FirstPersonController.h
436 строк
25 KB
roboco
Big VoxelRaycast & Highlight FIX
27 май 2026, 23:10
27 май 2026, 23:10
8139419
Код
Авторство
О чём код?
/** * @file FirstPersonController.h * @brief Контроллер игрока от первого лица с физикой капсулы и воксельными коллизиями * * Реализует управление камерой и перемещением персонажа в воксельном мире. * Поддерживает два режима: **FPS** (с гравитацией и коллизиями) и **Свободный полёт** (noclip). * * @par Архитектура: * - Наследуется от ax::Node для интеграции в граф сцены * - Использует компонентную композицию: камера + физический резолвер + обработчики ввода * - Потокобезопасное взаимодействие с ChunkManager через const-ссылку (только чтение) * - Все обновления физики выполняются в главном потоке в update() * * @par Физическая модель: * - Игрок представлен капсулой (радиус + высота) для плавного скольжения по углам * - Раздельное ускорение/замедление на земле и в воздухе для точного контроля * - Sweep-based коллизии с sub-stepping для предотвращения туннелирования * - Ground probe для стабильного детектирования пола при высоком FPS * * @threadsafe Публичный API вызывается из главного потока. * Внутренний _collisionResolver обращается к ChunkManager только для чтения. */ #pragma once #include "axmol.h" #include "VoxelCollisionResolver.h" #include "ChunkManager.h" // BlockId и BLOCK_AIR #include <memory> class VoxelRaycasterNode; // ============================================================================ /** @name Контроллер первого лица * @{ */ // ============================================================================ /** * @brief Основной компонент управления персонажем от первого лица * * Координирует ввод пользователя, движение камеры и физическое взаимодействие * с воксельным миром. Реализует паттерн "контроллер-представление": логика * движения отделена от визуализации (камера — отдельный объект). * * @par Ключевые механизмы: * - **Разделение режимов**: FPS (физика) ↔ FreeFlight (свободное перемещение) * - **Адаптивная физика**: разные параметры трения/ускорения для земли и воздуха * - **Защита от артефактов**: заморозка при незагруженных чанках, clamp позиции * - **Безопасный lifecycle**: явное снятие listener'ов в onExit()/деструкторе * * @note Для корректной работы требуется установленный ChunkManager через setChunkManager(). * Без него коллизии отключены, но базовое перемещение работает. */ class FirstPersonController : public ax::Node { public: // ======================================================================== /** @name Фабрика и инициализация * @{ */ // ======================================================================== /** * @brief Фабричный метод создания контроллера * @param camera Указатель на основную камеру для привязки обзора * @param moveSpeed Базовая скорость перемещения (единиц/сек) * @param mouseSensitivity Чувствительность мыши (градусы/пиксель) * @return Указатель на созданный объект или nullptr при ошибке * @note Возвращённый объект имеет автоматическое управление памятью (autorelease) */ static FirstPersonController* create(ax::Camera* camera, float moveSpeed = 10.0f, float mouseSensitivity = 0.1f); /** * @brief Инициализация внутренних систем контроллера * @param camera Основная камера для управления обзором * @param moveSpeed Базовая скорость перемещения * @param mouseSensitivity Чувствительность мыши * @return true при успешной инициализации * @pre Вызывается только из create() после выделения памяти * @post Настраиваются event listener'ы, физическая капсула, планировщик update() */ bool init(ax::Camera* camera, float moveSpeed, float mouseSensitivity); /// @} // конец группы "Фабрика и инициализация" // ======================================================================== /** @name Управление состоянием и режимами * @{ */ // ======================================================================== /** * @brief Включение/выключение обработки ввода и физики * @param enabled Флаг активности контроллера * @note При отключении: сбрасываются флаги клавиш, скорость, очищается collision resolver */ void setEnabled(bool enabled); /// @brief Проверка: активен ли контроллер bool isEnabled() const { return _enabled; } ///< Возвращает текущее состояние флага _enabled /** * @brief Переключение режима свободного полёта (noclip) * @param enabled true = свободный полёт, false = режим FPS с физикой * @note В режиме freeFlight: отключаются гравитация и коллизии, курсор мыши видим */ void setFreeFlightMode(bool enabled); /// @brief Проверка: активен ли режим свободного полёта bool isFreeFlightMode() const { return _freeFlightMode; } ///< Возвращает флаг _freeFlightMode /// @brief Быстрое переключение между режимами (F5) void toggleFlightMode(); ///< Инвертирует _freeFlightMode и обновляет состояние курсора /** * @brief Установка начальной позиции игрока в мире * @param worldPos Мировые координаты для позиции капсулы * @note Позиция камеры автоматически смещается на высоту глаз (0.85 × высота капсулы) */ void setInitialPosition(const ax::Vec3& worldPos); /// @} // конец группы "Управление состоянием и режимами" // ======================================================================== /** @name Интеграция с миром * @{ */ // ======================================================================== /** * @brief Установка ссылки на менеджер чанков для коллизий * @param mgr Указатель на активный ChunkManager * @note Вызывать после создания мира, до первого update(). * Ссылка хранится как const, модификации мира через контроллер запрещены. */ void setChunkManager(ChunkManager* mgr) { _chunkMgr = mgr; } /** * @brief Установка рейкастера для ломания/становки блоков */ void setRaycaster(VoxelRaycasterNode* raycaster) { _raycaster = raycaster; } /// @} // конец группы "Интеграция с миром" // ======================================================================== /** @name Векторы ориентации камеры * @{ */ // ======================================================================== /** * @brief Получение вектора направления "вперёд" относительно камеры * @return Нормированный ax::Vec3 в мировых координатах * @par Механизм: * - Извлекает угол рыскания (yaw) из rotation3D камеры * - Вычисляет вектор в плоскости XZ: (-sin(yaw), 0, -cos(yaw)) * - В режиме freeFlight добавляет вертикальную компоненту из угла тангажа (pitch) * @note Возвращаемый вектор всегда нормирован (length == 1.0) */ ax::Vec3 getForwardVector() const; /** * @brief Получение вектора направления "вправо" относительно камеры * @return Нормированный ax::Vec3 в мировых координатах (перпендикулярно forward) * @par Механизм: * - Вычисляется через ортогональный поворот от forward: (cos(yaw), 0, -sin(yaw)) * - Не зависит от режима freeFlight — всегда в горизонтальной плоскости * @note Используется для бокового перемещения (клавиши A/D) */ ax::Vec3 getRightVector() const; /// @} // конец группы "Векторы ориентации камеры" // ======================================================================== /** @name Геттеры состояния * @{ */ // ======================================================================== /// @brief Получение текущей позиции игрока (низ капсулы) ax::Vec3 getPlayerPosition() const { return _capsule.bottomPos; } ///< Возвращает _capsule.bottomPos /// @brief Получение текущей скорости перемещения (с учётом множителя) float getMoveSpeed() const { return _moveSpeed; } ///< Возвращает _moveSpeed (базовая × множитель) /// @brief Получение базовой скорости (без множителей) float getBaseMoveSpeed() const { return _baseMoveSpeed; } ///< Возвращает _baseMoveSpeed из init() /// @brief Проверка: находится ли игрок на земле bool isGrounded() const { return _isGrounded; } ///< Возвращает флаг _isGrounded из последнего resolve() /// @brief [FIX #8] Геттер для капсулы игрока const PlayerCapsule& getCapsule() const { return _capsule; } /// @} // конец группы "Геттеры состояния" // ======================================================================== /** @name Основной игровой цикл * @{ */ // ======================================================================== /** * @brief Обновление состояния контроллера за кадр * * @par Алгоритм в режиме FPS: * 1. **Проверка загрузки чанка**: если чанк под игроком не активен → заморозка по Y * 2. **Сбор ввода**: вычисление направления движения из флагов клавиш * 3. **Интерполяция скорости**: плавное ускорение/замедление к targetVelocity * 4. **Гравитация и прыжки**: применение _gravity, детектирование нажатия пробела * 5. **Разрешение коллизий**: вызов _collisionResolver->resolve() с sub-stepping * 6. **Пост-обработка**: сброс скорости при приземлении/ударе о потолок, clamp позиции * * @par Алгоритм в режиме FreeFlight: * - Прямое перемещение камеры по направлению ввода, без физики и коллизий * - Позиция капсулы синхронизируется с камерой для совместимости с внешними системами * * @param dt Дельта времени с прошлого кадра (секунды) * @note Вызывается автоматически через scheduleUpdate(). Не вызывать вручную. * При dt <= 0 или !_enabled метод возвращает немедленно (no-op). */ void update(float dt) override; /// @} // конец группы "Основной игровой цикл" // ======================================================================== /** @name Настройка физики * @{ */ // ======================================================================== /** * @brief Установка множителя скорости (для бега, замедления и т.п.) * @param multiplier Коэффициент умножения на базовую скорость (рекомендуется 0.5–2.0) * @note Значение < 0 автоматически приводится к 0 через std::max(0.0f, multiplier) */ void setSpeedMultiplier(float multiplier); /** * @brief Настройка параметров трения и ускорения * @param groundAccel Ускорение при движении по земле (единиц/сек²) * @param groundDecel Замедление при отпускании клавиш на земле * @param airAccel Ускорение в воздухе (меньше чем на земле для контроля) * @param airDecel Замедление в воздухе (плавная остановка) * @note Значения применяются в update() при интерполяции скорости через std::lerp() */ void setFrictionParams(float groundAccel, float groundDecel, float airAccel, float airDecel); /// @brief Сброс текущей и целевой скорости в ноль void resetVelocity(); ///< Устанавливает _currentVelocity и _targetVelocity в (0,0,0) /// @} // конец группы "Настройка физики" // ======================================================================== /** @name Lifecycle и безопасность * @{ */ // ======================================================================== /** * @brief Обработчик удаления из сцены (безопасная очистка listener'ов) * * @par Зачем переопределён: * - Keyboard listener зарегистрирован с Fixed Priority → не удаляется автоматически * - Без явного снятия при удалении ноды возможен dangling callback → краш * * @par Механизм: * 1. Проверяет наличие _eventDispatcher * 2. Вызывает removeEventListener() для _keyboardListener и _mouseListener * 3. Обнуляет указатели для предотвращения double-free * 4. Вызывает базовый onExit() для завершения стандартного lifecycle * * @note Вызывается автоматически при removeFromParent() или замене сцены. * Не вызывать вручную без понимания контекста. */ void onExit() override; /// @} // конец группы "Lifecycle и безопасность" // ======================================================================== /** @name Внутренние обработчики событий (приватные) * @{ */ // ======================================================================== /// @name Event callbacks — вызываются Axmol EventDispatcher /// @{ /// @note Обработчики нажатия и отпускания кнопок мыши /// onMouseDown и onMouseUp инкапсулированы в setupEventListeners() /// @brief Обработчик движения мыши (поворот камеры) void onMouseMove(ax::Event* event); /// @brief Обработчик нажатия клавиши (установка флага _key*) void onKeyPressed(ax::EventKeyboard::KeyCode code, ax::Event* event); /// @brief Обработчик отпускания клавиши (сброс флага _key*) void onKeyReleased(ax::EventKeyboard::KeyCode code, ax::Event* event); /// @brief Обработчик действий с блоками void processBlockInteraction(float dt); /// @} // конец группы "Event callbacks" /// @} // конец группы "Внутренние обработчики событий" // ======================================================================== /** @name Конструкторы (приватные) * @{ */ // ======================================================================== /// @brief Приватный конструктор — создание только через create() FirstPersonController(); /// @brief Деструктор: гарантирует очистку listener'ов при прямом удалении ~FirstPersonController() override; /// @} // конец группы "Конструкторы" /// @} // конец основной группы "Контроллер первого лица" private: // ======================================================================== /** @name Инициализация внутренних систем * @{ */ // ======================================================================== /** * @brief Настройка event listener'ов для ввода * * @par Механизм: * - Mouse listener: привязан к scene graph → удаляется автоматически * - Keyboard listener: Fixed priority = 10 → требует явного снятия в onExit() * - Callback'и привязаны через AX_CALLBACK_* к методам этого класса * * @note Вызывается один раз в init(). Не переиспользовать. */ void setupEventListeners(); /// @} // конец группы "Инициализация внутренних систем" // ======================================================================== /** @name Члены данных: конфигурация и состояние * @{ */ // ======================================================================== ax::Camera* _camera = nullptr; ///< Основная камера для управления обзором float _baseMoveSpeed = 10.0f; ///< Базовая скорость из init() (не меняется) float _moveSpeed = 10.0f; ///< Текущая скорость = _baseMoveSpeed × множитель float _mouseSensitivity = 0.1f; ///< Чувствительность мыши (градусы/пиксель) bool _enabled = true; ///< Флаг активности: если false → update() становится no-op bool _freeFlightMode = false; ///< Режим свободного полёта: отключает физику и коллизии /// @name Состояние мыши /// @{ bool _isLeftMousePressed = false; ///< Флаг: зажата ли ЛКМ (для look в freeFlight) ax::Vec2 _lastMousePos = ax::Vec2::ZERO; ///< Позиция курсора для расчёта дельты /// @} /// @name Флаги клавиш (WASD + пробел) /// @{ bool _keyW = false, _keyA = false, _keyS = false, _keyD = false, _keySpace = false; ///< Устанавливаются в onKeyPressed/Released, читаются в update() /// @} float _breakAccum = 0; ///< Накапливание состояния разрушения блока BlockId _selectedBlockId = BLOCK_AIR; /// @} // конец группы "Конфигурация и состояние" // ======================================================================== /** @name Физика и коллизии * @{ */ // ======================================================================== /// @brief Резолвер коллизий с воксельным миром (ленивая инициализация) ///< Создаётся в первом update() при наличии _chunkMgr, уникален для экземпляра std::unique_ptr<VoxelCollisionResolver> _collisionResolver; /// @brief Физическая капсула игрока для расчёта коллизий ///< _bottomPos обновляется в resolve(), синхронизируется с позицией ноды PlayerCapsule _capsule; /// @name Векторы скорости /// @{ ax::Vec3 _targetVelocity = ax::Vec3::ZERO; ///< Целевая скорость на основе ввода ax::Vec3 _currentVelocity = ax::Vec3::ZERO; ///< Текущая скорость (интерполируется к target) ///< Интерполяция через std::lerp с адаптивным коэффициентом на основе трения /// @} /// @name Параметры физики движения /// @{ float _groundAccel = 40.0f; ///< Ускорение на земле (единиц/сек²) float _groundDecel = 30.0f; ///< Замедление на земле float _airAccel = 20.0f; ///< Ускорение в воздухе (меньше для контроля) float _airDecel = 15.0f; ///< Замедление в воздухе /// @} /// @brief Порог для обнуления малых скоростей (предотвращение "дрожания") static constexpr float VELOCITY_EPSILON = 0.01f; bool _isGrounded = false; ///< Флаг: касается ли капсула земли (из последнего resolve()) float _gravity = -25.0f; ///< Ускорение свободного падения (отрицательное = вниз) float _jumpForce = 8.0f; ///< Начальная вертикальная скорость при прыжке /// @brief Скорость разрушения блока static constexpr float BREAK_TIME = 3.0f; /// @} // конец группы "Физика и коллизии" // ======================================================================== /** @name Event listeners (Axmol) * @{ */ // ======================================================================== ax::EventListenerMouse* _mouseListener = nullptr; ///< Listener мыши (scene graph priority) ax::EventListenerKeyboard* _keyboardListener = nullptr; ///< Listener клавиатуры (fixed priority = 10) ///< ⚠️ keyboardListener требует явного снятия в onExit()/деструкторе! /// @} // конец группы "Event listeners" // ======================================================================== /** @name Интеграция с миром * @{ */ // ======================================================================== ChunkManager* _chunkMgr = nullptr; ///< Ссылка на менеджер чанков для запросов блоков ///< Используется только для чтения: getBlockAtWorldPos(), isChunkActive() VoxelRaycasterNode* _raycaster = nullptr; ///< Cсылка на рейкастер для редактирования мира /// @} // конец группы "Интеграция с миром" }; /// @} // конец основной группы "Контроллер первого лица"