Files
XCEngine/docs/plan/输入模块的设计与实现.md
ssdfasd 36d3decef6 feat: 添加独立的输入系统和平台抽象层
- 新增 Platform 模块:PlatformTypes.h, Window.h, WindowsWindow
- 新增 Input 模块:InputTypes, InputEvent, InputAxis, InputModule, InputManager
- 新增 WindowsInputModule 处理 Win32 消息转换
- 将 RHI 集成测试从 render_model 迁移到 sphere
- 更新 CMakeLists.txt 添加 Platform 和 Input 模块
2026-03-22 15:21:52 +08:00

26 KiB
Raw Blame History

XCEngine 输入系统设计与实现

文档信息

  • 版本: 1.1
  • 日期: 2026-03-22
  • 状态: 设计文档
  • 目标: 设计引擎级输入系统

1. 概述

1.1 设计目标

XCEngine 输入系统需要提供:

  1. 统一的跨平台输入抽象 - 支持键盘、鼠标、触摸(手柄暂不考虑)
  2. 与引擎架构无缝集成 - 使用现有的 Core::Event 系统
  3. 轮询 + 事件混合模式 - 既支持 IsKeyDown() 轮询,也支持事件回调
  4. UI 系统支持 - 为 UI 组件 (Button, Slider 等) 提供指针事件
  5. 输入轴支持 - 参考 Unity 的 Input.GetAxis 设计,支持键盘/手柄统一映射

1.2 当前状态分析

模块 状态 说明
Core::Event 完备 线程安全Subscribe/Unsubscribe 模式
RHI::RHISwapChain ⚠️ PollEvents 空实现 需要填充 Windows 消息泵
现有 Input (mvs) 耦合 Windows 直接处理 HWND 消息,不适合引擎架构
Platform/Window 不存在 需要新建

1.3 设计决策

问题 决策
多窗口 单窗口,简化设计
手柄支持 暂不考虑,预留接口即可
输入轴 参考 Unity 实现 Input.GetAxis
IME/多语言 暂不考虑,只处理英文字符
模块位置 InputModule 放在 Input/ 命名空间

2. 架构设计

2.1 模块结构

engine/include/XCEngine/
├── Core/
│   └── Event.h              # 已有,复用
├── Input/
│   ├── InputTypes.h         # 枚举和结构体定义
│   ├── InputEvent.h         # 输入事件结构体
│   ├── InputAxis.h          # 输入轴定义
│   ├── InputManager.h       # 输入管理器(单例)
│   └── InputModule.h        # 平台相关输入处理模块接口
└── Platform/
    ├── PlatformTypes.h       # 平台类型抽象
    ├── Window.h             # 窗口抽象接口
    └── Windows/
        ├── WindowsPlatform.h     # Windows 平台实现
        └── WindowsInputModule.h # Windows 输入模块实现

2.2 核心设计原则

  1. 事件驱动 + 状态轮询双模式

    • 事件:用于 UI 交互、一次性按键响应
    • 轮询:用于连续输入检测(角色移动、视角控制)
  2. 平台抽象层

    • InputModule 接口:抽象平台特定的输入处理
    • Window 接口:抽象平台特定的窗口管理
  3. 输入轴映射

    • 键盘和手柄可以映射到同一个轴,实现统一输入
    • 支持在配置文件中定义轴映射关系
  4. 与现有引擎组件集成

    • 使用 Core::Event 作为事件系统
    • 使用 Math::Vector2 作为 2D 坐标类型

3. 详细设计

3.1 输入类型定义 (InputTypes.h)

#pragma once
#include "Core/Types.h"

namespace XCEngine {
namespace Input {

enum class KeyCode : uint8 {
    None = 0,
    
    // 字母键 A-Z
    A = 4, B = 5, C = 6, D = 7, E = 8, F = 9, G = 10,
    H = 11, I = 12, J = 13, K = 14, L = 15, M = 16, N = 17,
    O = 18, P = 19, Q = 20, R = 21, S = 22, T = 23, U = 24,
    V = 25, W = 26, X = 27, Y = 28, Z = 29,
    
    // 功能键 F1-F12
    F1 = 58, F2 = 59, F3 = 60, F4 = 61, F5 = 62, F6 = 63,
    F7 = 64, F8 = 65, F9 = 66, F10 = 67, F11 = 68, F12 = 69,
    
