- 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.
547 lines
14 KiB
Markdown
547 lines
14 KiB
Markdown
# 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`)
|
||
|
||
```cpp
|
||
#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`)
|
||
|
||
```cpp
|
||
#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`)
|
||
|
||
```cpp
|
||
#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`)
|
||
|
||
```cpp
|
||
#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`)
|
||
|
||
```cpp
|
||
#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()`:
|
||
|
||
```cpp
|
||
// 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` 的事件来响应用户输入:
|
||
|
||
```cpp
|
||
// 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 与场景生命周期的集成
|
||
|
||
```cpp
|
||
// 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 的映射
|
||
|
||
```cpp
|
||
// 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 轮询模式(角色移动)
|
||
|
||
```cpp
|
||
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 交互)
|
||
|
||
```cpp
|
||
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 文本输入
|
||
|
||
```cpp
|
||
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 现有的组件模式和事件系统。
|