Files
XCEngine/docs/api/_guides/Input/Input-Flow-and-Frame-Semantics.md

304 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Input Flow And Frame Semantics
## 这篇指南解决什么问题
这篇文档不是“列 API 名称”,而是解释 XCEngine 当前输入系统到底按什么规则工作。它直接基于当前这些实现和测试:
- `engine/src/Input/InputManager.cpp`
- `engine/include/XCEngine/Input/InputManager.h`
- `tests/Input/test_input_manager.cpp`
如果你在游戏层、编辑器运行态桥接、脚本绑定或平台输入后端里使用 `InputManager`,最重要的不是记住方法名,而是先建立一致的语义模型。
## 设计理念
当前输入系统刻意做成两层:
- `InputModule`
- 负责接平台消息,把 `WM_KEYDOWN``WM_MOUSEMOVE` 这类原始系统消息翻译成引擎调用
- `InputManager`
- 负责保存状态、提供轮询 API、同步分发事件
这种设计接近商业引擎里常见的“平台适配层 + 游戏语义层”分工。好处是:
- 游戏逻辑和编辑器逻辑不需要直接依赖 Win32 之类的平台细节
- 平台后端可以替换,输入查询接口可以保持稳定
- 轮询和事件可以共享同一份底层状态,不需要上层再维护第二套缓存
从使用风格上看,它也明显借鉴了 Unity 旧版 `Input Manager` 的思想:
- 支持具名轴,如 `Horizontal``Vertical`
- 支持具名按钮,如 `Jump``Fire1`
-`GetAxis()``GetButton()` 一类游戏层查询接口为中心
但它当前还是一个轻量级、同步、帧驱动的输入管理器,不是完整的设备抽象框架。
## 输入流到底怎么走
当前一帧内的典型流向是:
1. 平台窗口系统产生键盘、鼠标消息。
2. 平台后端把消息翻译成 `ProcessKeyDown()``ProcessKeyUp()``ProcessMouseMove()``ProcessMouseButton()``ProcessMouseWheel()` 等调用。
3. `InputManager` 立即更新内部缓存,并同步触发 `OnKeyEvent()``OnMouseMove()` 等事件。
4. 游戏逻辑、编辑器运行态桥接或脚本绑定在当前帧查询 `IsKeyDown()``GetAxis()``GetButtonDown()` 等 API。
5. 帧末或统一帧边界调用 `Update()`,清空瞬时输入缓存,准备下一帧。
这说明一个关键点:`Update()` 不是“收集输入”,而是“推进帧边界并清空瞬时状态”。
## 为什么同时提供事件和轮询
商业级引擎通常不会强迫所有系统只用一种输入风格。XCEngine 当前的取舍也是一样:
- 事件更适合 UI、文本输入、编辑器面板、平台桥接
- 轮询更适合角色移动、相机控制、游戏规则判断
`InputManager` 把两者放在同一个状态中心里,能减少几类常见问题:
- 上层不需要再手写“本帧按下 / 本帧释放”缓存
- 事件订阅者和轮询调用者看到的是同一组真实状态
- 平台输入桥接和游戏输入消费不需要重复实现状态机
## 帧边界语义
当前实现里,`Update()` 会清理这些瞬时状态:
- `m_keyDownThisFrame`
- `m_keyUpThisFrame`
- `m_mouseButtonDownThisFrame`
- `m_mouseButtonUpThisFrame`
- `m_mouseDelta`
- `m_mouseScrollDelta`
因此下面这些接口都天然是“本帧有效,过帧清空”:
- `IsKeyPressed()`
- `IsKeyReleased()`
- `IsMouseButtonClicked()`
- `IsMouseButtonReleased()`
- `GetButtonDown()`
- `GetButtonUp()`
- `IsAnyKeyPressed()`
- `GetMouseDelta()`
- `GetMouseScrollDelta()`
推荐实践只有三条,但都很重要:
1. 每帧只调用一次 `Update()`
2. 保持调用时机稳定,不要一会儿逻辑前一会儿逻辑后
3. 先消费瞬时输入,再执行 `Update()`
## 四类最常用的输入语义
先按语义理解 API比按方法名死记更可靠。
| 语义 | 典型 API | 当前真正含义 |
|------|------|------|
| 按住态 | `IsKeyDown()``IsMouseButtonDown()``GetButton()``GetAxis()``GetAxisRaw()``IsAnyKeyDown()` | 当前是否仍处于按住状态 |
| 抬起态 | `IsKeyUp()``IsMouseButtonUp()` | 当前没有按住,不包含“本帧刚释放”含义 |
| 按下边沿 | `IsKeyPressed()``IsMouseButtonClicked()``GetButtonDown()``IsAnyKeyPressed()` | 本帧出现按下消息 |
| 释放边沿 | `IsKeyReleased()``IsMouseButtonReleased()``GetButtonUp()` | 本帧出现释放消息 |
这也是很多团队最容易踩错的地方:`Up``Released` 不是同义词。
## `Up` 和 `Released` 为什么必须分开
这是当前模块最需要讲透的概念。
- `IsKeyUp()` / `IsMouseButtonUp()`
- 只表示“现在没有按住”
- 本质上等价于 `!IsKeyDown()` / `!IsMouseButtonDown()`
- 即使未初始化或索引越界,当前实现里也会表现为 `true`
- `IsKeyReleased()` / `IsMouseButtonReleased()`
- 只表示“这一帧刚收到释放消息”
-`ProcessKeyUp()` / `ProcessMouseButton(..., false, ...)` 写入
- 会在下一次 `Update()` 时被清空
为什么商业引擎通常都要保留这两套语义:
- “当前没按住”适合状态判断
- “这一帧刚释放”适合一次性触发逻辑
例如“角色是否仍在蓄力”更像 `IsKeyDown()` / `IsKeyUp()` 问题;“松开蓄力键时发射一次”则应该看 `IsKeyReleased()`
## `Pressed` 当前到底是不是“首次按下”
不是严格意义上的“首次物理按下”。
源码里 `ProcessKeyDown()` 的当前行为是:
- 无论 `repeat``true` 还是 `false`
- 都会把 `m_keyDown[index] = true`
- 也都会把 `m_keyDownThisFrame[index] = true`
这带来一个很关键的结果:
- `IsKeyPressed()`
- `GetButtonDown()`
- `IsAnyKeyPressed()`
它们当前表达的更准确含义是“这一帧收到了按下消息”,而不是“从这次物理按下开始到松开之前只触发一次”。
如果平台在后续帧继续发送自动重复 `KeyDown`
- `OnKeyEvent()` 会把事件类型标成 `KeyEvent::Repeat`
- 但按下边沿缓存仍会被写入
- 所以上述查询接口仍可能在这些帧再次返回 `true`
如果业务真的需要“严格首次按下”,需要在平台层或更高层自己过滤 repeat。
## 轴和按钮映射的真实行为
### 逻辑轴
当前 `RegisterAxis()` 把一个名字映射到“正方向键 + 负方向键”。
`GetAxis()``GetAxisRaw()` 目前都直接读 `IsKeyDown()`
- 正方向键按住时加 `1.0f`
- 负方向键按住时减 `1.0f`
- 同时按下则抵消为 `0.0f`
当前两者没有行为差异。它们都:
- 不读取 `InputAxis::m_value`
- 不做平滑、重力、灵敏度或死区处理
- 不会因为只是“本帧刚按下”而产生非零值
这和很多商业引擎早期的“raw axis / smoothed axis”区分还不一样当前实现更接近“两个名字不同但逻辑相同的持续态接口”。
### 逻辑按钮
当前 `RegisterButton()` 只是把名字映射到一个 `KeyCode`
- `GetButton()` 转发到 `IsKeyDown()`
- `GetButtonDown()` 转发到 `IsKeyPressed()`
- `GetButtonUp()` 转发到 `IsKeyReleased()`
因此按钮系统当前不是独立状态机,而只是对键盘查询的命名包装。
## 和 Unity 的关系
如果你熟悉 Unity这里最容易建立直觉但也最容易因为“看起来像”而误判。
相似点:
- 都有具名轴和具名按钮
- 都强调游戏层直接查询,而不是让业务处理原始平台消息
- 都适合快速搭建角色控制、相机控制和简单交互
不同点:
- 当前没有 Unity 旧版 Input Manager 里的 gravity、sensitivity、dead zone、snap 等参数
- `GetAxisRaw()` 目前并不比 `GetAxis()` 更“底层”,两者等价
- 默认注册的 `Mouse X` / `Mouse Y` 还没有绑定到鼠标移动增量
- `GetButtonUp()` 是释放边沿,不是“当前按钮没按着就算 up”
所以当前更适合把它理解成“Unity 风格接口外观 + XCEngine 当前真实实现语义”,而不是直接按 Unity 经验套用全部行为。
## `any-key` 目前到底统计什么
当前 `any-key` 会把鼠标按钮也算进去。
- `IsAnyKeyDown()`
- 扫描 `m_keyDown``m_mouseButtonDown`
- `IsAnyKeyPressed()`
- 扫描 `m_keyDownThisFrame``m_mouseButtonDownThisFrame`
它目前不包含:
- 触摸
- 滚轮
- 文本输入
- 摇杆或手柄输入
同时,`IsAnyKeyPressed()` 也继承键盘 repeat 语义。只要平台后端在某一帧再次调用 `ProcessKeyDown()`,它就可能再次成立。
## 默认映射与当前落地程度
初始化后默认注册这些逻辑项:
-
- `Horizontal`: `D / A`
- `Vertical`: `W / S`
- `Mouse X`: `None / None`
- `Mouse Y`: `None / None`
- 按钮
- `Jump`: `Space`
- `Fire1`: `LeftCtrl`
- `Fire2`: `LeftAlt`
- `Fire3`: `LeftShift`
这说明当前默认配置的实际可用性是分层的:
- `Horizontal` / `Vertical` 可以直接驱动基础移动逻辑
- `Jump` / `Fire1-3` 可以直接用于具名按钮查询
- `Mouse X` / `Mouse Y` 当前更像预留命名位,而不是已经接通的鼠标轴
## 平台桥接为什么值得保留独立层
当前 Windows 路径里,`WindowsInputModule::HandleMessage()` 会把 Win32 消息翻译成 `InputManager::Process*()` 调用。
这种分层设计的直接收益是:
- 游戏层不用理解平台消息细节
- 测试可以绕开平台窗口系统,直接驱动 `InputManager::Process*()` 来验证行为
- 将来替换平台层时,输入消费方可以尽量保持不变
这也是为什么现有测试大多直接命中 `InputManager`,而不是依赖完整窗口消息环境。
## 当前版本最该知道的限制
- `KeyCode` 底层值当前存在重复项,部分按键会共享状态槽。
- Windows 路径当前不能准确区分左右 Ctrl / Alt / Shift。
- 触摸与摇杆接口大多还是预留位。
- 事件回调同步执行,不是异步消息队列。
- `Shutdown()` 会清空状态和映射,但不会清空事件监听器。
- `ClearAxes()` 当前会同时清空轴和按钮映射。
## 推荐接入方式
初始化阶段:
```cpp
using namespace XCEngine::Input;
InputManager::Get().Initialize(windowHandle);
```
平台消息阶段:
```cpp
windowsInputModule.HandleMessage(hwnd, msg, wParam, lParam);
```
游戏逻辑阶段:
```cpp
if (InputManager::Get().GetButton("Jump")) {
// 持续按住态
}
if (InputManager::Get().GetButtonDown("Jump")) {
// 本帧按下边沿
}
if (InputManager::Get().GetButtonUp("Jump")) {
// 本帧释放边沿
}
```
帧边界阶段:
```cpp
InputManager::Get().Update(deltaTime);
```
如果你把 `Update()` 放错位置,或者在同一帧里调用多次,最先出问题的通常就是 `Pressed` / `Released` / `MouseDelta` 这些瞬时语义。
## 相关 API
- [Input](../../XCEngine/Input/Input.md)
- [InputManager](../../XCEngine/Input/InputManager/InputManager.md)
- [InputModule](../../XCEngine/Input/InputModule/InputModule.md)
- [InputTypes](../../XCEngine/Input/InputTypes/InputTypes.md)