    // 控制键
    Space = 49, Tab = 48, Enter = 36, Escape = 53,
    LeftShift = 56, RightShift = 60, LeftCtrl = 59, RightCtrl = 62,
    LeftAlt = 58, RightAlt = 61,
    
    // 方向键
    Up = 126, Down = 125, Left = 123, Right = 124,
    
    // 编辑键
    Home = 115, End = 119, PageUp = 116, PageDown = 121,
    Delete = 51, Backspace = 51,
    
    // 数字键 0-9
    Zero = 39, One = 30, Two = 31, Three = 32,
    Four = 33, Five = 34, Six = 35, Seven = 37,
    Eight = 38, Nine = 40,
    
    // 符号键
    Minus = 43, Equals = 46, BracketLeft = 47, BracketRight = 54,
    Semicolon = 42, Quote = 40, Comma = 54, Period = 55,
    Slash = 44, Backslash = 45, Backtick = 41
};

enum class MouseButton : uint8 {
    Left = 0,
    Right = 1,
    Middle = 2,
    Button4 = 3,
    Button5 = 4
};

enum class JoystickAxis : uint8 {
    LeftX = 0,
    LeftY = 1,
    RightX = 2,
    RightY = 3,
    LeftTrigger = 4,
    RightTrigger = 5
};

} // namespace Input
} // namespace XCEngine

3.2 输入事件结构体 (InputEvent.h)

#pragma once
#include "InputTypes.h"
#include "Math/Vector2.h"
#include "Containers/String.h"

namespace XCEngine {
namespace Input {

struct KeyEvent {
    KeyCode keyCode;
    bool alt;
    bool ctrl;
    bool shift;
    bool meta;
    enum Type { Down, Up, Repeat } type;
};

struct MouseButtonEvent {
    MouseButton button;
    Math::Vector2 position;
    enum Type { Pressed, Released } type;
};

struct MouseMoveEvent {
    Math::Vector2 position;
    Math::Vector2 delta;
};

struct MouseWheelEvent {
    Math::Vector2 position;
    float delta;
};

struct TextInputEvent {
    char character;
    Containers::String text;
};

struct TouchState {
    int touchId;
    Math::Vector2 position;
    Math::Vector2 deltaPosition;
    float deltaTime;
    int tapCount;
    enum Phase { Began, Moved, Stationary, Ended, Canceled } phase;
};

} // namespace Input
} // namespace XCEngine

3.3 输入轴定义 (InputAxis.h)

#pragma once
#include "InputTypes.h"
#include "Math/Vector2.h"
#include "Containers/String.h"

namespace XCEngine {
namespace Input {

class InputAxis {
public:
    InputAxis() = default;
    InputAxis(const Containers::String& name, KeyCode positive, KeyCode negative = KeyCode::None)
        : m_name(name), m_positiveKey(positive), m_negativeKey(negative) {}
    
    const Containers::String& GetName() const { return m_name; }
    KeyCode GetPositiveKey() const { return m_positiveKey; }
    KeyCode GetNegativeKey() const { return m_negativeKey; }
    
    void SetKeys(KeyCode positive, KeyCode negative) {
        m_positiveKey = positive;
        m_negativeKey = negative;
    }
    
    float GetValue() const { return m_value; }
    void SetValue(float value) { m_value = value; }
    
private:
    Containers::String m_name;
    KeyCode m_positiveKey = KeyCode::None;
    KeyCode m_negativeKey = KeyCode::None;
    float m_value = 0.0f;
};

} // namespace Input
} // namespace XCEngine

3.4 输入管理器 (InputManager.h)

#pragma once
#include "Core/Event.h"
#include "InputTypes.h"
#include "InputEvent.h"
#include "InputAxis.h"
#include "Math/Vector2.h"

namespace XCEngine {
namespace Input {

class InputManager {
public:
    static InputManager& Get();

    void Initialize(void* platformWindowHandle);
    void Shutdown();
    void Update(float deltaTime);

    // ============ 轮询接口 ============
    
    // 键盘
    bool IsKeyDown(KeyCode key) const;
    bool IsKeyUp(KeyCode key) const;
    bool IsKeyPressed(KeyCode key) const;
    
