【XInput】游戏手柄模拟鼠标动作

这篇具有很好参考价值的文章主要介绍了【XInput】游戏手柄模拟鼠标动作。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

老周一般很少玩游戏,在某宝上买了一堆散件,计划在过年期间自己做个机械臂耍耍。头脑中划过一道紫蓝色的闪电,想起用游戏手柄来控制机械臂。机械臂是由树莓派(大草莓)负责控制,然后客户端通过 Socket UDP 来发送信号。优先考虑在 PC 和手机上测试,就顺便折腾一下 XInput API。当然,读取手柄数据有多套 API。本文老周先介绍 XInput 方案,后面再介绍 Windows.Gaming.Input 方案。Windows.Gaming.Input 是 UWP API,也可以在.NET 项目中使用。.NET 程序适合用这套 API。

XInput 中的 X 指的就是“西瓜手柄”,哦不,是 XBox 手柄。当然了,并不局限于 XB 手柄,结构与 XB 相似的手柄也能用。老周用的是北通的阿修罗无线版,经测试是可用的。

XInput API 基本的定义都在 Xinput.h 头文件中。读手柄的数值要用到 XInputGetState 函数,它的原型如下:

DWORD WINAPI XInputGetState
(
    _In_  DWORD         dwUserIndex,  // Index of the gamer associated with the device
    _Out_ XINPUT_STATE* pState        // Receives the current state
) 

dwUserIndex 指的是手柄设备索引,范围为 0 - 3,也就是只能连接四个手柄(其实也够玩了)。如果只连接了一个手柄,这个参数直接用 0 就可以了。如果你想用 for 循环来访问各个手柄,还可以用到以下宏:

#define XUSER_MAX_COUNT       4

pState 参数是指针类型,指向 XINPUT_STATE 结构体,它只有两个成员:

typedef struct _XINPUT_STATE
{
    DWORD                               dwPacketNumber;
    XINPUT_GAMEPAD                      Gamepad;
} XINPUT_STATE, *PXINPUT_STATE;

DWORD 就是 double word,一个 word 是 16 位无符号整数,两个就是 32 位。所以 dwPacketNumber 字段是一个整数值。它表示你读取数据的序号,它的值会不断累加,在读取数据时,咱们可以用一个变量保存它的值,每次读手柄后进行比较,如果这个序号没有变化,说明用户没有操作手柄;相反,如果值不同,表明手柄的状态有改变。

Gamepad 字段是另一个结构体—— XINPUT_GAMEPAD。

typedef struct _XINPUT_GAMEPAD
{
    WORD                                wButtons;
    BYTE                                bLeftTrigger;
    BYTE                                bRightTrigger;
    SHORT                               sThumbLX;
    SHORT                               sThumbLY;
    SHORT                               sThumbRX;
    SHORT                               sThumbRY;
} XINPUT_GAMEPAD, *PXINPUT_GAMEPAD;

wButtons 表示手柄被按下的按键,“w”表示它的值是 word 类型。以下宏定义了这些按键:

1、方向键。

/* 下面这四个是手柄上的方向键:上、下、左、右 */
#define XINPUT_GAMEPAD_DPAD_UP            0x0001
#define XINPUT_GAMEPAD_DPAD_DOWN       0x0002
#define XINPUT_GAMEPAD_DPAD_LEFT         0x0004
#define XINPUT_GAMEPAD_DPAD_RIGHT       0x0008

就是中间那四个,如下图红圈内。

【XInput】游戏手柄模拟鼠标动作

2、开始和返回。

#define XINPUT_GAMEPAD_START            0x0010
#define XINPUT_GAMEPAD_BACK             0x0020

如下图黄圈那两个键:

【XInput】游戏手柄模拟鼠标动作

3、X、Y、A、B 键。

#define XINPUT_GAMEPAD_A                0x1000
#define XINPUT_GAMEPAD_B                0x2000
#define XINPUT_GAMEPAD_X                0x4000
#define XINPUT_GAMEPAD_Y                0x8000

