来源说明: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 侧“发送成功”和游戏侧“接受为有效操作”不是一回事。

  • 普通窗口:多半能吃 SendSendInputSendEvent
  • 老游戏或普通前台游戏:可能吃 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 也有 #InputLevelSendLevel。它们的核心思路是:热键有输入级别,发送出来的按键也有发送级别;只有发送级别高于热键输入级别时,模拟按键才会触发该热键。

#InputLevel 1
*F6::
MsgBox, F6 被模拟输入触发
return
#InputLevel 0

F5::
SendLevel, 2
SendEvent, {F6}
SendLevel, 0
return

注意重点:这里用的是 SendEvent。原文也强调,SendInput 不适合用 SendLevel 来触发钩子热键。游戏脚本如果要做“热键转发热键”“脚本内部层级触发”,优先考虑函数调用,其次才是 SendEvent + SendLevel

九、修饰键混入:为什么外部钩子下 SendInput 可能更糟

有外部钩子时,SendInput 的批量优势可能消失,用户真实输入也可能混进来。更麻烦的是修饰键状态。例如用户真实按着 Shift,你用 SendInputa,目标里可能变成 A;而 SendEvent 更容易通过发送过程调整修饰键状态。

SendMode, Event
SetKeyDelay, -1, 0

F7::
; 游戏或聊天框里需要稳定发送小写时,先测试 Event 是否更可控
SendEvent, abc
return

游戏里这类问题不一定表现为大小写,也可能表现为 CtrlAltShift 卡住,技能键变成组合键,或者菜单被误触发。

十、游戏脚本发送模式怎么选

可以按下面这个顺序测试:

  • 短促技能键:先试 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 就可能从“最快方案”变成“不稳定来源”。写游戏按键脚本时,要先弄清楚目标游戏吃哪种输入,再决定发送模式。

站内延伸

声明:站内资源为整理优化好的代码上传分享与学习研究,如果是开源代码基本都会标明出处,方便大家扩展学习路径。请不要恶意搬运,破坏站长辛苦整理维护的劳动成果。本站为爱好者分享站点,所有内容不作为商业行为。如若本站上传内容侵犯了原著者的合法权益,请联系我们进行删除下架。