    // 鼠标
    Math::Vector2 GetMousePosition() const;
    Math::Vector2 GetMouseDelta() const;
    float GetMouseScrollDelta() const;
    bool IsMouseButtonDown(MouseButton button) const;
    bool IsMouseButtonUp(MouseButton button) const;
    bool IsMouseButtonClicked(MouseButton button) const;
    
    // 触摸
    int GetTouchCount() const;
    TouchState GetTouch(int index) const;

    // ============ 轴接口 (参考 Unity) ============
    
    float GetAxis(const Containers::String& axisName) const;
    float GetAxisRaw(const Containers::String& axisName) const;
    
    bool GetButton(const Containers::String& buttonName) const;
    bool GetButtonDown(const Containers::String& buttonName) const;
    bool GetButtonUp(const Containers::String& buttonName) const;
    
    // 注册轴
    void RegisterAxis(const InputAxis& axis);
    void RegisterButton(const Containers::String& name, KeyCode key);
    void ClearAxes();

    // ============ 事件接口 ============
    
    Core::Event<const KeyEvent&>& OnKeyEvent() { return m_onKeyEvent; }
    Core::Event<const MouseButtonEvent&>& OnMouseButton() { return m_onMouseButton; }
    Core::Event<const MouseMoveEvent&>& OnMouseMove() { return m_onMouseMove; }
    Core::Event<const MouseWheelEvent&>& OnMouseWheel() { return m_onMouseWheel; }
    Core::Event<const TextInputEvent&>& OnTextInput() { return m_onTextInput; }
    
    // 内部方法(供 PlatformInputModule 调用)
    void ProcessKeyDown(KeyCode key, bool repeat);
    void ProcessKeyUp(KeyCode key);
    void ProcessMouseMove(int x, int y, int deltaX, int deltaY);
    void ProcessMouseButton(MouseButton button, bool pressed, int x, int y);
    void ProcessMouseWheel(float delta, int x, int y);
    void ProcessTextInput(char c);

private:
    InputManager() = default;
    ~InputManager() = default;

    void* m_platformWindowHandle = nullptr;
    
    // 键盘状态
    std::vector<bool> m_keyDownThisFrame;
    std::vector<bool> m_keyDownLastFrame;
    std::vector<bool> m_keyDown;
    
    // 鼠标状态
    Math::Vector2 m_mousePosition;
    Math::Vector2 m_mouseDelta;
    float m_mouseScrollDelta = 0.0f;
    std::vector<bool> m_mouseButtonDownThisFrame;
    std::vector<bool> m_mouseButtonDownLastFrame;
    std::vector<bool> m_mouseButtonDown;
    
    // 触摸状态
    std::vector<TouchState> m_touches;
    
    // 轴映射
    std::unordered_map<Containers::String, InputAxis> m_axes;
    std::unordered_map<Containers::String, KeyCode> m_buttons;
    std::vector<bool> m_buttonDownThisFrame;
    std::vector<bool> m_buttonDownLastFrame;
    
    // 事件
    Core::Event<const KeyEvent&> m_onKeyEvent;
    Core::Event<const MouseButtonEvent&> m_onMouseButton;
    Core::Event<const MouseMoveEvent&> m_onMouseMove;
    Core::Event<const MouseWheelEvent&> m_onMouseWheel;
    Core::Event<const TextInputEvent&> m_onTextInput;
};

} // namespace Input
} // namespace XCEngine

3.5 平台输入模块接口 (InputModule.h)

#pragma once

namespace XCEngine {
namespace Input {

class InputModule {
public:
    virtual ~InputModule() = default;
    
    virtual void Initialize(void* windowHandle) = 0;
    virtual void Shutdown() = 0;
    virtual void PumpEvents() = 0;
    
protected:
    InputModule() = default;
};

} // namespace Input
} // namespace XCEngine

3.6 Windows 输入模块实现 (WindowsInputModule.h)

#pragma once
#include "InputModule.h"
#include <Windows.h>

namespace XCEngine {
namespace Input {
namespace Platform {

class WindowsInputModule : public InputModule {
public:
    void Initialize(void* windowHandle) override;
    void Shutdown() override;
    void PumpEvents() override;
    