就是右摇杆上面的四个键,如下图绿色圈内部分:

【XInput】游戏手柄模拟鼠标动作

 

4、下面两个表示摇杆上的按钮按下时触发:

/* 左摇杆按下 */
#define XINPUT_GAMEPAD_LEFT_THUMB       0x0040

/* 右摇杆按下 */
#define XINPUT_GAMEPAD_RIGHT_THUMB      0x0080

 

5、下面两个是“肩膀”键,在手柄的左上角和右上角。

#define XINPUT_GAMEPAD_LEFT_SHOULDER    0x0100
#define XINPUT_GAMEPAD_RIGHT_SHOULDER   0x0200

下图中蓝色圈内就是。

【XInput】游戏手柄模拟鼠标动作

 

好,回到 XINPUT_GAMEPAD 结构体,接着看其他字段。

typedef struct _XINPUT_GAMEPAD
{
    ……
    BYTE                                bLeftTrigger;
    BYTE                                bRightTrigger;
    SHORT                               sThumbLX;
    SHORT                               sThumbLY;
    SHORT                               sThumbRX;
    SHORT                               sThumbRY;
} XINPUT_GAMEPAD, *PXINPUT_GAMEPAD;

bLeftTrigger 和 bRightTrigger 是两个扳机键,玩打鬼子游戏时用来开枪,它的范围是 0 - 255,所以类型是字节。

后机四个 sThumb-- 是两个摇杆的读数(左摇杆的X、Y值,右摇杆的X、Y值),范围是有符号的 16 位整数值,取值在 -32768 和 32767 内,摇杆位于中心位置时,读值为 0。摇杆向前(向上)推时Y为正值,向后(向下)推时Y为负值;摇杆向左推时X为负值,向右推时X为正值。

就这样了,有了上述知识,你已经可以读手柄了。下面咱们做个示例。

新建一个 C++ 控制台项目就可以了,不需要 Windows / Win32 应用。

a、包含 Xinput.h 头文件。

#include <stdio.h>
#include <Windows.h>
#include <Xinput.h>

 

b、光包含头文件还不行,因为项目默认没有导入相关的 .lib 文件。.lib 可不是什么静态库,只是描述动态库的符号罢了。在“解决方案”窗口右击项目,打开属性窗口。“配置”处选“所有”,免得为 Debug 和 Release 版本重复配置。

【XInput】游戏手柄模拟鼠标动作

 

在左边导航节点中找到“链接器” -> “输入”,并选中。在窗口右边点击“附加依赖项”右边的下拉箭头,点击“编辑...”。

【XInput】游戏手柄模拟鼠标动作

 

在弹出的对话框中加上 “Xinput.lib”。

【XInput】游戏手柄模拟鼠标动作

确定保存即可。 

下面是整个程序的代码:

#include <stdio.h>
#include <Windows.h>
#include <Xinput.h>

/* 此变量保存读数序号 */
static unsigned long readOrder = 0;

/* 入口点 */
int main(int argc, char** argv)
{
    /* 开始读数 */
    XINPUT_STATE xis = { 0 };
    while (1)
    {
        if (ERROR_SUCCESS != XInputGetState(0, &xis)) {
            continue;    /* 这一次没读成功,下一次再读 */
        }
        /* 注意比较一下数据序号,相同的值不需要处理 */
        if (readOrder == xis.dwPacketNumber) {
            continue;
        }
        /* 保存新的序号 */
        readOrder = xis.dwPacketNumber;
        /* 分析数据 */
        printf_s("左摇杆:x= %d,y= %d\t右摇杆:x= %d,y= %d\n",
            xis.Gamepad.sThumbLX,
            xis.Gamepad.sThumbLY,
            xis.Gamepad.sThumbRX,
            xis.Gamepad.sThumbRY);

        /* 休息一会儿 */
        Sleep(60);
    }
    printf_s("即将退出\n");
    return 0;
}

