日期:2025/04/05 05:55来源:未知 人气:51
斌哥说
大家好,我是斌哥。
今天要说的是用Python开发游戏辅助,这里指的是Python自动化实现的自动点击,自动打怪等操作。
而非嵌入到游戏内的辅助,如果是这类辅助,请使用lua,如果游戏允许lua寄生的话。
现代网络游戏对键盘处理,大致分为以下2种:
对键盘响应要求不高,采用普通按键方式
对键盘响应速度要求很高,采用高速按键
键盘方面,如果是纯鼠标或者键盘操作较少的游戏,那么基本上用pyautogui这类模块或者自己封装WindowsAPI中的keybd_event即可搞定。
而对于那种鼠标键盘互动的游戏,例如3D射击、moba、赛车等游戏,对键盘要求极高的游戏。我们用pyautogui或者是API提供的keybd_event,再者通过windows消息发送按键消息,都会发生这样的一幕:无效。
这种无效,不是代码有问题,也不是运行过程发生问题,而是仅仅的因为一个问题:那就是Windows上对键盘要求高的网络游戏,都采用的DirectX框架中的DirectInput接口。
按键无效(游戏采用DirectInput接口)的解决办法
解决办法只有两种:
1、通过winio这类驱动级的按键库(这个过程是汇编出真实的键盘事件),再者如果汇编水平不错,可以考虑自己写。Python也有winio的模块,叫rabird.winio,不过相当不友好,只能在windows调试模式中运行。
2、响应DirectInput支持的按键代码
为什么pyautogui和keybd_event在游戏中无效?
开始之前,首先要清楚,为什么pyautogui和keybd_event在游戏中无效?
keybd_event是WindowsAPI的一员,它的作用是模拟出windows环境中的按键。
其原型为:VOID keybd_event(BYTE bVk,BYTE bScan,DWORD dwFlags,DWORD dwExtralnfo);
参数:
bVk:定义一个虚拟键码。键码值必须在1~254之间。bScan:定义该键的硬件扫描码。dwFlags:定义函数操作的各个方面的一个标志位集。应用程序可使用如下一些预定义常数的组合设置标志位。 KEYEVENTF_EXTENDEDKEY:若指定该值,则扫描码前一个值为OXEO(224)的前缀字节。 KEYEVENTF_KEYUP:若指定该值,该键将被释放;若未指定该值,该键将被按下。dwExtralnfo:定义与击键相关的附加的32位值。
要知道,任何操作系统和键盘交互过程都是这样的:
用户按下按键 -> 键盘驱动 -> 操作系统产生键盘事件 -> 响应
而我们的keybd_event只是提供了keybd_event接口规定的虚拟键盘码,例如虚拟键盘码F1定义为:#define VK_F1 0x70,而游戏的键盘接口却是用的DirectInput,DirectInput中虚拟键盘码则是这样定义的:#define DIK_F1 0x3B
一个是0x70 一个是0x3b
假设这样一个场景,Python程序需要在某个游戏中按下F1按键,Python调用keybd_event产生键盘事件,0x70被传到DirectInput中,这时DirectInput会以为用户按下了日文键盘中的KANA键,因为DirectInput键盘码宏就有这样的定义:#define DIK_KANA 0x70
至于pyautogui这类模块为什么一样无效,其实很简单,因为他们就是在方法内调用了keybd_event
解决办法——SendInput在呼唤你
事实上,无论是keybd_event还是mouse_event(用于处理鼠标的winAPI)都是SendInput的简单封装。我们用SendInput可以解决更复杂的操作,就比如模拟DirectX的DirectInput键盘事件。DX键盘码:http://www.gamespp.com/directx/directInputKeyboardScanCodes.html
DX键盘码宏定义
SendInput原形:
UINT SendInput(UINT nInputs, LPINPUT pInputs, int cbSize );
Parameters
nInputs
Specifies how many structures pInputspoints to.
pInputs
Pointer to an array of INPUT structures.
cbSize
Specifies the size of an INPUTstructure. If cbSize is not the INPUT structure, the function will fail.
Return Values
The number of events that the function inserted into the keyboard or mouse
简单粗暴的说:nInputs指定了发送pInputs事件次数,pInputs则是键盘或鼠标事件,cbSize是pInputs事件结构体的大小。Specifies the size of an INPUT structure
INPUT结构:
typedef struct tagINPUT {
DWORD type;
union{
MOUSEINPUT mi; //鼠标结构
KEYBDINPUT ki;//键盘结构
HARDWAREINPUT hi; //硬件事件结构
};
} INPUT, PINPUT, FAR LPINPUT;
鼠标事件结构体:MOUSEINPUT
typedef struct tagMOUSEINPUT {
LONG dx;
LONG dy;
DWORD mouseData;
DWORD dwFlags;
DWORD time;
ULONG_PTR dwExtraInfo;
} MOUSEINPUT, *PMOUSEINPUT;
键盘事件结构体KEYBDINPUT
typedef struct tagKEYBDINPUT {
WORD wVk;
WORD wScan;
DWORD dwFlags;
DWORD time;
ULONG_PTR dwExtraInfo;
} KEYBDINPUT, *PKEYBDINPUT;
硬件事件结构体
typedef struct tagHARDWAREINPUT {
DWORD uMsg;
WORD wParamL;
WORD wParamH;
} HARDWAREINPUT, *PHARDWAREINPUT;
构造SendInput环境
import ctypes
import time
DIK_1 = 0x02
DIK_2 = 0x03
.....
SendInput = ctypes.windll.user32.SendInput
PL = ctypes.POINTER(ctypes.c_ulong) #unsigned long类型指针
class KeyBdInput(ctypes.Structure):
fields = [("wVk", ctypes.c_ushort),
("wScan", ctypes.c_ushort),
("dwFlags", ctypes.c_ulong),
("time", ctypes.c_ulong),
("dwExtraInfo", PL)]
class HardwareInput(ctypes.Structure):
fields = [("uMsg", ctypes.c_ulong),
("wParamL", ctypes.c_short),
("wParamH", ctypes.c_ushort)]
class MouseInput(ctypes.Structure):
fields = [("dx", ctypes.c_long),
("dy", ctypes.c_long),
("mouseData", ctypes.c_ulong),
("dwFlags", ctypes.c_ulong),
("time",ctypes.c_ulong),
("dwExtraInfo", PL)]
class Input_I(ctypes.Union):
fields = [("ki", KeyBdInput),
("mi", MouseInput),
("hi", HardwareInput)]
class Input(ctypes.Structure):
fields = [("type", ctypes.c_ulong),
("ii", Input_I)]
DX_PRESS_DOWN = 0x0008 #键盘按下
DX_PRESS_UP = DX_PRESS_DOWN | 0x0002 #键盘松开
INFO_MOUSE_TYPE = 0 #鼠标类型
INFO_KEY_TYPE = 1 #键盘类型
INFO_HARDWARE_TYPE = 2 #硬件类型
def PressDown(hexKeyCode):
extra = ctypes.c_ulong(0)
ii_ = Input_I()
ii_.ki = KeyBdInput( 0, hexKeyCode, DX_PRESS_DOWN, 0, ctypes.pointer(extra) )
x = Input( ctypes.c_ulong(INFO_KEYTYPE), ii )
return SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
def PressUp(hexKeyCode):
extra = ctypes.c_ulong(0)
ii_ = Input_I()
ii_.ki = KeyBdInput( 0, hexKeyCode, DX_PRESS_UP, 0, ctypes.pointer(extra) )
x = Input( ctypes.c_ulong(INFO_KEYTYPE), ii )
return SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
def Press(hexKeyCode):
PressDown(hexKeyCode)
time.sleep(0.1)
PressUp(hexKeyCode)
def PressStr(string):
dxkeys = {
"a" : DIK_A,\
"b" : DIK_B,\
"c" : DIK_C,\
"d" : DIK_D,\
"e" : DIK_E,\
"f" : DIK_F,\
"g" : DIK_G,\
...#定义a-z的按键
"0" : DIK_0,\
"1" : DIK_1,\
...#定义0-9的按键
}
if(type(string) == str):
mys = string.lower() #转换成小写,为了对应字段键名
for i in range(len(string)):
Press(dxkeys[mys[i]]) #按下对应的值
例如我们要在游戏中输入“斌哥说python”
第一步,唤起游戏内的输入状态,例如按下Enter为唤起游戏内的聊天输入框
Press(DIK_RETURN)
time.sleep(1)
第二步,切换输入法
PressDown(DIK_LCONTROL) #按下左Ctrl
Press(DIK_DIK_SPACE) #按空格
PressUp(DIK_LCONTROL) #松开左Ctrl
这时就设置了当前输入法为输入法管理器内的第一个输入法
第三步,输入“斌哥说”对应的拼音是:bingeshuo(前提是输入法必须是训练过的,现代输入法都有记忆功能)
PressStr("bingeshuo")
Press(DIK_DIK_SPACE) #按空格 输入法特性,按下空格传递当前字符到编辑控件
第四步,输入python
Press(DIK_LSHIFT) #按下左Shift 现代输入法几乎都支持
PressStr("python")
第五步,发送聊天消息,即可按下回车Press(DIK_RETURN)
很久之前,有个人找到我,跟我要Python版本的winio,他的目的就是开发一个自动化游戏辅助,那时候还没有rabird.winio这种东西,他跟我说愿意给我钱,那时,我感到很悲哀。
尽管现在有winio for Python,但是太不友好了,希望不久的将来,更多热爱Python的小伙伴可以开发出winio这种驱动级的模块......
我是斌哥,喜欢请点击关注。
斌哥说Python,只专注于Python技术!