    void HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
    
private:
    void ProcessKeyDown(WPARAM wParam, LPARAM lParam);
    void ProcessKeyUp(WPARAM wParam, LPARAM lParam);
    void ProcessMouseMove(WPARAM wParam, LPARAM lParam);
    void ProcessMouseButton(WPARAM wParam, LPARAM lParam, bool pressed);
    void ProcessMouseWheel(WPARAM wParam, LPARAM lParam);
    void ProcessCharInput(WPARAM wParam, LPARAM lParam);
    
    KeyCode VKCodeToKeyCode(int vkCode);
    
    HWND m_hwnd = nullptr;
    Math::Vector2 m_lastMousePosition;
    bool m_captureMouse = false;
    bool m_isInitialized = false;
};

} // namespace Platform
} // namespace Input
} // namespace XCEngine

4. InputManager 实现细节

4.1 状态追踪机制

每帧 Update() 调用时:
1. m_keyDownLastFrame = m_keyDownThisFrame
2. 处理 m_keyDownThisFrame = m_keyDown (当前帧按下状态)
3. 清零 m_mouseDelta, m_mouseScrollDelta

IsKeyPressed(key) = m_keyDownThisFrame[key] && !m_keyDownLastFrame[key]  // 当前帧按下
IsKeyDown(key) = m_keyDown[key]  // 当前正按下
IsKeyUp(key) = !m_keyDown[key]   // 当前已释放

4.2 轴值计算

float InputManager::GetAxis(const Containers::String& axisName) const {
    auto it = m_axes.find(axisName);
    if (it == m_axes.end()) return 0.0f;
    
    const auto& axis = it->second;
    float value = 0.0f;
    
    if (axis.GetPositiveKey() != KeyCode::None && IsKeyDown(axis.GetPositiveKey())) {
        value += 1.0f;
    }
    if (axis.GetNegativeKey() != KeyCode::None && IsKeyDown(axis.GetNegativeKey())) {
        value -= 1.0f;
    }
    
    return value;
}

float InputManager::GetAxisRaw(const Containers::String& axisName) const {
    auto it = m_axes.find(axisName);
    if (it == m_axes.end()) return 0.0f;
    
    const auto& axis = it->second;
    float value = 0.0f;
    
    if (axis.GetPositiveKey() != KeyCode::None && IsKeyPressed(axis.GetPositiveKey())) {
        value += 1.0f;
    }
    if (axis.GetNegativeKey() != KeyCode::None && IsKeyPressed(axis.GetNegativeKey())) {
        value -= 1.0f;
    }
    
    return value;
}

4.3 默认轴配置

void InputManager::Initialize(void* platformWindowHandle) {
    // 注册默认轴 (类似 Unity)
    RegisterAxis(InputAxis("Horizontal", KeyCode::D, KeyCode::A));
    RegisterAxis(InputAxis("Vertical", KeyCode::W, KeyCode::S));
    RegisterAxis(InputAxis("Mouse X", KeyCode::None, KeyCode::None));  // 鼠标驱动
    RegisterAxis(InputAxis("Mouse Y", KeyCode::None, KeyCode::None));  // 鼠标驱动
    
    // 注册默认按钮
    RegisterButton("Jump", KeyCode::Space);
    RegisterButton("Fire1", KeyCode::LeftCtrl);
    RegisterButton("Fire2", KeyCode::LeftAlt);
    RegisterButton("Fire3", KeyCode::LeftShift);
}

5. Windows 消息到引擎事件的映射

5.1 消息映射表

Windows Message Engine Method 说明
WM_KEYDOWN ProcessKeyDown 按键按下wParam=VK码
WM_KEYUP ProcessKeyUp 按键释放wParam=VK码
WM_CHAR ProcessTextInput 字符输入wParam=字符
WM_MOUSEMOVE ProcessMouseMove 鼠标移动
WM_LBUTTONDOWN ProcessMouseButton(Left, Pressed) 左键按下
WM_LBUTTONUP ProcessMouseButton(Left, Released) 左键释放
WM_RBUTTONDOWN ProcessMouseButton(Right, Pressed) 右键按下
WM_RBUTTONUP ProcessMouseButton(Right, Released) 右键释放
WM_MBUTTONDOWN ProcessMouseButton(Middle, Pressed) 中键按下
WM_MBUTTONUP ProcessMouseButton(Middle, Released) 中键释放
WM_MOUSEWHEEL ProcessMouseWheel 滚轮滚动
WM_XBUTTONDOWN ProcessMouseButton(Button4/5, Pressed) 侧键按下
WM_XBUTTONUP ProcessMouseButton(Button4/5, Released) 侧键释放