XInputGetState 函数调用成功,返回 ERROR_SUCCESS,也就是 0。注意:每次读取后,要比较一下数据序号,如果新序号没有变,说明手柄的状态未改变,不用处理;如果值不相同,说明有新的状态,要处理,并且保存最新的数据序号

把你的手柄连接好,运行程序。接着推动左右摇杆,会看到控制台打印各个坐标值。

【XInput】游戏手柄模拟鼠标动作

如果需要,还可以加入对按键的判断,比如这里,我加入对 X、Y、A、B 键的判断。

    while (1)
    {
        ………………
        /* 判断按键 */
        if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_A) == XINPUT_GAMEPAD_A)
        {
            printf_s("你按下了【A】键\t");
        }
        else if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_B) == XINPUT_GAMEPAD_B)
        {
            printf_s("你按下了【B】键\t");
        }
        else if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_X) == XINPUT_GAMEPAD_X)
        {
            printf_s("你按下了【X】键\t");
        }
        else if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_Y) == XINPUT_GAMEPAD_Y)
        {
            printf_s("你按下了【Y】键");
        }
        printf_s("\n");

        /* 休息一会儿 */
        Sleep(60);
    }

各个键位的宏所定义的值都是占用一个二进制位,所以这里咱们可以通过位运算来确定哪个按钮被触发。

效果如下:

【XInput】游戏手柄模拟鼠标动作

 

-----------------------------------------------------------------------------------------------------

好了,现在,离模拟鼠标动作不远了,下一步就是如发送消息了。发送输入模拟需要用 SendInput 函数。它的原型如下:

UINT SendInput(
    UINT cInputs,                   // number of input in the array
    LPINPUT pInputs,                // array of inputs
    int cbSize);                    // sizeof(INPUT)

为了好看,我去掉修饰参数的宏。这个函数如果返回 0,说明输入消息发送不成功;成功的时候是返回已发送的消息数。调用这个函数的核心是 INPUT 结构体。一个 INPUT 实例就代表一条指令,一个操作可能会有多条指令完成,所以, INPUT以数组的形式传递。

cInputs 参数指定数组中 INPUT 实例的个数,cbSize 是一个 INPUT 实例的大小(字节,用 sizeof 运算符)。pInputs 就是指向 INPUT 数组第一个元素的指针。

然后,咱们了解一下 INPUT 结构体。

typedef struct tagINPUT {
    DWORD   type;

    union
    {
        MOUSEINPUT      mi;
        KEYBDINPUT      ki;
        HARDWAREINPUT   hi;
    } DUMMYUNIONNAME;
} INPUT, *PINPUT, FAR* LPINPUT;

指向 INPUT 结构的指针是 LP(长指针、分配远程堆,所以有 far),这个咱们一般不用管它,这东西是16位处理器的遗留物,现在处理器都 32 位以上了。不过,咱们得关注的是:这个结构体里面,除了第一个字段 type,其他的字段是共用内存的(union)。说人话就是,mi、ki、hi 字段的偏移地址相同。不过,这个结构体是 8 字节对齐的,type 只有4字节,剩下4字节留空,下一个字段是从第9个字节开始(偏移是8)。最后就相当于第一个字段用了8字节,后面的字段用32字节,整个结构体40字节。如果 dll import 到 .NET 项目,得注意这个偏移,不然后无法调用。如果你照抄网上的 PInvoke 代码,至少在 64 位系统上是不起作用的。老周后面的水文中会告诉大伙伴们怎么 Dll Import,

mi、ki、hi 这几个货的大小并不一致,分别占用 32、24、8 字节。为了使用访问性能最优,故选择 8 字节对齐。说人话就是程序单次处理8个字节,如 8、16、24、32、64 等。

INPUT 结构体的 type 字段指明要模拟的输入类型:

#define INPUT_MOUSE     0            /* 鼠标 */
#define INPUT_KEYBOARD  1            /* 键盘 */
#define INPUT_HARDWARE  2            /* 硬件消息 */

