923 lines
26 KiB
Markdown
923 lines
26 KiB
Markdown
|
|
# 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`)
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
#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`)
|
|||
|
|
|
|||
|
|
```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 输入轴定义 (`InputAxis.h`)
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
#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`)
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
#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`)
|
|||
|
|
|
|||
|
|
```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.6 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;
|
|||
|
|
|
|||
|
|
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 轴值计算
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
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 默认轴配置
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
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 映射
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
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()`:
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
// 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` 的事件来响应用户输入:
|
|||
|
|
|
|||
|
|
```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 (!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 与场景生命周期的集成
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
// Scene.cpp
|
|||
|
|
void Scene::Awake() {
|
|||
|
|
// 获取窗口句柄并初始化输入系统
|
|||
|
|
void* windowHandle = GetEngine()->GetWindowHandle();
|
|||
|
|
Input::InputManager::Get().Initialize(windowHandle);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Scene::OnDestroy() {
|
|||
|
|
Input::InputManager::Get().Shutdown();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 使用示例
|
|||
|
|
|
|||
|
|
### 7.1 轮询模式(角色移动)
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
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 交互)
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
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 文本输入
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
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 射击游戏开火
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
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 单元测试
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
// 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 提供完整的事件支持
|