Files
XCEngine/docs/api/_guides/Editor/Game-View-Runtime-Input-Bridge.md

196 lines
6.7 KiB
Markdown
Raw Permalink 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.
# Game View Runtime Input Bridge
这篇 guide 说明当前 Editor 里 `GameViewPanel -> EventBus -> PlaySessionController -> InputManager` 这条运行时输入桥接链路。
它不是抽象设计图,而是基于当前 `GameViewPanel.cpp``PlaySessionController.cpp``tests/Editor/test_play_session_controller.cpp` 的真实实现整理出来的工作流说明。
## 参与者
- [GameViewPanel](../../XCEngine/Editor/panels/GameViewPanel/GameViewPanel.md)
- [EditorEvents / GameViewInputFrameEvent](../../XCEngine/Editor/Core/EditorEvents/EditorEvents.md)
- [EventBus](../../XCEngine/Editor/Core/EventBus/EventBus.md)
- [PlaySessionController](../../XCEngine/Editor/Core/PlaySessionController/PlaySessionController.md)
- [InputManager](../../XCEngine/Input/InputManager/InputManager.md)
## 链路总览
当前链路固定按下面的顺序工作:
1. `GameViewPanel::Render()` 采集当前帧 ImGui 键鼠状态。
2. 面板把这些状态封装成 `GameViewInputFrameEvent`
3. 事件通过 `EventBus` 发布。
4. `PlaySessionController::Attach()` 期间注册的订阅回调把它缓存到 `m_pendingGameViewInput`
5. 只有 play session 正在运行时,`PlaySessionController::Update()` 才会调用 `ApplyGameViewInputFrame(deltaTime)`
6. `ApplyGameViewInputFrame(...)` 先推进 `InputManager` 的帧边界,再把快照差异翻译成标准 `ProcessKey* / ProcessMouse*` 调用。
所以运行时输入不是 UI 层直接写进 `InputManager`,而是经过一层显式桥接。
## 第一步GameViewPanel 采样输入
`GameViewPanel` 每一帧都会发布一次事件,不依赖“本帧有没有输入变化”。
### 面板关闭
如果面板当前没打开,`Render()` 会立即发布一个空的 `GameViewInputFrameEvent{}`
这不是多余操作,而是为了通知桥接层:
- 这一帧已经没有有效 Game View 输入
- 之前保持的运行时按键状态应该在下一帧被释放
### 视口打开
视口打开时,`BuildGameViewInputFrame(...)` 会使用 `ViewportPanelContentResult` 构建快照:
- `hovered`
- `focused`
- `mousePosition`
- `mouseDelta`
- `mouseWheel`
- `keyDown`
- `mouseButtonDown`
这里有三个实现细节需要特别注意:
1. `mousePosition` 是相对 Game View 左上角的局部坐标,不是整窗口坐标。
2. `mouseWheel` 只有在 `hovered == true` 时才保留。
3. 只有 `hovered || focused` 时,面板才会填充键盘和鼠标按钮按住态。
## 第二步EventBus 只传快照,不做运行时逻辑
`GameViewInputFrameEvent` 是一个轻量状态结构,不带成员函数,也不在事件总线层做任何特殊处理。
`EventBus` 当前只负责把这份快照广播给订阅者。
这意味着:
- `GameViewPanel` 不需要知道谁在消费事件
- `PlaySessionController` 也不需要依赖面板实例
## 第三步PlaySessionController 缓存 pending 输入
`PlaySessionController::Attach()` 订阅 `GameViewInputFrameEvent` 之后,当前回调只做两件事:
- `m_pendingGameViewInput = event`
- `m_hasPendingGameViewInput = true`
它不会在事件回调里立刻调用 `InputManager`
真正的桥接被推迟到 `Update()`,这样运行时输入的帧边界就能和 `RuntimeLoop` 保持一致。
## 第四步:只有 play session 运行时才会真正驱动 InputManager
`PlaySessionController::Update()` 的第一道判断是:
```cpp
if (!m_runtimeLoop.IsRunning()) {
return;
}
```
所以即使 `GameViewPanel` 继续每帧发事件:
- 只要当前不在 Play / Paused 运行态
- 这些事件都不会影响运行时 `InputManager`
这也是为什么测试里要单独覆盖“非 Play mode 不驱动输入”。
## 第五步ApplyGameViewInputFrame 如何把快照变成标准输入流
### 1. 先推进输入帧边界
函数一进入就先调用:
```cpp
InputManager::Get().Update(deltaTime);
```
这一步会清理上一帧的 pressed / released / scroll / delta 这类瞬时状态。
### 2. 选择本帧输入来源
- 如果本帧收到了新的 `GameViewInputFrameEvent`,就使用 `m_pendingGameViewInput`
- 否则回退到空事件
随后会把 `m_hasPendingGameViewInput` 重新清成 `false`
### 3. 计算输入是否激活
当前规则是:
```cpp
input.hovered || input.focused
```
也就是说:
- 鼠标悬停时,输入激活
- 鼠标暂时移出但视口仍持有焦点时,输入仍激活
- 两者都失去时,桥接层会把输入视为失活
### 4. 用 diff 方式补发 down / up
桥接层不会把整份快照“整体覆盖”进 `InputManager`
它会拿本帧 `input` 和上一帧 `m_appliedGameViewInput` 逐项比较:
- 键盘:`ProcessKeyDown(...)` / `ProcessKeyUp(...)`
- 鼠标按钮:`ProcessMouseButton(...)`
- 鼠标移动:`ProcessMouseMove(...)`
- 滚轮:`ProcessMouseWheel(...)`
这样运行时侧看到的仍是标准事件流,所以:
- `IsKeyPressed()`
- `IsKeyReleased()`
- `IsMouseButtonClicked()`
这些查询都还能按 `InputManager` 的既有语义工作。
### 5. 失活时自动释放
函数末尾会先把 `m_appliedGameViewInput` 清空;只有当 `inputActive == true` 时,才把它改成当前快照。
因此一旦:
- 面板关闭
- 视口没有被 hovered也没有被 focused
- 或者本帧没有新的有效事件
下一帧比较时,桥接层就会把上一帧保持的 down 状态补发成 up。
这就是“Game View 失活时自动释放运行时输入”的真正来源。
## 进入和退出 Play Mode 时为什么要重建输入桥
`PlaySessionController::StartPlay()` 会先:
- `ResetRuntimeInputBridge()`
- `InputManager::Shutdown()`
- `InputManager::Initialize(nullptr)`
`StopPlay()` 则会:
- `ResetRuntimeInputBridge()`
- `InputManager::Shutdown()`
这保证了两件事:
1. 编辑态不会把旧的运行时输入状态带进 play mode。
2. 退出 play mode 后,运行时输入也不会残留回编辑态。
## 当前测试覆盖
[tests/Editor/test_play_session_controller.cpp](D:/Xuanchi/Main/XCEngine/tests/Editor/test_play_session_controller.cpp) 当前直接覆盖了两条关键规则:
- 非 Play mode 下,`GameViewInputFrameEvent` 不会驱动 `InputManager`
- Play mode 下,输入会被桥接,并在空事件后释放
## 当前实现边界
- 当前桥接只处理 `GameViewInputFrameEvent`,不是平台原始输入直通。
- 当前键盘映射只覆盖 `GameViewPanel.cpp` 明确列出的 `ImGuiKey -> KeyCode` 子集。
- 当前鼠标按钮只采集 Left / Right / Middle。
## 相关文档
- [GameViewPanel](../../XCEngine/Editor/panels/GameViewPanel/GameViewPanel.md)
- [EditorEvents](../../XCEngine/Editor/Core/EditorEvents/EditorEvents.md)
- [PlaySessionController](../../XCEngine/Editor/Core/PlaySessionController/PlaySessionController.md)
- [Input Flow and Frame Semantics](../Input/Input-Flow-and-Frame-Semantics.md)