5.2 VK 码到 KeyCode 映射

KeyCode WindowsInputModule::VKCodeToKeyCode(int vkCode) {
    switch (vkCode) {
        case 'A': return KeyCode::A;
        case 'B': return KeyCode::B;
        case 'C': return KeyCode::C;
        // ... Z
        case VK_SPACE: return KeyCode::Space;
        case VK_TAB: return KeyCode::Tab;
        case VK_RETURN: return KeyCode::Enter;
        case VK_ESCAPE: return KeyCode::Escape;
        case VK_SHIFT: return KeyCode::LeftShift;
        case VK_CONTROL: return KeyCode::LeftCtrl;
        case VK_MENU: return KeyCode::LeftAlt;
        case VK_UP: return KeyCode::Up;
        case VK_DOWN: return KeyCode::Down;
        case VK_LEFT: return KeyCode::Left;
        case VK_RIGHT: return KeyCode::Right;
        case VK_HOME: return KeyCode::Home;
        case VK_END: return KeyCode::End;
        case VK_PRIOR: return KeyCode::PageUp;
        case VK_NEXT: return KeyCode::PageDown;
        case VK_DELETE: return KeyCode::Delete;
        case VK_BACK: return KeyCode::Backspace;
        case VK_F1: return KeyCode::F1;
        case VK_F2: return KeyCode::F2;
        // ... F12
        case '0': return KeyCode::Zero;
        case '1': return KeyCode::One;
        // ... 9
        case VK_OEM_MINUS: return KeyCode::Minus;
        case VK_OEM_PLUS: return KeyCode::Equals;
        // ... 其他 OEM 键
        default: return KeyCode::None;
    }
}

6. 与引擎其他模块的集成

6.1 与 RHI/SwapChain 的集成

RHISwapChain::PollEvents() 需要调用 InputModule::PumpEvents()InputManager::Update():

