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

6.7 KiB
Raw Blame History

Game View Runtime Input Bridge

这篇 guide 说明当前 Editor 里 GameViewPanel -> EventBus -> PlaySessionController -> InputManager 这条运行时输入桥接链路。

它不是抽象设计图,而是基于当前 GameViewPanel.cppPlaySessionController.cpptests/Editor/test_play_session_controller.cpp 的真实实现整理出来的工作流说明。

参与者

链路总览

当前链路固定按下面的顺序工作:

  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() 的第一道判断是:

if (!m_runtimeLoop.IsRunning()) {
    return;
}

所以即使 GameViewPanel 继续每帧发事件:

  • 只要当前不在 Play / Paused 运行态
  • 这些事件都不会影响运行时 InputManager

这也是为什么测试里要单独覆盖“非 Play mode 不驱动输入”。

第五步ApplyGameViewInputFrame 如何把快照变成标准输入流

1. 先推进输入帧边界

函数一进入就先调用:

InputManager::Get().Update(deltaTime);

这一步会清理上一帧的 pressed / released / scroll / delta 这类瞬时状态。

2. 选择本帧输入来源

  • 如果本帧收到了新的 GameViewInputFrameEvent,就使用 m_pendingGameViewInput
  • 否则回退到空事件

随后会把 m_hasPendingGameViewInput 重新清成 false

3. 计算输入是否激活

当前规则是:

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 当前直接覆盖了两条关键规则:

  • 非 Play mode 下,GameViewInputFrameEvent 不会驱动 InputManager
  • Play mode 下,输入会被桥接,并在空事件后释放

当前实现边界

  • 当前桥接只处理 GameViewInputFrameEvent,不是平台原始输入直通。
  • 当前键盘映射只覆盖 GameViewPanel.cpp 明确列出的 ImGuiKey -> KeyCode 子集。
  • 当前鼠标按钮只采集 Left / Right / Middle。

相关文档