196 lines
6.7 KiB
Markdown
196 lines
6.7 KiB
Markdown
# 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)
|