跨平台游戏手柄适配踩坑记

快乐杆使我快乐


正在进行的小项目 Wandering Melody 的主催提出想做手柄支持,感觉是个好主意,于是踩入了这个坑。鬼知道这个坑有这么大啊!!

将心路历程记录在此,姑且作为一个综述吧。也不知道会不会有用。

游戏手柄前置知识 🕹️

操纵杆(Joystick)

操纵杆可以指各种输入设备(并不一定是标准的手柄),其状态可归为如下几类:

  1. 元数据
    • 名字
    • 厂商 ID(Vendor ID)
    • 产品 ID(Product ID)
    • 可能也有版本之类的其他信息
  2. 按钮(Button)状态 —— 均为 0/1
  3. 控制轴(Axis,复数 Axes!)状态 —— 浮点数,一般从 -1 到 +1

当然,有时候还有重力传感器,以及震动、LED 之类的输入,不过暂时不是重点啦。

作为例子,PlayStation 4 的手柄 DualShock 4 长成下面这个样子:

它的元数据:

  • 名字: Wireless Controller(这也太草率了叭…… 人类世界的全称是「Sony Interactive Entertainment Wireless Controller」)
  • Vendor ID: 0x54c = 1356
  • Product ID: 0x9cc = 2508

它共有 18 个按钮、6 个控制轴,编号如下:

  • Buttons 0, 1, 2, 3: □✕○△ 按钮
  • Buttons 4, 5, 6, 7: L1, R1, L2, R2 按钮
  • Buttons 8, 9: SHARE, OPTIONS 两个药丸形状的小按钮
  • Buttons 10, 11: 左右指摇杆,没错这两个是可以按下去的……
  • Button 12: 两个摇杆之间的「PS」按钮
  • Button 13: 「PS」上面的一大块长方形,这个也是可以按的……
  • Buttons 14, 15, 16, 17: ↑→↓← 四个方向按钮
  • Axes 0, 1: 左摇杆横纵方向坐标
  • Axes 2, 5: 右摇杆横纵方向坐标
  • Axes 3, 4: L2, R2 下压位置

从上面可以看到这些输出并不都是独立的,比如有些时候同一个按钮既对应一个 Axis 也对应一个 Button,L2 和 R2 即属于此类。

这个网站 可以显示操纵杆的各种状态。

控制器(Gamepad / Controller)

Joystick 的输入是原始数据,而世界上有那么多不同的操纵杆,Buttons 和 Axes 的设置和编号顺序天差地别,专门针对每一种做适配当然不可行。于是机智的开发者们想到了「抽象」:游戏手柄上常用的按钮就那么几种,把所有厂商所有产品所有型号的按钮全部对应到一套固定的名字,游戏只要考虑这些名字就可以啦!

映射过后的这一套名字便称作控制器。虽然 Joystick 和 Controller 从字面意思上并没有太大区别,不过开发者已经约定了这样的称呼,在开发过程中需要注意区分。本文中需要区分的地方均使用英文名称。

大部分接口遵循 Xbox 的设定,控制器由下面的部分组成:

  1. 四个按钮 A、B、X、Y(少见 C 和 Z),或称 ✕○□△;
  2. 左右两个肩键(Shoulders / Bumpers),或称 L1、R1;
  3. 左右两个扳机键(Triggers),或称 L2、R2;
  4. 左右两个摇杆(Thumbsticks);
  5. 一组十字键(D-pad);
  6. 若干辅助按钮,常见名字有 Start、Select、Back、Options 等。

从 Joystick 到 Controller 需要有一个映射,它可以通过 Vendor ID 和 Product ID 确定。而我们的目标也能由此分成两个明确的子任务:

  1. 实现跨平台读取 Joystick 输入;
  2. 尽可能广泛地将 Joystick 输入映射为 Controller。

好了,Game Start!

方案 0:Cocos

Wandering Melody 是在 Cocos2d-x 引擎下实现的,自然希望在内部直接解决啦。可惜事实总是不尽如人意,在 macOS 上只能识别出 18 个按钮中的 6 个(△○✕□, L1, R1)。

究其本源,Cocos 在 macOS 上调用的是 Apple 的 Game Controller 框架,而目前的版本(macOS 10.13.5, GameController 1.0)对 DS4 的映射是不全的。

