完成输入管理器1

This commit is contained in:
Ziyu
2025-06-16 14:44:47 +08:00
parent d3b9ad3f84
commit e015ff4a49
5 changed files with 337 additions and 6 deletions
+1
View File
@@ -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
)
# 链接库
+48 -6
View File
@@ -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 <SDL3/SDL.h>
#include <spdlog/spdlog.h>
@@ -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<engine::input::InputManager>(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<std::string> 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
+7
View File
@@ -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<engine::render::Renderer> renderer_;
std::unique_ptr<engine::render::Camera> camera_;
std::unique_ptr<engine::core::Config> config_;
std::unique_ptr<engine::input::InputManager> 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
+216
View File
@@ -0,0 +1,216 @@
#include "input_manager.h"
#include "../core/config.h"
#include <stdexcept>
#include <SDL3/SDL.h>
#include <spdlog/spdlog.h>
#include <glm/vec2.hpp>
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<std::string>& 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<std::string>& 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<int>(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<int>(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
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <SDL3/SDL_render.h>
#include <glm/vec2.hpp>
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<std::string, std::vector<std::string>> actions_to_keyname_map_; ///< @brief 存储动作名称到按键名称列表的映射
std::unordered_map<SDL_Scancode, std::vector<std::string>> scancode_to_actions_map_;///< @brief 从键盘(Scancode)到关联的动作名称列表
std::unordered_map<Uint8, std::vector<std::string>> mouse_button_to_actions_map_; ///< @brief 从鼠标按钮 (Uint8) 到关联的动作名称列表
std::unordered_map<std::string, ActionState> 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