来源说明:AutoHotkey 论坛讨论 Understanding SendInput and keyboard hooks,并重新整理,本文重点放在游戏脚本里的按键发送机制。
游戏脚本里最常见的误判是:以为 SendInput 一定最快、一定最像真实按键、一定能绕开用户输入干扰。实际不是。游戏可能读取前台键盘消息、Raw Input、DirectInput 类状态,也可能过滤注入按键;同时 AHK 自己的热键、热字符串、条件热键又可能安装低级键盘钩子,改变 SendInput 的行为。最后表现出来就是:按键漏发、原键穿透、连发节奏漂移、长按状态卡住、游戏里无效但记事本正常。
一、游戏接收按键,不只看 Send 有没有发出去
从 Windows 输入链路看,一个物理按键大致会经过键盘硬件、驱动层、低级键盘钩子、热键匹配、Raw Input、窗口键盘消息,最后才到应用自己的处理逻辑。SendInput 不是从键盘硬件发出的,它是把模拟输入插入到系统输入流里,位置大约在驱动处理之后、低级钩子之前。
这对游戏脚本非常关键:游戏可能不只看 WM_KEYDOWN/WM_KEYUP,还可能看 Raw Input、DirectInput、设备扫描码、前后台焦点、权限等级,甚至区分 injected 标记。也就是说,AHK 侧“发送成功”和游戏侧“接受为有效操作”不是一回事。
- 普通窗口:多半能吃
Send、SendInput、SendEvent。 - 老游戏或普通前台游戏:可能吃
SendEvent,但节奏要调。 - 读取 Raw Input 的游戏:可能不理普通窗口消息,
ControlSend基本别指望。 - 对 injected 输入敏感的游戏:
SendInput发出去了,也可能被忽略。
二、SendInput 在游戏脚本里的真正优势
SendInput 的优势是批量、快速、按键间默认无延迟。在没有键盘钩子干扰时,它可以把一串输入一次性塞进输入流,用户真实输入通常不容易插进这串模拟输入中。这很适合短促、确定的组合键,例如释放技能、按菜单快捷键、输入少量文本。
#Requires AutoHotkey v1.1
#NoEnv
#SingleInstance Force
SendMode, Input
F1::
SendInput, {1}{2}{3}
return
但这只适合“短按序列”。如果你要做长按、循环按下/弹起、按住一个键映射成另一个键,或者依赖 GetKeyState("P") 判断物理状态,SendInput 反而可能成为问题来源。
三、低级键盘钩子会改变 SendInput 的行为
AHK 的复杂热键能力很多都依赖低级键盘钩子。比如 $、*、~ 热键,自定义组合键,按键释放热键,热字符串,复杂条件热键,#InstallKeybdHook,输入监听等。
根据社区讨论,AHK 调用 SendInput 前会做一些适配:
- 如果发现其它 AHK 脚本装了键盘钩子,
SendInput可能退化为类似SendEvent的发送方式,并使用接近SetKeyDelay, -1, 0的节奏。 - 如果没有其它 AHK 钩子,但当前脚本自己装了钩子,AHK 可能在
SendInput期间临时卸载自身钩子,发送完再装回去。 - 如果是输入法、键盘宏软件、驱动工具、监控工具等非 AHK 程序装的钩子,AHK 无法完整判断,只能照常发送,结果可能变慢、交织或泄漏。
对游戏脚本来说,最危险的是第二点:脚本为了让 SendInput 保持高速,会临时卸载自身钩子;但你的热键、物理按键状态跟踪、拦截逻辑,恰恰可能依赖这个钩子。
四、典型游戏坑:按住 Z 发 N,却漏出 Z
假设你想在游戏里按住 z 时,每隔一段时间按一次 n,并用 $z 防止自身发送再次触发热键。
$z::
while GetKeyState("z", "P")
{
SendInput, {n down}
Sleep, 100
SendInput, {n up}
}
return
这个脚本看起来合理,但可能出现“原本应该被拦截的 z 偶尔漏进游戏”的情况。原因是:$z 依赖钩子拦截原始 z,而 SendInput 期间 AHK 可能临时卸载自身钩子。用户如果正在快速按键或按住键,钩子短暂不在时,本应被挡住的原键就可能穿过去。
更稳的写法是改用 SendEvent,并明确按键延迟:
SendMode, Event
SetKeyDelay, -1, 0
$z::
while GetKeyState("z", "P")
{
SendEvent, {n down}
Sleep, 100
SendEvent, {n up}
}
return
这不是说 SendEvent 永远比 SendInput 好,而是在“钩子拦截 + 长按循环 + 游戏前台”这种组合里,稳定性往往比极限速度更重要。
五、长按脚本要区分按下、弹起、状态恢复
游戏脚本常见写法是 {key down} 和 {key up}。这类脚本一定要考虑退出和异常恢复,否则很容易卡键。尤其是脚本被暂停、重载、目标窗口切走、热键提前中断时,游戏可能一直认为某个键还按着。
SendMode, Event
SetKeyDelay, -1, 0
OnExit, ReleaseKeys
$x::
if GetKeyState("x", "P")
{
SendEvent, {Shift down}
while GetKeyState("x", "P")
{
SendEvent, {Space}
Sleep, 80
}
SendEvent, {Shift up}
}
return
ReleaseKeys:
SendEvent, {Shift up}{Ctrl up}{Alt up}
ExitApp
凡是用了 {某键 down},就要写对应的释放逻辑。不要只在正常流程里释放,脚本退出时也要兜底释放。
六、GetKeyState 的 P 状态不是硬件真相
GetKeyState("z", "P") 获取的是 AHK 通过钩子跟踪出来的物理状态,不是 Windows 提供的硬件绝对真相。如果脚本启动时按键已经按下,钩子没有看到“按下”事件,物理状态可能就是错的。SendInput 期间如果钩子被临时卸载,也会影响这种跟踪。
F2::
msg := "逻辑状态: " GetKeyState("z")
msg .= "`n物理状态: " GetKeyState("z", "P")
MsgBox, % msg
return
游戏脚本里用 P 做循环条件时,要知道它依赖钩子。如果你需要真正区分多键盘、读取硬件扫描码或绕开注入标记,那已经不是普通 AHK 钩子的范畴,通常要看 HID、Raw Input、AutoHotInterception 这类方案。
七、SendInput 不能触发钩子热键和热字符串
另一个实战坑是:你用 SendInput 发送一个键,希望它触发脚本里的另一个热键,结果没反应。原因是 SendInput 不适合用来触发依赖键盘钩子的热键和热字符串。简单 F1:: 可能由 RegisterHotkey 实现,而 *F1::、$F1::、热字符串等通常依赖钩子。
F3::
SendInput, {F4}
return
*F4::
MsgBox, 这个热键通常不会被上面的 SendInput 触发
return
如果你真的需要“脚本发送的按键触发另一个热键”,更应该用函数调用,或者用 SendEvent 配合 #InputLevel / SendLevel。
八、SendLevel 和 InputLevel:控制模拟按键能不能触发热键
AHKv1 也有 #InputLevel 和 SendLevel。它们的核心思路是:热键有输入级别,发送出来的按键也有发送级别;只有发送级别高于热键输入级别时,模拟按键才会触发该热键。
#InputLevel 1
*F6::
MsgBox, F6 被模拟输入触发
return
#InputLevel 0
F5::
SendLevel, 2
SendEvent, {F6}
SendLevel, 0
return
注意重点:这里用的是 SendEvent。原文也强调,SendInput 不适合用 SendLevel 来触发钩子热键。游戏脚本如果要做“热键转发热键”“脚本内部层级触发”,优先考虑函数调用,其次才是 SendEvent + SendLevel。
九、修饰键混入:为什么外部钩子下 SendInput 可能更糟
有外部钩子时,SendInput 的批量优势可能消失,用户真实输入也可能混进来。更麻烦的是修饰键状态。例如用户真实按着 Shift,你用 SendInput 发 a,目标里可能变成 A;而 SendEvent 更容易通过发送过程调整修饰键状态。
SendMode, Event SetKeyDelay, -1, 0 F7:: ; 游戏或聊天框里需要稳定发送小写时,先测试 Event 是否更可控 SendEvent, abc return
游戏里这类问题不一定表现为大小写,也可能表现为 Ctrl、Alt、Shift 卡住,技能键变成组合键,或者菜单被误触发。
十、游戏脚本发送模式怎么选
可以按下面这个顺序测试:
- 短促技能键:先试
SendInput,不稳再试SendEvent。 - 长按、连发、按下/弹起:优先
SendEvent,并写释放兜底。 - 目标只吃前台输入:保证窗口激活和权限一致,不要指望
ControlSend。 - 目标读 Raw Input 或设备状态:普通
Send可能无效,需要换 HID、驱动级或目标 API 路线。 - 脚本内部触发热键:优先函数调用,其次
SendEvent + SendLevel。 - 多脚本常驻:合并热键脚本,减少钩子链。
十一、多脚本钩子会拖慢游戏输入
多个 AHK 脚本同时常驻,并且都用了热字符串、#If 条件热键、$/*/~ 热键,会让钩子链变长。游戏里按键频率高,任何钩子回调里的慢操作都会放大成明显延迟。
; 推荐把同类热键合并到一个主脚本 #NoEnv #SingleInstance Force SendMode, Event SetKeyDelay, -1, 0 #Include %A_ScriptDir%\game_hotkeys.ahk #Include %A_ScriptDir%\text_hotkeys.ahk #Include %A_ScriptDir%\window_hotkeys.ahk
另外,条件热键的判断函数不要做文件读写、网络请求、长循环、复杂窗口枚举。需要判断时可以缓存结果,用 SetTimer 定时更新状态。
十二、实战排查清单
- 先在记事本测试脚本发送逻辑,确认 AHK 代码本身没错。
- 确认脚本和游戏权限一致,游戏管理员运行时脚本也要管理员运行。
- 退出其它 AHK 脚本和键盘增强工具,只保留当前脚本测试。
- 用
KeyHistory查看当前脚本是否安装键盘钩子。 - 长按循环优先用
SendEvent,不要盲目用SendInput。 - 所有
{key down}都要有{key up}和退出兜底。 - 如果游戏完全不吃模拟输入,考虑它是否读取 Raw Input、设备状态或过滤 injected 输入。
- 如果需要精准物理按键、多键盘区分,普通 AHK 钩子可能不够。
结论很简单:游戏脚本里,SendInput 是一个重要工具,但不是默认答案。它的优势依赖干净的钩子环境;一旦脚本本身依赖钩子、其它脚本也装钩子、游戏读取方式特殊,SendInput 就可能从“最快方案”变成“不稳定来源”。写游戏按键脚本时,要先弄清楚目标游戏吃哪种输入,再决定发送模式。
站内延伸
- 如何在大多数游戏中生效:按键、点击和窗口模式排查
- 为什么 Send 有时失效:SendInput、ControlSend、后台发送的区别
- AHK 热键不生效?从权限、输入法、窗口焦点一步步排查
- AHK 脚本为什么会误触发:热键穿透、按键连发、修饰键卡住排查
- AHK的多键盘-多鼠标支持

评论(0)