今天我们来一起学习在Linux环境下如何编写游戏手柄驱动,并通过手柄按键模拟键盘鼠标操作,最终在Linux系统中使用游戏手柄来玩魔兽世界游戏。
一、操作系统的选择
网上很多朋友在对Linux操作系统选择时非常困惑,不知道应该选用哪一个发行版进行学习。的确,Linux的发行版非常众多,要想在众多发行版中选出一个适合自己学习和工作的确实比较困难。我自己使用Linux已经有相当长的一段时间了,自己曾经使用过发行版有:RedHat Linux、CentOS、Ubuntu Desktop、Fedora、Debian、OpenSUSE、Gentoo、ArchLinux这里我所说的使用过是指确确实实在电脑上安装并使用超过3个月以上,而不是通过虚拟机安装简单的体验。
提起Linux,很多人都会想到Ubuntu,并且把Ubuntu作为Linux的代名词。我们需要跟大家说的是,Linux的发行版众呈纷纭,每一个发行版都有其相应的优点和缺点。Ubuntu只是Linux家族中一个较为流行的发行版而已,千万不能把二者等价想看。很多朋友热爱Ubuntu因为其安装简便,图形化界面操作简易、美观,在Ubuntu系统下完成工作环境的搭建和部署较为容易,因此就使得Ubuntu成为了人们心中热衷的Linux发行版。但实际上Ubuntu系统却略显笨重,对于Linux入门者来说是个不错的选择,但是Ubuntu的这种更人性化的图形界面会让使用者陷入图形化界面操作的误区,而对Linux的命令行界面产生排斥。
实际上,Linux的命令行界面功能强大,强大的命令行界面完全超越图形化界面。而对于众多Linux的发行版来说,软件包参差不齐,图形界面也各有千秋,但命令行界面的Shell命令却一致统一,因此学好Linux命令行是进入Linux大门的关键,不要把主要精力放在其多样化的图形界面上,学好了命令行界面,无论在什么发行版下工作和学习有会游刃有余,而不依赖于各个图形化界面的不同操作方式。图形化界面只是为了使用者操作方便而开发的一套可视化的用户界面,例如GNOME、KED、XFCE4、LXQT、MATE-COMPIZ、LXDE、SOAS等等。
作者同样尝试过各个不同的图形化界面,最终的选择的发行版为ArchLinux,图形化界面为XFCE4。
-`
.o+` lidq@archlinux
`ooo/ OS: Arch Linux
`+oooo: Kernel: x86_64 Linux 5.2.8-arch1-1-ARCH
`+oooooo: Uptime: 56m
-+oooooo+: Packages: 1242
`/:-:++oooo+: Shell: bash 5.0.7
`/++++/+++++++: Resolution: 3360x1080
`/++++++++++++++: DE: XFCE4
`/+++ooooooooooooo/` WM: Xfwm4
./ooosssso++osssssso+` WM Theme: Agualemon
.oossssso-````/ossssss+` GTK Theme: Adwaita [GTK2]
-osssssso. :ssssssso. Icon Theme: Mezzanotte
:osssssss/ osssso+++. Font: Sans 10
/ossssssss/ +ssssooo/- CPU: Intel Core i9-9900K @ 16x 5GHz [27.8°C]
`/ossssso+/:- -:/+osssso+- GPU: GeForce RTX 2060
`+sso+:-` `.-/+oso: RAM: 2401MiB / 64330MiB
`++:. `-/+/
.` `/
二、游戏手柄驱动
我们今天的主要学习内容就是编写游戏手柄驱动。首先我们选用一款普通的USB有线游戏手柄,如下图:
此游戏手柄下方有2个摇杆、左侧一个十字方向盘、右侧4个按钮,顶部左右各有2个按钮,另外,游戏手柄中间还有几个功能按钮,我们暂时只使用2个,加在一起我们需要处理20个按钮。将游戏手边的USB端插入电脑,在Linux系统中会自动识别此USB设备,并为其创建一个/dev/hidrawN的设备节点,其中N为数字,在不同的电脑上数字会不同,但通常为4,例如,在作者的电脑上接入游戏手柄之后系统为其分配的设备节点为/dev/hidraw4如下图所示:
有了设备节点之后的事情就方便多了,我们可以编写程序,从此设备节点中读取此游戏手柄的按键信息:
int fd_joy = open("/dev/hidraw4", O_RDWR | O_NONBLOCK);
if (fd_joy < 0)
{
printf("could not open %s.\n", devs->dev_j);
goto _out;
}
while (1)
{
uint8_t buff[0x20];
int len = read(fd_joy, buff, 0x20);
if (len <= 0)
{
return 0;
}
for (int i = 0; i < len; ++i) {
printf("%02X ", buff[i]);
}
printf("\n");
usleep(10 * 1000);
}
这样将程序编译运行即可从游戏手柄中读出当前按键的相关信息,作者手中的游戏手柄在没有任何按键时数据如下:
84 85 7B 7B FF 0F 00 00
我们可以将手柄中的按键一个一个按下,并查看每一个字节的变化内容来推算出按钮与字节的对应关系,例如作者的游戏手柄中左侧十字方向盘的4个按键分别对应数组中下标为5的字节的低4位的值:
上: 0x0
下: 0x4
左: 0x2
右: 0x6
空: 0xF
其它按钮都可以根据这种方法来一一对应,这里不再赘述,请读者根据自己的游戏手柄自行计算并得到当前按键。
三、模拟鼠标键盘
我们选择Linux来进行模拟按键开发是因为Linux给编程者提供了非常丰富的调用接口,我们可以使用程序对键盘和鼠标在文件系统中的设备节点进行读写,来完成模拟操作。我们首先来看一下键盘和鼠标的设备节点:
我们可以通过下面命令来查看键盘设备和鼠标设备的event编号:
由此看来,键盘设备为event4,鼠标设备为event3。
键盘和鼠标的操作事件被定义在input.h当中,其结构体如下:
struct input_event {
u16 type;
u16 code;
s32 value;
};
其中type表示事件类型,在这里我们只使用EV_KEY,code为实际键盘和鼠标的按键码:
#define KEY_RESERVED 0
#define KEY_ESC 1
#define KEY_1 2
#define KEY_2 3
#define KEY_3 4
#define KEY_4 5
#define KEY_5 6
#define KEY_6 7
#define KEY_7 8
#define KEY_8 9
#define KEY_9 10
#define KEY_0 11
#define KEY_MINUS 12
#define KEY_EQUAL 13
#define KEY_BACKSPACE 14
#define KEY_TAB 15
#define KEY_Q 16
#define KEY_W 17
#define KEY_E 18
#define KEY_R 19
#define KEY_T 20
#define KEY_Y 21
#define KEY_U 22
#define KEY_I 23
#define KEY_O 24
#define KEY_P 25
#define KEY_LEFTBRACE 26
#define KEY_RIGHTBRACE 27
#define KEY_ENTER 28
#define KEY_LEFTCTRL 29
#define KEY_A 30
#define KEY_S 31
#define KEY_D 32
#define KEY_F 33
#define KEY_G 34
#define KEY_H 35
#define KEY_J 36
#define KEY_K 37
#define KEY_L 38
#define KEY_SEMICOLON 39
#define KEY_APOSTROPHE 40
#define KEY_GRAVE 41
#define KEY_LEFTSHIFT 42
#define KEY_BACKSLASH 43
#define KEY_Z 44
#define KEY_X 45
#define KEY_C 46
#define KEY_V 47
#define KEY_B 48
#define KEY_N 49
#define KEY_M 50
#define KEY_COMMA 51
#define KEY_DOT 52
#define KEY_SLASH 53
#define KEY_RIGHTSHIFT 54
#define KEY_KPASTERISK 55
#define KEY_LEFTALT 56
#define KEY_SPACE 57
#define KEY_CAPSLOCK 58
#define KEY_F1 59
#define KEY_F2 60
#define KEY_F3 61
#define KEY_F4 62
#define KEY_F5 63
#define KEY_F6 64
#define KEY_F7 65
#define KEY_F8 66
#define KEY_F9 67
#define KEY_F10 68
#define KEY_NUMLOCK 69
#define KEY_SCROLLLOCK 70
#define KEY_KP7 71
#define KEY_KP8 72
#define KEY_KP9 73
#define KEY_KPMINUS 74
#define KEY_KP4 75
#define KEY_KP5 76
#define KEY_KP6 77
#define KEY_KPPLUS 78
#define KEY_KP1 79
#define KEY_KP2 80
#define KEY_KP3 81
#define KEY_KP0 82
#define KEY_KPDOT 83
#define BTN_MOUSE 0x110
#define BTN_LEFT 0x110
#define BTN_RIGHT 0x111
#define BTN_MIDDLE 0x112
#define BTN_SIDE 0x113
#define BTN_EXTRA 0x114
#define BTN_FORWARD 0x115
#define BTN_BACK 0x116
#define BTN_TASK 0x117
...
//其他略
value为按键实际的值,对于键盘和鼠标的按键来说0表示未按下,1表示按下;对于鼠标的移动事件可以表示鼠标的相对位置,于是我们就可以编写程序来将游戏手柄的按键模拟为键盘和鼠标的按键了,具体程序如下:
void* joy_to_input(void *arg)
{
dev_s *devs = (dev_s *) arg;
if (devs->dev_j == NULL)
{
goto _out;
}
if (devs->dev_k == NULL)
{
goto _out;
}
if (devs->dev_m == NULL)
{
goto _out;
}
inv_evt_s *evts = malloc(sizeof(inv_evt_s) * KEY_COUNT);
if (evts == NULL)
{
printf("malloc evts error.\n");
goto _out;
}
load_conf_file(devs->conf, evts);
int fd_joy = open(devs->dev_j, O_RDWR | O_NONBLOCK);
if (fd_joy < 0)
{
printf("could not open %s.\n", devs->dev_j);
goto _out;
}
int fd_kbd = open(devs->dev_k, O_RDWR | O_NONBLOCK);
if (fd_kbd < 0)
{
printf("could not open %s.\n", devs->dev_k);
goto _out;
}
int fd_mouse = open(devs->dev_m, O_RDWR | O_NONBLOCK);
if (fd_mouse < 0)
{
printf("could not open %s.\n", devs->dev_m);
goto _out;
}
inv_evt_s event;
inv_evt_s event_ctl_shift;
event.type = EV_SYN;
event.code = SYN_REPORT;
event.value = 0;
uint16_t *btns = malloc(sizeof(uint16_t) * KEY_COUNT);
uint8_t ctl_key = 0;
uint8_t ctl_cnt = 0;
uint8_t shift_key = 0;
uint8_t shift_cnt = 0;
uint16_t *press_key = malloc(sizeof(uint16_t) * KEY_COUNT);
uint16_t *press_cnt = malloc(sizeof(uint16_t) * KEY_COUNT);
memset(press_key, 0, sizeof(uint16_t) * KEY_COUNT);
memset(press_cnt, 0, sizeof(uint16_t) * KEY_COUNT);
while (1)
{
int cnt = get_curr_joy_btn(fd_joy, btns);
if (cnt != 0)
{
for (int i = 0; i < cnt; i++)
{
uint16_t btn = btns[i];
if (evts[btn].type == EV_KEY
&& (evts[btn].code < 0x110 || evts[btn].code > 0x117))
{
if (evts[btn].code == KEY_1
|| evts[btn].code == KEY_2
|| evts[btn].code == KEY_3
|| evts[btn].code == KEY_4)
{
evts[btn].value = 1;
write(fd_kbd, &(evts[btn]), sizeof(inv_evt_s));
write(fd_kbd, &event, sizeof(inv_evt_s));
evts[btn].value = 0;
write(fd_kbd, &(evts[btn]), sizeof(inv_evt_s));
write(fd_kbd, &event, sizeof(inv_evt_s));
}
else
{
press_cnt[evts[btn].code] = 5;
if (press_key[evts[btn].code] == 0)
{
press_key[evts[btn].code] = 1;
evts[btn].value = 1;
write(fd_kbd, &(evts[btn]), sizeof(inv_evt_s));
write(fd_kbd, &event, sizeof(inv_evt_s));
}
}
}
if (evts[btn].type == EV_KEY && evts[btn].code >= 0x110 && evts[btn].code <= 0x117)
{
if (evts[btn].code == BTN_LEFT || evts[btn].code == BTN_RIGHT)
{
press_cnt[evts[btn].code] = 5;
if (press_key[evts[btn].code] == 0)
{
press_key[evts[btn].code] = 1;
evts[btn].value = 1;
write(fd_mouse, &(evts[btn]), sizeof(inv_evt_s));
write(fd_mouse, &event, sizeof(inv_evt_s));
}
}
else
{
evts[btn].value = 1;
write(fd_mouse, &(evts[btn]), sizeof(inv_evt_s));
write(fd_mouse, &event, sizeof(inv_evt_s));
evts[btn].value = 0;
write(fd_mouse, &(evts[btn]), sizeof(inv_evt_s));
write(fd_mouse, &event, sizeof(inv_evt_s));
}
}
if (evts[btn].type == EV_REL)
{
write(fd_mouse, &(evts[btn]), sizeof(inv_evt_s));
write(fd_mouse, &event, sizeof(inv_evt_s));
}
}
}
for (uint16_t i = 0; i < KEY_COUNT; i++)
{
if (press_cnt[i] != 0)
{
press_cnt[i]--;
}
else if (press_key[i] == 1)
{
press_key[i] = 0;
event_ctl_shift.type = EV_KEY;
event_ctl_shift.code = i;
event_ctl_shift.value = 0;
if (i == BTN_LEFT || i == BTN_RIGHT)
{
write(fd_mouse, &event_ctl_shift, sizeof(inv_evt_s));
write(fd_mouse, &event, sizeof(inv_evt_s));
}
else
{
write(fd_kbd, &event_ctl_shift, sizeof(inv_evt_s));
write(fd_kbd, &event, sizeof(inv_evt_s));
}
}
}
usleep(10 * 1000);
}
_out: ;
return NULL;
}
四、运行魔兽世界
万事具备,只欠东风。现在我们来在Linux上运行魔兽世界游戏,这款游戏从上市以来到今天,以及可遇见的未来,一直只支持MacOS和Windows这两款操作系统,而对于想作者这样的Linux爱好者来说只好使用Wine、PlayOnLinux或CrossOver来运行Windows下的游戏软件,由于原声Wine需要进行非常多的自定义配置,并且CrossOver是收费的,因此在这里我们推荐使用PlayOnLinux,对于不同的Linux发行版,安装PlayOnLinux的指令略有不同,例如:
ArchLinux: pacman -S playonlinux
Fedora: yum install playonlinux
Ubuntu: apt-get install playonlinux
安装完之后启动PlayOnLinux,然后点击Install a none-listed program,根据提示创建虚拟磁盘驱动器,并下载安装暴雪战网平台,如下图所示:
安装成功之后,在PlayOnLinux的主界面中会出现Battle.net运行图表,将其选中并执行Run,即可打开战网,之后跟在Windows下一样,可以安装并运行魔兽世界游戏:
进入游戏,打开我们的游戏手柄模拟键盘鼠标程序,将游戏手柄中指定的按键,作为游戏中特定的快捷键即可,最后我们来看看作者的运行效果。
Copyright © 2015-2023 问渠网 辽ICP备15013245号