从名字就知道是啥了,就是模拟鼠标、键盘事件,这两个是最常用的;第三个是除鼠标、键盘以外的硬件输入消息,直接用消息编号,这个极少用。这三个值对应 INPUT 结构体中的 mi、ki、hi 字段。

咱们的例子只是模拟鼠标动作,所以用到 MOUSEINPUT 结构体。

typedef struct tagMOUSEINPUT {
    LONG    dx;
    LONG    dy;
    DWORD   mouseData;
    DWORD   dwFlags;
    DWORD   time;
    ULONG_PTR dwExtraInfo;
} MOUSEINPUT, *PMOUSEINPUT, FAR* LPMOUSEINPUT;

dwFlags 是一个整数值,可以由多个二进制组合使用,包括:

#define MOUSEEVENTF_MOVE        0x0001 /* mouse move */
#define MOUSEEVENTF_LEFTDOWN    0x0002 /* left button down */
#define MOUSEEVENTF_LEFTUP      0x0004 /* left button up */
#define MOUSEEVENTF_RIGHTDOWN   0x0008 /* right button down */
#define MOUSEEVENTF_RIGHTUP     0x0010 /* right button up */
#define MOUSEEVENTF_MIDDLEDOWN  0x0020 /* middle button down */
#define MOUSEEVENTF_MIDDLEUP    0x0040 /* middle button up */
#define MOUSEEVENTF_XDOWN       0x0080 /* x button down */
#define MOUSEEVENTF_XUP         0x0100 /* x button down */
#define MOUSEEVENTF_WHEEL                0x0800 /* wheel button rolled */
#if (_WIN32_WINNT >= 0x0600)
#define MOUSEEVENTF_HWHEEL              0x01000 /* hwheel button rolled */
#endif
#if(WINVER >= 0x0600)
#define MOUSEEVENTF_MOVE_NOCOALESCE      0x2000 /* do not coalesce mouse moves */
#endif /* WINVER >= 0x0600 */
#define MOUSEEVENTF_VIRTUALDESK          0x4000 /* map to entire virtual desktop */
#define MOUSEEVENTF_ABSOLUTE             0x8000 /* absolute move */

这里老周仅模拟移动、左键按下/弹起、右键按下/弹起,以及滚轮。如果要模拟滚轮,要指定 MOUSEEVENTF_WHEEL,滚轮的值通过 MOUSEINPUT 结构体的 mouseData 字段传递

 

把前面的示例程序改一下,这回咱们不输出文本了,而是从手柄读数据,然后用 SendInput 函数发送鼠标模拟。

    while (1)
    {
        if (ERROR_SUCCESS != XInputGetState(0, &xis)) {
            continue;    /* 这一次没读成功,下一次再读 */
        }
        /* 注意比较一下数据序号,相同的值不需要处理 */
        if (readOrder == xis.dwPacketNumber) {
            continue;
        }
        /* 保存新的序号 */
        readOrder = xis.dwPacketNumber;
        
        // 转换一下
        int xx = xis.Gamepad.sThumbRX / 1000;
        int yy = -xis.Gamepad.sThumbRY / 1000;
        // 这个是滚轮
        int wheel = xis.Gamepad.sThumbLY / 500;
        /* 准备发送消息 */
        INPUT mouseAction = { 0 };
        mouseAction.type = INPUT_MOUSE;
        // 设置偏移坐标
        mouseAction.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_WHEEL;
        mouseAction.mi.dx = xx;
        mouseAction.mi.dy = yy;
        mouseAction.mi.mouseData = wheel;    // 滚轮数据
        SendInput(1, &mouseAction, sizeof(INPUT));

        /* 模拟左键单击 */
        if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_A) == XINPUT_GAMEPAD_A)
        {
            // 一次单击包含两条消息:按下 + 弹起
            INPUT leftDown = { 0 };
            leftDown.type = INPUT_MOUSE;
            leftDown.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
            INPUT leftUp = { 0 };
            leftUp.type = INPUT_MOUSE;
            leftUp.mi.dwFlags = MOUSEEVENTF_LEFTUP;
            // 创建数组
            INPUT cmds[] = { leftDown, leftUp };
            SendInput(2, cmds, sizeof(INPUT));
        }

        /* 模拟右键单击 */
        if ((xis.Gamepad.wButtons & XINPUT_GAMEPAD_B) == XINPUT_GAMEPAD_B)
        {
            // 也是两条消息:右键按下 + 弹起
            INPUT rightDown = { 0 };
            rightDown.type = INPUT_MOUSE;
            rightDown.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN;
            INPUT rightUp = { 0 };
            rightUp.type = INPUT_MOUSE;
            rightUp.mi.dwFlags = MOUSEEVENTF_RIGHTUP;
            // 创建数组
            INPUT inputs[] = { rightDown, rightUp };
            SendInput(2, inputs, sizeof(INPUT));
        }

        /* 休息一会儿 */
        Sleep(60);
    }

