diff --git a/CMakeLists.txt b/CMakeLists.txt index f31fba2..8aab540 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,7 @@ add_executable(${TARGET} src/engine/resource/font_manager.cpp src/engine/render/renderer.cpp src/engine/render/camera.cpp + src/engine/input/input_manager.cpp ) # 链接库 diff --git a/src/engine/core/game_app.cpp b/src/engine/core/game_app.cpp index 62d46e8..21d1258 100644 --- a/src/engine/core/game_app.cpp +++ b/src/engine/core/game_app.cpp @@ -3,6 +3,7 @@ #include "../resource/resource_manager.h" #include "../render/renderer.h" #include "../render/camera.h" +#include "../input/input_manager.h" #include "config.h" #include #include @@ -27,7 +28,8 @@ void GameApp::run() { while (is_running_) { time_->update(); float delta_time = time_->getDeltaTime(); - + input_manager_->update(); // 每帧首先更新输入管理器 + handleEvents(); update(delta_time); render(); @@ -46,6 +48,7 @@ bool GameApp::init() { if (!initResourceManager()) return false; if (!initRenderer()) return false; if (!initCamera()) return false; + if (!initInputManager()) return false; // 测试资源管理器 testResourceManager(); @@ -56,12 +59,13 @@ bool GameApp::init() { } void GameApp::handleEvents() { - SDL_Event event; - while (SDL_PollEvent(&event)) { - if (event.type == SDL_EVENT_QUIT) { - is_running_ = false; - } + if (input_manager_->shouldQuit()) { + spdlog::trace("GameApp 收到来自 InputManager 的退出请求。"); + is_running_ = false; + return; } + + testInputManager(); } void GameApp::update(float /* delta_time */) { @@ -185,6 +189,17 @@ bool GameApp::initCamera() { return true; } +bool GameApp::initInputManager() +{ + try { + input_manager_ = std::make_unique(sdl_renderer_, config_.get()); + } catch (const std::exception& e) { + spdlog::error("初始化输入管理器失败: {}", e.what()); + return false; + } + spdlog::trace("输入管理器初始化成功。"); + return true; +} // --- 测试用函数 --- @@ -224,4 +239,31 @@ void GameApp::testCamera() if (key_state[SDL_SCANCODE_RIGHT]) camera_->move(glm::vec2(1, 0)); } +void GameApp::testInputManager() +{ + std::vector actions = { + "move_up", + "move_down", + "move_left", + "move_right", + "jump", + "attack", + "pause", + "MouseLeftClick", + "MouseRightClick" + }; + + for (const auto& action : actions) { + if (input_manager_->isActionPressed(action)) { + spdlog::info(" {} 按下 ", action); + } + if (input_manager_->isActionReleased(action)) { + spdlog::info(" {} 抬起 ", action); + } + if (input_manager_->isActionDown(action)) { + spdlog::info(" {} 按下中 ", action); + } + } +} + } // namespace engine::core \ No newline at end of file diff --git a/src/engine/core/game_app.h b/src/engine/core/game_app.h index 5b77b03..729c4c1 100644 --- a/src/engine/core/game_app.h +++ b/src/engine/core/game_app.h @@ -14,6 +14,10 @@ class Renderer; class Camera; } +namespace engine::input { +class InputManager; +} + namespace engine::core { // 命名空间的最佳实践:与文件路径一致 class Time; class Config; @@ -33,6 +37,7 @@ private: std::unique_ptr renderer_; std::unique_ptr camera_; std::unique_ptr config_; + std::unique_ptr input_manager_; public: GameApp(); ~GameApp(); @@ -62,11 +67,13 @@ private: [[nodiscard]] bool initResourceManager(); [[nodiscard]] bool initRenderer(); [[nodiscard]] bool initCamera(); + [[nodiscard]] bool initInputManager(); // 测试用函数 void testResourceManager(); void testRenderer(); void testCamera(); + void testInputManager(); }; } // namespace engine::core diff --git a/src/engine/input/input_manager.cpp b/src/engine/input/input_manager.cpp new file mode 100644 index 0000000..4473e82 --- /dev/null +++ b/src/engine/input/input_manager.cpp @@ -0,0 +1,216 @@ +#include "input_manager.h" +#include "../core/config.h" +#include +#include +#include +#include + + +namespace engine::input { + +InputManager::InputManager(SDL_Renderer* sdl_renderer, const engine::core::Config* config): sdl_renderer_(sdl_renderer) { + if (!sdl_renderer_) { + spdlog::error("输入管理器: SDL_Renderer 为空指针"); + throw std::runtime_error("输入管理器: SDL_Renderer 为空指针"); + } + initializeMappings(config); + // 获取初始鼠标位置 + float x, y; + SDL_GetMouseState(&x, &y); + mouse_position_ = {x, y}; + spdlog::trace("初始鼠标位置: ({}, {})", mouse_position_.x, mouse_position_.y); +} + +// --- 更新和事件处理 --- + +void InputManager::update() { + // 1. 根据上一帧的值更新默认的动作状态 + for (auto& [action_name, state] : action_states_) { + if (state == ActionState::PRESSED_THIS_FRAME) { + state = ActionState::HELD_DOWN; // 当某个键按下不动时,并不会生成SDL_Event。 + } else if (state == ActionState::RELEASED_THIS_FRAME) { + state = ActionState::INACTIVE; + } + } + + // 2. 处理所有待处理的 SDL 事件 (这将设定 action_states_ 的值) + SDL_Event event; + while (SDL_PollEvent(&event)) { + processEvent(event); + } +} + +void InputManager::processEvent(const SDL_Event& event) { + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: { + SDL_Scancode scancode = event.key.scancode; // 获取按键的scancode + bool is_down = event.key.down; + bool is_repeat = event.key.repeat; + + auto it = scancode_to_actions_map_.find(scancode); + if (it != scancode_to_actions_map_.end()) { // 如果按键有对应的action + const std::vector& associated_actions = it->second; + for (const std::string& action_name : associated_actions) { + updateActionState(action_name, is_down, is_repeat); // 更新action状态 + } + } + break; + } + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: { + Uint8 button = event.button.button; // 获取鼠标按钮 + bool is_down = event.button.down; + auto it = mouse_button_to_actions_map_.find(button); + if (it != mouse_button_to_actions_map_.end()) { // 如果鼠标按钮有对应的action + const std::vector& associated_actions = it->second; + for (const std::string& action_name : associated_actions) { + // 鼠标事件不考虑repeat, 所以第三个参数传false + updateActionState(action_name, is_down, false); // 更新action状态 + } + } + // 在点击时更新鼠标位置 + mouse_position_ = {event.button.x, event.button.y}; + break; + } + case SDL_EVENT_MOUSE_MOTION: // 处理鼠标运动 + mouse_position_ = {event.motion.x, event.motion.y}; + break; + case SDL_EVENT_QUIT: + should_quit_ = true; + break; + default: + break; + } +} + +// --- 状态查询方法 --- + +bool InputManager::isActionDown(const std::string& action_name) const { + // C++17 引入的 “带有初始化语句的 if 语句” + if (auto it = action_states_.find(action_name); it != action_states_.end()) { + return it->second == ActionState::PRESSED_THIS_FRAME || it->second == ActionState::HELD_DOWN; + } + return false; +} + +bool InputManager::isActionPressed(const std::string& action_name) const { + if (auto it = action_states_.find(action_name); it != action_states_.end()) { + return it->second == ActionState::PRESSED_THIS_FRAME; + } + return false; +} + +bool InputManager::isActionReleased(const std::string& action_name) const { + if (auto it = action_states_.find(action_name); it != action_states_.end()) { + return it->second == ActionState::RELEASED_THIS_FRAME; + } + return false; +} + +bool InputManager::shouldQuit() const { + return should_quit_; +} + +void InputManager::setShouldQuit(bool should_quit) +{ + should_quit_ = should_quit; +} + +glm::vec2 InputManager::getMousePosition() const +{ + return mouse_position_; +} + +glm::vec2 InputManager::getLogicalMousePosition() const +{ + glm::vec2 logical_pos; + // 通过窗口坐标获取渲染坐标(逻辑坐标) + SDL_RenderCoordinatesFromWindow(sdl_renderer_, mouse_position_.x, mouse_position_.y, &logical_pos.x, &logical_pos.y); + return logical_pos; +} + +// --- 初始化输入映射 --- + +void InputManager::initializeMappings(const engine::core::Config* config) { + spdlog::trace("初始化输入映射..."); + if (!config) { + spdlog::error("输入管理器: Config 为空指针"); + throw std::runtime_error("输入管理器: Config 为空指针"); + } + actions_to_keyname_map_ = config->input_mappings_; // 获取配置中的输入映射(动作 -> 按键名称) + scancode_to_actions_map_.clear(); + mouse_button_to_actions_map_.clear(); + action_states_.clear(); + + // 如果配置中没有定义鼠标按钮动作(通常不需要配置),则添加默认映射, 用于 UI + if (actions_to_keyname_map_.find("MouseLeftClick") == actions_to_keyname_map_.end()) { + spdlog::debug("配置中没有定义 'MouseLeftClick' 动作,添加默认映射到 'MouseLeft'."); + actions_to_keyname_map_["MouseLeftClick"] = {"MouseLeft"}; // 如果缺失则添加默认映射 + } + if (actions_to_keyname_map_.find("MouseRightClick") == actions_to_keyname_map_.end()) { + spdlog::debug("配置中没有定义 'MouseRightClick' 动作,添加默认映射到 'MouseRight'."); + actions_to_keyname_map_["MouseRightClick"] = {"MouseRight"}; // 如果缺失则添加默认映射 + } + // 遍历 动作 -> 按键名称 的映射 + for (const auto& [action_name, key_names] : actions_to_keyname_map_) { + // 每个动作对应一个动作状态,初始化为 INACTIVE + action_states_[action_name] = ActionState::INACTIVE; + spdlog::trace("映射动作: {}", action_name); + // 设置 "按键 -> 动作" 的映射 + for (const std::string& key_name : key_names) { + SDL_Scancode scancode = scancodeFromString(key_name); // 尝试根据按键名称获取scancode + Uint8 mouse_button = mouseButtonUint8FromString(key_name); // 尝试根据按键名称获取鼠标按钮 + // 未来可添加其它输入类型 ... + + if (scancode != SDL_SCANCODE_UNKNOWN) { // 如果scancode有效,则将action添加到scancode_to_actions_map_中 + scancode_to_actions_map_[scancode].push_back(action_name); + spdlog::trace(" 映射按键: {} (Scancode: {}) 到动作: {}", key_name, static_cast(scancode), action_name); + } else if (mouse_button != 0) { // 如果鼠标按钮有效,则将action添加到mouse_button_to_actions_map_中 + mouse_button_to_actions_map_[mouse_button].push_back(action_name); + spdlog::trace(" 映射鼠标按钮: {} (Button ID: {}) 到动作: {}", key_name, static_cast(mouse_button), action_name); + // else if: 未来可添加其它输入类型 ... + } else { + spdlog::warn("输入映射警告: 未知键或按钮名称 '{}' 用于动作 '{}'.", key_name, action_name); + } + } + } + spdlog::trace("输入映射初始化完成."); +} + +// --- 工具函数 --- +// 将字符串名称转换为 SDL_Scancode +SDL_Scancode InputManager::scancodeFromString(const std::string& key_name) { + return SDL_GetScancodeFromName(key_name.c_str()); +} + +// 将鼠标按钮名称字符串转换为 SDL 按钮 Uint8 值 +Uint8 InputManager::mouseButtonUint8FromString(const std::string& button_name) { + if (button_name == "MouseLeft") return SDL_BUTTON_LEFT; + if (button_name == "MouseMiddle") return SDL_BUTTON_MIDDLE; + if (button_name == "MouseRight") return SDL_BUTTON_RIGHT; + // SDL 还定义了 SDL_BUTTON_X1 和 SDL_BUTTON_X2 + if (button_name == "MouseX1") return SDL_BUTTON_X1; + if (button_name == "MouseX2") return SDL_BUTTON_X2; + return 0; // 0 不是有效的按钮值,表示无效 +} + +void InputManager::updateActionState(const std::string& action_name, bool is_input_active, bool is_repeat_event) { + auto it = action_states_.find(action_name); + if (it == action_states_.end()) { + spdlog::warn("尝试更新未注册的动作状态: {}", action_name); + return; + } + + if (is_input_active) { // 输入被激活 (按下) + if (is_repeat_event) { + it->second = ActionState::HELD_DOWN; + } else { // 非重复的按下事件 + it->second = ActionState::PRESSED_THIS_FRAME; + } + } else { // 输入被释放 (松开) + it->second = ActionState::RELEASED_THIS_FRAME; + } +} + +} // namespace engine::input \ No newline at end of file diff --git a/src/engine/input/input_manager.h b/src/engine/input/input_manager.h new file mode 100644 index 0000000..83c88ca --- /dev/null +++ b/src/engine/input/input_manager.h @@ -0,0 +1,65 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace engine::core { + class Config; +} + +namespace engine::input { + +enum class ActionState { + INACTIVE, ///< @brief 动作未激活 + PRESSED_THIS_FRAME, ///< @brief 动作在本帧刚刚被按下 + HELD_DOWN, ///< @brief 动作被持续按下 + RELEASED_THIS_FRAME ///< @brief 动作在本帧刚刚被释放 +}; + +class InputManager final { +private: + SDL_Renderer* sdl_renderer_; ///< @brief 用于获取逻辑坐标的 SDL_Renderer 指针 + std::unordered_map> actions_to_keyname_map_; ///< @brief 存储动作名称到按键名称列表的映射 + std::unordered_map> scancode_to_actions_map_;///< @brief 从键盘(Scancode)到关联的动作名称列表 + std::unordered_map> mouse_button_to_actions_map_; ///< @brief 从鼠标按钮 (Uint8) 到关联的动作名称列表 + + std::unordered_map action_states_; ///< @brief 存储每个动作的当前状态 + + bool should_quit_ = false; ///< @brief 退出标志 + glm::vec2 mouse_position_; ///< @brief 鼠标位置 (针对屏幕坐标) + +public: + /** + * @brief 构造函数 + * @param sdl_renderer 指向 SDL_Renderer 的指针 + * @param config 配置对象 + * @throws std::runtime_error 如果任一指针为 nullptr。 + */ + InputManager(SDL_Renderer* sdl_renderer, const engine::core::Config* config); + + void update(); ///< @brief 更新输入状态,每轮循环最先调用 + + + // 动作状态检查 + bool isActionDown(const std::string& action_name) const; ///< @brief 动作当前是否触发 (持续按下或本帧按下) + bool isActionPressed(const std::string& action_name) const; ///< @brief 动作是否在本帧刚刚按下 + bool isActionReleased(const std::string& action_name) const; ///< @brief 动作是否在本帧刚刚释放 + + bool shouldQuit() const; ///< @brief 查询退出状态 + void setShouldQuit(bool should_quit); ///< @brief 设置退出状态 + + glm::vec2 getMousePosition() const; ///< @brief 获取鼠标位置 (屏幕坐标) + glm::vec2 getLogicalMousePosition() const; ///< @brief 获取鼠标位置 (逻辑坐标) + +private: + void processEvent(const SDL_Event& event); ///< @brief 处理 SDL 事件(将按键转换为动作状态) + void initializeMappings(const engine::core::Config* config); ///< @brief 根据 Config配置初始化映射表 + + void updateActionState(const std::string& action_name, bool is_input_active, bool is_repeat_event); ///< @brief 辅助更新动作状态 + SDL_Scancode scancodeFromString(const std::string& key_name); ///< @brief 将字符串键名转换为 SDL_Scancode + Uint8 mouseButtonUint8FromString(const std::string& button_name); ///< @brief 将字符串按钮名转换为 SDL_Button +}; + +} // namespace engine::input \ No newline at end of file