// D3D12SwapChain.cpp
void D3D12SwapChain::PollEvents() {
    // 抽取 Windows 消息
    if (m_inputModule) {
        m_inputModule->PumpEvents();
    }
    
    // 处理其他 Windows 消息(关闭请求等)
    MSG msg;
    while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
        if (msg.message == WM_QUIT) {
            m_shouldClose = true;
            break;
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

// 在 GameLoop 中每帧调用
void GameLoop::Update(float deltaTime) {
    // 更新输入状态(计算 IsKeyPressed 等)
    Input::InputManager::Get().Update(deltaTime);
    
    // 更新场景和组件
    m_currentScene->Update(deltaTime);
}

6.2 与 UI 系统的集成

UI 组件通过订阅 InputManager 的事件来响应用户输入:

// ButtonComponent.cpp
void ButtonComponent::Update(float deltaTime) {
    if (!IsEnabled()) return;
    
    auto& input = Input::InputManager::Get();
    Vector2 mousePos = input.GetMousePosition();
    
    // 检测鼠标是否在按钮区域内
    if (IsPointInRect(mousePos, m_rect)) {
        // 检测悬停状态变化
        if (!m_isHovered) {
            m_isHovered = true;
            OnPointerEnter.Invoke();
        }
        
        // 检测点击
        if (input.IsMouseButtonClicked(Input::MouseButton::Left)) {
            OnClick.Invoke();
        }
        
        // 检测按下/释放
        if (input.IsMouseButtonDown(Input::MouseButton::Left)) {
            OnPointerDown.Invoke();
        } else if (m_wasMouseDown && !input.IsMouseButtonDown(Input::MouseButton::Left)) {
            OnPointerUp.Invoke();
        }
    } else if (m_isHovered) {
        m_isHovered = false;
        OnPointerExit.Invoke();
    }
    
    m_wasMouseDown = input.IsMouseButtonDown(Input::MouseButton::Left);
}

6.3 与场景生命周期的集成

// Scene.cpp
void Scene::Awake() {
    // 获取窗口句柄并初始化输入系统
    void* windowHandle = GetEngine()->GetWindowHandle();
    Input::InputManager::Get().Initialize(windowHandle);
}

void Scene::OnDestroy() {
    Input::InputManager::Get().Shutdown();
}

7. 使用示例

7.1 轮询模式(角色移动)

void PlayerController::Update(float deltaTime) {
    auto& input = Input::InputManager::Get();
    
    // 使用轴 (推荐方式,兼容手柄)
    float horizontal = input.GetAxis("Horizontal");
    float vertical = input.GetAxis("Vertical");
    
    m_velocity.x = horizontal * m_moveSpeed;
    m_velocity.z = vertical * m_moveSpeed;
    
    // 或者使用原始轴(无平滑)
    float rawH = input.GetAxisRaw("Horizontal");
    
    // 直接轮询也可以
    if (input.IsKeyDown(Input::KeyCode::W)) {
        m_velocity.z = 1.0f;
    }
}

7.2 事件模式UI 交互)

class MyUIButton : public Component {
    uint64_t m_clickHandlerId = 0;
    uint64_t m_hoverHandlerId = 0;
    
    void Awake() override {
        // 订阅鼠标按钮事件
        m_clickHandlerId = Input::InputManager::Get().OnMouseButton().Subscribe(
            [this](const Input::MouseButtonEvent& event) {
                if (event.type == Input::MouseButtonEvent::Pressed &&
                    event.button == Input::MouseButton::Left &&
                    IsPointInButton(event.position)) {
                    OnClicked();
                }
            }
        );
        
        // 订阅鼠标移动事件用于悬停检测
        m_hoverHandlerId = Input::InputManager::Get().OnMouseMove().Subscribe(
            [this](const Input::MouseMoveEvent& event) {
                bool isHovering = IsPointInButton(event.position);
                if (isHovering && !m_isHovered) {
                    m_isHovered = true;
                    OnPointerEnter.Invoke();
                } else if (!isHovering && m_isHovered) {
                    m_isHovered = false;
                    OnPointerExit.Invoke();
                }
            }
        );
    }
    
    void OnDestroy() override {
        Input::InputManager::Get().OnMouseButton().Unsubscribe(m_clickHandlerId);
        Input::InputManager::Get().OnMouseMove().Unsubscribe(m_hoverHandlerId);
    }
};

7.3 文本输入

void InputFieldComponent::Update(float deltaTime) {
    static uint64_t textHandlerId = 0;
    
    if (m_isFocused) {
        if (textHandlerId == 0) {
            textHandlerId = Input::InputManager::Get().OnTextInput().Subscribe(
                [this](const Input::TextInputEvent& event) {
                    if (event.character == '\b') {  // Backspace
                        if (!m_text.empty()) {
                            m_text.pop_back();
                        }
                    } else if (event.character == '\r') {  // Enter
                        OnSubmit.Invoke();
                    } else if (isprint(event.character)) {
                        if (m_characterLimit == 0 || m_text.length() < m_characterLimit) {
                            m_text += event.character;
                            OnValueChanged.Invoke(m_text);
                        }
                    }
                }
            );
        }
    } else if (textHandlerId != 0) {
        Input::InputManager::Get().OnTextInput().Unsubscribe(textHandlerId);
        textHandlerId = 0;
    }
}

7.4 射击游戏开火

void WeaponComponent::Update(float deltaTime) {
    auto& input = Input::InputManager::Get();
    
    // 方式1: 按钮按下检测
    if (input.GetButtonDown("Fire1")) {
        Fire();
    }
    
    // 方式2: 轮询检测(适合连发)
    if (input.GetButton("Fire1") && m_fireRateTimer <= 0) {
        Fire();
        m_fireRateTimer = m_fireRate;
    }
}

8. 实现计划

Phase 1: 核心输入系统

任务 文件 优先级
创建目录 engine/include/XCEngine/Input/ P0
实现 InputTypes.h KeyCode, MouseButton 枚举 P0
实现 InputEvent.h 事件结构体 P0
实现 InputAxis.h 输入轴定义 P1
实现 InputManager.h/cpp 单例管理器 P0
实现 InputModule.h 平台输入模块接口 P0
实现 WindowsInputModule.h/cpp Windows 平台实现 P0

Phase 2: 与 RHI 集成

任务 文件 优先级
修改 RHISwapChain 添加 InputModule 成员 P0
实现 D3D12SwapChain::PollEvents 填充消息泵逻辑 P0
实现 OpenGLSwapChain::PollEvents GL 消息处理 P1

Phase 3: 轴系统完善

任务 说明 优先级
默认轴配置 注册 Horizontal, Vertical 等默认轴 P1
轴平滑处理 GetAxis 的平滑插值 P2
配置文件支持 从 JSON/配置加载轴映射 P2

Phase 4: UI 输入支持

任务 说明 优先级
Canvas 射线检测 将屏幕坐标转换为 UI 元素 P1
Button 事件 OnClick, OnPointerDown/Up/Enter/Exit P1
Slider 拖拽 使用鼠标事件实现滑块拖拽 P1
InputField 文本输入 接收 TextInputEvent P1

9. 测试策略

9.1 单元测试

// test_input_manager.cpp
TEST(InputManager, KeyDownDetection) {
    InputManager::Get().Initialize(nullptr);
    
    // 模拟按下 W 键
    InputManager::Get().ProcessKeyDown(KeyCode::W, false);
    
    EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::W));
    EXPECT_TRUE(InputManager::Get().IsKeyPressed(KeyCode::W));  // 第一帧按下
    EXPECT_FALSE(InputManager::Get().IsKeyUp(KeyCode::W));
    
    // 模拟释放
    InputManager::Get().ProcessKeyUp(KeyCode::W);
    
    EXPECT_FALSE(InputManager::Get().IsKeyDown(KeyCode::W));
    EXPECT_TRUE(InputManager::Get().IsKeyUp(KeyCode::W));
    
    InputManager::Get().Shutdown();
}

