【模块实现 04】输入拦截系统:菜单显示时 “接管” 键鼠的实现逻辑
·
菜单能渲染出来只是第一步,能不能正常交互才是关键。注入项目的输入处理很特殊:我们不能抢游戏的输入,也不能让游戏的输入穿透到菜单上,必须做到 “菜单开的时候,键鼠归菜单;菜单关的时候,输入完全还给游戏,游戏感知不到任何异常”。
一、输入系统的核心目标
我给输入系统定了几个核心目标:
- 无感注入:菜单隐藏时,绝对不能影响游戏的键鼠输入,不能出现按键失灵、鼠标卡顿、输入延迟;
- 精准接管:菜单呼出后,所有键鼠输入只交给 ImGui,游戏收不到任何输入,避免操作菜单时角色乱动、视角乱转;
- 多输入源兼容:要兼容窗口消息、原始输入、轮询读取等多种游戏输入方式,不能只拦一种,不然游戏还是能收到输入;
- 低性能开销:输入处理是每帧都要走的,不能有太大的性能损耗。
二、整体架构设计
输入系统我封装成了独立的OptiInput命名空间,整体流程分成三阶段:
- 帧开始(BeginFrame):校验目标窗口,更新窗口焦点,轮询输入状态,处理原始输入消息;
- 输入投喂(FeedImGui):把收集到的键鼠状态、按键事件、文本输入同步给 ImGui;
- 帧结束(EndFrame):根据菜单显隐状态,更新输入拦截策略,清空瞬态输入标记。
三、核心实现细节
1. 多输入源兜底机制
不同游戏的输入方式不一样,有的靠窗口消息,有的用 RawInput,还有的直接轮询 GetAsyncKeyState。只拦一种的话,其他方式游戏还是能读到输入,所以我做了多层拦截兜底:
- 窗口消息层:通过子类化窗口过程,拦截 WM_KEYDOWN、WM_MOUSEMOVE 等消息,菜单可见时直接吃掉不传给游戏原窗口过程;
- API Hook 层:Hook GetAsyncKeyState、GetKeyState、SendInput 等输入 API,菜单可见时返回 “无按键按下” 的状态;
- 原始输入层:拦截 RawInput 消息,菜单可见时过滤掉键鼠的原始输入数据;
- 轮询兜底层:如果前面的方式都没拿到输入,就每帧主动轮询键鼠状态,作为给 ImGui 的输入兜底。
2. 输入拦截策略
拦截不是简单的全拦,而是分粒度控制:鼠标、键盘、光标可以单独控制,菜单可见时三个都拦,隐藏时都放开。
// 摘自 input_system.cpp
bool ShouldBlockKeyboardInputLocked()
{
return bypassHookDepth == 0 && _state.MenuVisible && _state.BlockKeyboard;
}
bool ShouldBlockMouseInputLocked()
{
return bypassHookDepth == 0 && _state.MenuVisible && _state.BlockMouse;
}
这里加了个bypassHookDepth的绕过计数,因为我们自己的代码有时候也需要调用真实的输入 API(比如轮询输入状态),这时候要临时绕过拦截,不然自己也读不到真实输入了。
3. ImGui 输入投喂
收集到的输入状态不能直接扔给 ImGui,要做格式转换,按 ImGui 的事件规范投喂:
- 鼠标位置转换成窗口客户区坐标;
- 按键事件转换成 ImGui 的键位枚举,左右 Ctrl、Shift、Alt 单独处理;
- 鼠标滚轮、文本输入单独处理。 核心的投喂逻辑如下:
// 摘自 input_system.cpp
void FeedImGui(bool menuVisible)
{
ImGuiIO& io = ImGui::GetIO();
// 菜单隐藏时清空输入,不给ImGui传任何事件
if (!menuVisible)
{
io.ClearEventsQueue();
io.ClearInputKeys();
io.ClearInputMouse();
return;
}
// 同步焦点状态
io.AddFocusEvent(_state.Focused);
if (!_state.Focused)
{
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
return;
}
// 同步修饰键
io.AddKeyEvent(ImGuiMod_Ctrl, ctrlDown);
io.AddKeyEvent(ImGuiMod_Shift, shiftDown);
io.AddKeyEvent(ImGuiMod_Alt, altDown);
// 同步鼠标位置
io.AddMousePosEvent(static_cast<float>(_state.MouseClientPos.x),
static_cast<float>(_state.MouseClientPos.y));
// 同步鼠标按键
io.AddMouseButtonEvent(0, _state.MouseButtons[0].Down);
io.AddMouseButtonEvent(1, _state.MouseButtons[1].Down);
// ... 省略键盘按键、滚轮、文本输入同步
}
4. 虚拟鼠标适配
针对一些特殊场景(比如外部覆盖层、游戏强制居中鼠标),还做了虚拟鼠标模式:不用真实的系统光标,完全靠 ImGui 绘制软件光标,通过鼠标增量计算虚拟光标位置。这种模式下完全不依赖系统光标,就算游戏隐藏光标、强制居中,菜单也能正常操作。
四、踩过的坑
- 输入穿透:最开始只拦了窗口消息,结果游戏用 GetAsyncKeyState 还是能读到按键,操作菜单时角色还在动。后来补了 API Hook 层和 RawInput 拦截,多层兜底后彻底解决。
- 自己读输入也被拦了:一开始没做绕过机制,我们自己轮询按键状态的时候,也被自己的 Hook 拦住了,永远读不到按键。加了 bypass 绕过计数后,自己调用真实 API 前增加深度,调用完减回去,完美解决。
- 菜单关闭后按键卡住:菜单关闭时如果正好有按键按下,游戏收不到抬起事件,就会出现 “一直往前走”“视角一直转” 的卡住问题。现在菜单关闭时会清空所有按键状态,同时给游戏补一次按键抬起的消息。
- 多线程输入冲突:游戏的输入线程和渲染线程不是同一个,一开始没加锁,输入状态读写冲突,偶尔会出现按键乱跳。给所有输入状态访问都加了互斥锁后解决。
五、效果验证
现在菜单的交互已经完全正常:
- 菜单隐藏时,游戏操作没有任何异常,测试了多款游戏都没察觉到输入被注入过;
- 菜单呼出后,键鼠操作完全作用在菜单上,游戏不会收到任何输入,不会出现操作穿透;
- 快捷键呼出隐藏、手柄导航、文本输入都能正常工作。 下一篇就来讲讲项目的核心业务逻辑:NvAPI 伪装与超分后端切换,怎么实现让只支持 DLSS 的游戏用上 FSR。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)