Files
XCEngine/docs/used/XCEngine输入系统设计.md
ssdfasd 16e2065c6c Unified logging: Replace LogSystem with EditorConsoleSink
- Created EditorConsoleSink (implements ILogSink interface)
- EditorConsoleSink stores logs in memory buffer (max 1000 entries)
- Added to Debug::Logger in Application::Initialize()
- ConsolePanel now reads from EditorConsoleSink via static GetInstance()
- Removed separate LogSystem singleton
- Removed editor/src/Core/LogEntry.h (no longer needed)

Now Editor and Engine share the same Debug::Logger, with ConsolePanel
displaying logs via EditorConsoleSink.
2026-03-25 16:13:02 +08:00

14 KiB
Raw Blame History

XCEngine 输入系统设计与实现

文档信息

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

1. 概述

1.1 设计目标

XCEngine 输入系统需要提供:

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

1.2 当前状态分析

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

2. 架构设计

2.1 模块结构

engine/include/XCEngine/
├── Core/
│   └── Event.h              # 已有,复用
├── Input/
│   ├── InputTypes.h         # 枚举和结构体定义
│   ├── InputEvent.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. 与现有引擎组件集成

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

3. 详细设计

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

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

namespace XCEngine {
namespace Input {

enum class KeyCode : uint8 {
    None = 0,
    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 = 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
};

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

enum class JoystickButton : uint8 {
    Button0 = 0,
    Button1 = 1,
    Button2 = 2,
    // ... Xbox/PlayStation 标准按钮
};

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 输入管理器 (InputManager.h)

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

namespace XCEngine {
namespace Input {

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

    void Initialize(void* platformWindowHandle);
    void Shutdown();
    void Update();  // 每帧调用,更新输入状态

    // ============ 轮询接口 ============
    
    // 键盘
    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 GetJoystickCount() const;
    bool IsJoystickConnected(int joystick) const;
    Math::Vector2 GetJoystickAxis(int joystick, JoystickAxis axis) const;
    bool IsJoystickButtonDown(int joystick, JoystickButton button) const;

    // ============ 事件接口 ============
    
    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;
    
    // 事件
    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.4 平台输入模块接口 (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.5 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;
    
    // 供 Window 调用的消息处理
    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);
    