由于我们并不知道 Apple 的框架实现,所以没法追踪下去了。可惜啦(´ - `).。oO(

方案 1:SDL

SDL 是个老牌的多媒体程序库,其中的控制器模块也经过了相当长时间的积淀,是一个身经百战的成熟模块。

源代码可以看到,SDL 在 macOS 上通过 IOKit 提供的接口读取 USB HID 硬件,这样得到的数据便是原始的 Joystick 数据。此后,通过一个超大的数据库查找对应型号的设备,完成 Joystick 到 Controller 的映射。(我们的 DS4 在第 321 行,开头的一长串十六进制标识符其实就是「Vendor 为 0x54c、Product 为 0x9cc 的 USB 设备」的意思~ 处理这一串标识符的源代码在这儿

另外,SDL 还包括了震动功能(macOS 上通过 Force Feedback 框架实现),非常完善。可是对于某些固执的 coders 来说,增加一个如此复杂的依赖项总是令人不禁蹙眉。有没有别的办法呢?

方案 2:GLFW

其实 SDL 的手柄数据库有自己的 GitHub 项目 SDL_GameControllerDB,接入其他项目非常容易。

这不,GLFW 在 3.3 版本引入了 SDL_GameControllerDB,提供了一套跨平台的 Controller 接口。当然,也可以用原始的 Joystick 数据应对各种情况。从源码看,GLFW 在 macOS 上也选择了 IOKit 的 HID 接口来读取硬件。

相比 SDL,GLFW 更加轻量,接口简单,并通过 SDL_GameControllerDB 全面支持各种手柄的映射,是对接其他框架的优秀选择。写了一个超级简单的程序,显示原始 Joystick 数据和映射后的 Controller 数据,放在 Gist 上,非常简短,而且 Joystick 成功识别了全部 18 个按钮。

另外,GLFW 的手柄震动支持也正在鹿上

至此,对接手柄的任务大功告成啦!

…… Or is it?

仔细观察会发现,SDL_GameControllerDB 并没有考虑 Trigger 按钮和其他功能按钮的存在,这也导致 DS4 的按钮仍有 3 个(L2、R2 和长方形大按钮)在 Controller 中没有对应的映射。革命尚未成功哦 (ง •̀_•́)ง

方案 3:RetroArch

RetroArch 是跨平台游戏与媒体接口标准 libretro 的一个前端实现。

我们只关注它的手柄适配原理。RetroArch 有若干个 drivers,通过不同接口读取硬件,每一个都可以在一个或多个平台上运行。macOS 版本只有一个名为 hid 的 driver(它也是通过 IOKit 实现的!)。

不过 driver 的复杂细节都不重要啦。RetroArch 与之前几个方案最大的不同在于,它可以将不同的按钮对应到控制器上标示的名称。例如接入 Xbox 手柄时,玩家希望看见「ABXY」的标识;而用 PlayStation 手柄时,自然希望看见「△○✕□」。RetroArch 完成了这个提升游戏体验的功能。

RetroArch 的这个奇妙功能归功于它的超超详细数据库,里面针对每个 driver 维护了一份手柄按钮映射表(之所以要分 driver 是因为不同接口读取到的按键编号并不一定相同,此前的 SDL 数据库实际上也是如此),而且标明了按钮在控制器上的名称。DS4 的这一份记录简直是不能再详细了 (´▽`)

RetroArch 彻底解决了跨平台手柄适配的问题,不过它同样过于庞大,难以作为单独的模块整合进其他程序中。

方案 4:Steamworks

Valve 的 Steamworks API 有一个游戏控制器模块,完成了更加全面的功能(映射到动作图标震动LED),而且容易在程序中对接。对于 Steam 平台的游戏,是完美的解决方案。

不过它并不开放,而且由于需要读取 Steam 的设置,在非 Steam 平台的游戏上不太适用(虽然据说已经受到关注)。

几篇相关的文章倒是很有意思:《PC 游戏中控制器的使用情况》分析 Steam 玩家使用控制器的情况,一定程度上也代表了桌面玩家群体;Steam Dev Days: Steam Controller 后半部分介绍的几项控制器交互设计原则,虽然简单却很值得注意。

方案 5:Hack!

既要跨平台又要跨设备,控制器适配真的好难啊 (´ Д ` )

回顾开头的两个问题:

  1. 实现跨平台读取 Joystick 输入;
  2. 尽可能广泛地将 Joystick 输入映射为 Controller。

其中第一个子任务已经由 GLFW 解决,第二个子任务已经由 RetroArch 的数据库解决。至于它们之间的对接(接口实现之间可能仍存在微小偏差?),可以采用算法寻找最佳匹配,外加玩家手动设定的支持,应当可以制造一个非常优秀的解决方案!

然而这个方案还是只在梦里才有呢 (=﹃=)~

其他框架

其他游戏引擎都是怎么实现的呢?

Godot 同样通过 IOKit 读取数据利用 SDL 数据库映射

MonoGame 也是直接读取 SDL 数据库

FNA 作为 MonoGame 的近亲当然也不例外

Unity 让玩家在启动游戏时自行设置「按钮」到「行为」的映射(当然开发者可以提供一个预设)。少数的不将 Joystick 映射到 Controller 的引擎,直接以 button 0, 1, 2… 的形式命名

值得一提的是,游戏《蔚蓝山脉》尽管基于 MonoGame(which 基于 SDL),却仍能识别 DS4 的 L2 和 R2 按钮,并且按照手柄上的标识来显示图标。可能是手动做的适配吧 hhhhh

所以最后选择了啥?

折腾了若干天之后,决定 GLFW 是目前最适合 Wandering Melody 的方案。于是手动将 Cocos2d-x 内置的 GLFW 升级到了 3.3,并且在此基础上重新实现了 Cocos2d-x 的控制器事件处理,成功在 macOS 和 Windows 上运行,识别了 18 个 DS4 按钮中的 15 个

这个方案有三个缺陷,其一是按钮名称不够灵活(全部称为 A, B, X, Y, Start, Back),其二是不能处理未被映射的按钮(或许可以作为 GLFW 的功能提议?),其三是缺少重力传感器、震动、LED 等附加功能的支持。

不过对于一个音游来说,或许已经足够了吧……

这些更改也已经发了 PR(GLFW 3.3重写逻辑),希望能对大家都有用~

时隔五年半,又给 Cocos2d-x 发起了 PR,是很奇妙的感觉呢。

以上。

鬼知道这个坑有这么大啊!!


TODO

  • 上面那个 GLFW 的功能提议真的可以考虑一下啊 (/ω\)
作者:Shiqing
链接:https://kawa.moe/2019/07/crossplat-joystick/
版权:文章在 CC BY-NC-SA 4.0 许可证下发布。转载请注明来自 quq