MOUSEINPUT结构体的 dwFlags 字段要注意一下:

1、移动鼠标用的是 MOUSEEVENTF_MOVE, 滚轮用的是 MOUSEEVENTF_WHEEL。这里老周是把两者合起来发送:MOUSEEVENTF_MOVE | MOUSEEVENTF_WHEEL;

2、MOUSEEVENTF_MOVE 值可以与 MOUSEEVENTF_ABSOLUTE 值组合用。如果指定了 MOUSEEVENTF_ABSOLUTE,表示使用绝对定位坐标,值的范围在 0 和 65535 之间。这个范围不管你的显示器屏幕的大小,总之,左上角是 (0, 0),右下角是 (65535, 65535),鼠标指针的位置在这范围内换算。这里不用 MOUSEEVENTF_ABSOLUTE 值,dx、dy 就变成移动量,以像素为单位的。正值表示向下/向右移动;负值表示向上/向左移动。这里还是选择移动量好一些,可避免鼠标指针飘得太快难以操控。例如,-22 表示向反方向移动 22 像素,+50 表示正向移动 50 像素。

前面的演示中咱们知道摇杆的读值是 -32768 到 32767,这个值有点大,所以老周做了运算:

int xx = xis.Gamepad.sThumbRX / 1000;
int yy = -xis.Gamepad.sThumbRY / 1000;
// 这个是滚轮
int wheel = xis.Gamepad.sThumbLY / 500;

移动量除以 1000,滚轮值除以 500。这个换算不是固定的,只是老周觉得这个值比较合适,除数的值越大,活动的范围越小。

在上面演示中,老周用 A 键模拟左键单击,B 键模拟右键单击。左摇杆的 Y 方向模拟滚轮,右摇杆模拟鼠标指针的移动。当然,你可以使用任何你喜欢的键和摇杆来模拟。我这里仅作参考。

用手柄移动手柄的效果如下:

【XInput】游戏手柄模拟鼠标动作

 

模拟鼠标点击效果如下:

【XInput】游戏手柄模拟鼠标动作

 

滚轮模拟的效果如下:

【XInput】游戏手柄模拟鼠标动作

 

好了,今天就说到这里。下一篇咱们用 .NET P/Invoke 来实现。文章来源地址https://www.toymoban.com/news/detail-827305.html