    HWND m_hwnd = nullptr;
    bool m_captureMouse = false;
};

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

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

4.1 与 RHI/SwapChain 的集成

RHISwapChain::PollEvents() 需要调用 InputManager::Update() 和平台输入模块的 PumpEvents():

// D3D12SwapChain.cpp
void D3D12SwapChain::PollEvents() {
    // 抽取 Windows 消息
    if (m_inputModule) {
        m_inputModule->PumpEvents();
    }
    
    // 更新输入管理器状态
    Input::InputManager::Get().Update();
    
    // 处理关闭请求
    MSG msg;
    if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
        if (msg.message == WM_QUIT) {
            m_shouldClose = true;
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

4.2 与 UI 系统的集成

UI 组件 (Button, Slider 等) 通过订阅 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 (input.IsMouseButtonClicked(Input::MouseButton::Left)) {
            OnClick.Invoke();
        }
    }
}

4.3 与场景生命周期的集成

// Scene.cpp
void Scene::Awake() {
    // 初始化输入系统
    Input::InputManager::Get().Initialize(m_windowHandle);
}

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

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

Windows Message Engine Event
WM_KEYDOWN InputManager::ProcessKeyDown
WM_KEYUP InputManager::ProcessKeyUp
WM_CHAR InputManager::ProcessTextInput
WM_MOUSEMOVE InputManager::ProcessMouseMove
WM_LBUTTONDOWN/RBUTTONDOWN/MBUTTONDOWN InputManager::ProcessMouseButton
WM_LBUTTONUP/RBUTTONUP/MBUTTONUP InputManager::ProcessMouseButton
WM_MOUSEWHEEL InputManager::ProcessMouseWheel

5.1 Windows VK 码到 KeyCode 的映射

// WindowsVKToKeyCode 映射表
static const KeyCode VK_TO_KEYCODE[] = {
    // VK 0-29 对应 KeyCode A-Z
    KeyCode::A, KeyCode::B, KeyCode::C, KeyCode::D, KeyCode::E,
    KeyCode::F, KeyCode::G, KeyCode::H, KeyCode::I, KeyCode::J,
    KeyCode::K, KeyCode::L, KeyCode::M, KeyCode::N, KeyCode::O,
    KeyCode::P, KeyCode::Q, KeyCode::R, KeyCode::S, KeyCode::T,
    KeyCode::U, KeyCode::V, KeyCode::W, KeyCode::X, KeyCode::Y,
    KeyCode::Z,
    // ... 其他映射
};

6. 实现计划

Phase 1: 核心输入系统

任务 文件 说明
创建 Input 模块目录 engine/include/XCEngine/Input/
实现 InputTypes.h KeyCode, MouseButton 等枚举
实现 InputEvent.h 事件结构体
实现 InputManager.h/cpp 单例管理器
实现 WindowsInputModule.h/cpp Windows 平台实现

Phase 2: 与 RHI 集成

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

Phase 3: UI 输入支持

任务 说明
实现 Canvas 组件的射线检测 将屏幕坐标转换为 UI 元素
Button/Slider 事件集成 使用 InputManager 事件

7. 使用示例

7.1 轮询模式(角色移动)

void PlayerController::Update(float deltaTime) {
    auto& input = Input::InputManager::Get();
    
    if (input.IsKeyDown(Input::KeyCode::W)) {
        m_velocity.z = 1.0f;
    } else if (input.IsKeyDown(Input::KeyCode::S)) {
        m_velocity.z = -1.0f;
    }
    
    if (input.IsKeyDown(Input::KeyCode::A)) {
        m_velocity.x = -1.0f;
    } else if (input.IsKeyDown(Input::KeyCode::D)) {
        m_velocity.x = 1.0f;
    }
}

7.2 事件模式UI 交互)

class MyUIButton : public Component {
    uint64_t m_clickHandlerId = 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();
                }
            }
        );
    }
    
    void OnDestroy() override {
        Input::InputManager::Get().OnMouseButton().Unsubscribe(m_clickHandlerId);
    }
};

7.3 文本输入

void InputFieldComponent::Update(float deltaTime) {
    auto& input = Input::InputManager::Get();
    
    uint64_t textHandlerId = input.OnTextInput().Subscribe(
        [this](const Input::TextInputEvent& event) {
            if (m_isFocused) {
                m_text += event.character;
                OnValueChanged.Invoke(m_text);
            }
        }
    );
    
    // 清理
    input.OnTextInput().Unsubscribe(textHandlerId);
}

8. 待讨论问题

  1. 多窗口支持: 引擎是否需要支持多窗口?如果是,输入系统如何路由事件到正确的窗口?

  2. 手柄/游戏手柄支持: 是否需要实现 XInput 支持?这会增加复杂性。

  3. 原始输入 vs 输入模式: Unity 有 "Input.GetAxis" vs "Input.GetButtonDown"。是否需要实现类似的轴概念?

  4. IME/输入法支持: 对于中文、日文输入,是否需要特殊处理?

  5. 平台模块位置: InputModule 放在 Input/ 还是 Platform/ 命名空间下?


9. 结论

XCEngine 输入系统设计遵循以下核心原则:

  1. 复用现有 Core::Event - 不重复造轮子
  2. 平台抽象 - 通过 InputModule 接口隔离平台差异
  3. 双模式支持 - 轮询 + 事件,兼顾性能和响应性
  4. 与 UI 架构协同 - 为 Canvas/Button 提供必要的事件支持

该设计参考了 Unity 的 Input 系统架构,同时适配了 XCEngine 现有的组件模式和事件系统。