TEST(InputManager, AxisRegistration) {
    InputManager::Get().Initialize(nullptr);
    
    InputManager::Get().RegisterAxis(InputAxis("TestAxis", KeyCode::W, KeyCode::S));
    
    EXPECT_EQ(InputManager::Get().GetAxis("TestAxis"), 0.0f);
    
    InputManager::Get().ProcessKeyDown(KeyCode::W, false);
    EXPECT_EQ(InputManager::Get().GetAxis("TestAxis"), 1.0f);
    
    InputManager::Get().ProcessKeyUp(KeyCode::W);
    InputManager::Get().ProcessKeyDown(KeyCode::S, false);
    EXPECT_EQ(InputManager::Get().GetAxis("TestAxis"), -1.0f);
    
    InputManager::Get().Shutdown();
}

TEST(InputManager, MousePositionTracking) {
    InputManager::Get().Initialize(nullptr);
    
    InputManager::Get().ProcessMouseMove(100, 200, 0, 0);
    EXPECT_EQ(InputManager::Get().GetMousePosition(), Math::Vector2(100, 200));
    
    InputManager::Get().ProcessMouseMove(105, 205, 5, 5);
    EXPECT_EQ(InputManager::Get().GetMouseDelta(), Math::Vector2(5, 5));
    
    InputManager::Get().Shutdown();
}

9.2 集成测试

  • 测试输入系统与 SwapChain 的集成
  • 测试输入系统与 Scene 的集成
  • 测试 UI Button 点击响应

10. 文件清单

头文件

engine/include/XCEngine/Input/
├── InputTypes.h          # 48 行
├── InputEvent.h          # 78 行
├── InputAxis.h           # 45 行
├── InputManager.h        # 130 行
└── InputModule.h         # 20 行

engine/include/XCEngine/Platform/
├── PlatformTypes.h       # (新建)
├── Window.h              # (新建)
└── Windows/
    └── WindowsInputModule.h  # 55 行

源文件

engine/src/Input/
├── InputManager.cpp      # 180 行
└── Platform/
    └── Windows/
        └── WindowsInputModule.cpp  # 200 行

测试文件

tests/Input/
├── test_input_types.cpp      # KeyCode/MouseButton 枚举测试
├── test_input_event.cpp      # 事件结构体测试
├── test_input_axis.cpp       # 轴注册和值计算测试
└── test_input_manager.cpp    # 核心功能测试

11. 设计原则总结

  1. 复用 Core::Event - 使用现有的线程安全事件系统
  2. 平台抽象 - InputModule 接口隔离 Windows 消息处理
  3. 双模式支持 - 轮询 + 事件,兼顾性能和响应性
  4. 轴系统 - 参考 Unity支持键盘/手柄统一输入映射
  5. 单窗口 - 简化设计,满足当前需求
  6. UI 协同 - 为 Canvas/Button 提供完整的事件支持