到了这里,关于【XInput】游戏手柄模拟鼠标动作的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Unity上接入手柄,手柄控制游戏物体移动

    1、unity软件上安装system input 组件。菜单栏【window】-【Packag Manager】打开如下界面,查找Input System,并且安装。 2、安装成功后插入手柄到windows上,打开菜单栏上【window】--【Analysis】--【Input Debuger】 进入Input Debug界面,可以看到手柄设备能被Unity识别。 3、双击【XinputControllerW

    2024年04月15日
    浏览(48)
  • 【Unity】Input——检测鼠标、键盘、手柄输入、鼠标在屏幕上的位置等等

    Unity提供的 更方便的 控制对象的 位移和旋转的解决方案 这个Axis是可以自定义的:

    2024年02月11日
    浏览(88)
  • (20)操纵杆或游戏手柄

    文章目录 前言 20.1 你将需要什么 20.2 校准 20.3 用任务规划器进行设置 20.4 飞行前测试控制装置 20.5 测试失控保护 20.6 减少控制的滞后性  本文解释了如何用 操纵杆 或 游戏手柄 控制你的飞行器,使用任务计划器向飞行器发送\\\" RC Override \\\"消息。 其他 GCS 也可能支持\\\" RC Override

    2024年02月16日
    浏览(27)
  • 复活小米蓝牙手柄,让手柄控制电脑PC玩React写的网页游戏

    小案例系列:小米蓝牙手柄玩PC上React写的网页游戏 环境: Windows11 Nodejs:16.10.0 Python:3.8.1 小米手柄:2015年随天猫魔盒一起购入;已经几年没碰过了 家中有娃到了玩游戏的年龄,在同学家玩过手柄以后,手机都不香了。每天摸着7,8年有多的小米手柄,眼神中充满渴望。好吧

    2024年02月13日
    浏览(49)
  • 基于ESP32 蓝牙游戏手柄设计

    使用 ESP32 并通过 BLE 通信的 DIY 手持游戏手柄   硬件组件 esp32        ×    1     ws2812b        ×    6     操纵杆        ×    2     角度按钮    ×    2     按钮        ×    8     18560电池和电池座×    2     3路拨动开关    ×    1     TP4056带保

    2024年02月02日
    浏览(35)
  • FPS游戏实战数据集|yolov8训练模型导出|C/C++项目|驱动鼠标模拟人工|加密狗USB硬件虚拟化

    目录 数据集准备 训练模型 模型部署 总结 YOLO(You Only Look Once)是一种基于深度学习的目标检测算法,能够快速准确地识别图像中的目标。在游戏领域,YOLO可以应用于游戏场景中的人物识别和动作捕捉等方面。本文将介绍如何使用YOLO识别游戏人物。 15000张FPS实战数据集yolo

    2024年02月05日
    浏览(60)
  • 苹果为 Vision Pro 头显申请游戏手柄专利

    苹果Vision Pro 推出后,美国专利局公布了两项苹果公司申请的游戏手柄专利,其中一项的专利图如下图所示。据 PatentlyApple 报道,虽然申请专利并不能保证苹果公司会推出游戏手柄,但是苹果公司同时也为游戏手柄申请了商标,这表明苹果公司有可能在未来为 Apple Vision Pro 配

    2024年02月11日
    浏览(38)
  • UI自动化Selenium ActionChains鼠标(动作链)

    我们在实现UI自动化过程中,有时会遇到鼠标模拟操作,如鼠标悬停后,菜单划出;鼠标按下后,下拉展开;单击、双击、拖动等等;但我们常常对鼠标的单击和双击比较了解(click和doubleclick)但是其他的如何使用呢?且看下面分解: 1、selenium提供了一个类来专门处理鼠标的

    2024年01月18日
    浏览(48)
  • Qt3D 输入类处理鼠标键盘动作

    Qt3D模块中的输入类用于处理用户输入,比如鼠标、键盘等输入事件。 ```cpp #include Qt3DExtras #include Qt3DInput #include Qt3DCore #include Qt3DRender #include Qt3DLogic #include Qt3DExtras int main(int argc, char *argv[]) {     QApplication app(argc, argv);     // 创建Qt3D窗     Qt3DExtras::Qt3DWindow view;     // 创建3

    2024年02月02日
    浏览(34)
  • Lua语言实现游戏动作

    Lua是一种轻量级的脚本语言,它具有高效性、可扩展性和易学性等优点。在游戏开发领域,Lua语言得到了广泛应用。Lua语言可以用来实现游戏动作,包括角色行走、攻击、技能释放等。本文将详细介绍如何使用Lua语言实现游戏动作。 一、Lua语言介绍 Lua语言是一种轻量级的脚

    2023年04月14日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包