如何为你的 iOS App/游戏快速适配主机手柄? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
pjhubs
V2EX    程序员

如何为你的 iOS App/游戏快速适配主机手柄?

  •  
  •   pjhubs
    windstormeye 2019-12-01 10:26:00 +08:00 2477 次点击
    这是一个创建于 2144 天前的主题,其中的信息可能已经有所发展或是发生改变。

    iOS 13 支持主机手柄连接了!!!需要注意的是,并不是手柄去支持 iOS 13,而是 Apple 终于允许自家平台「发现」手柄了......

    前言

    在 WWDC19 上,apple 正式对外宣称 iOS,macOS,tvOS 三大平台正式支持接入遵守 MFi 协议的手柄进行操作,接入方式为「蓝牙」。

    因此,我们可以推测出今后在 Apple 生态圈中游戏发布在以上三个平台时都会一定去考虑对手柄的支持。仔细观察目前在 Apple 生态圈中的游戏,尤其是 iOS 上的游戏,或者说是移动平台上的游戏,几乎都逃不掉左边一个虚拟滚轮,右边三四个虚拟按键。

    可以说,这些虚拟操作都是在模拟最初手柄对用户已经养成的习惯,这些习惯既然是从实体手柄时代遗传下来的,并且在 iOS 13 之前 Apple 并不允许接入其他手柄,那现在已经允许接入的前提下,我们为何不顺势而为呢?

    还有一种说法,部分游戏会故意通过虚拟按键位置的排布来消减玩家的游戏体验,以降低玩家完成某个要求的速度和质量,这就是 QWERTY 的现代玩法。

    选择合适的手柄

    我已经有了 Switch,想当然的再入一个 NS Pro Controller,但幸亏事先做了调查,发现 NS Pro Controller 和 joy-Controller 均不支持连接,个人猜测老任并没有为其手柄申请 MFi 认证。

    因此,只能回落到 PS4 还是 Xbox 的手柄选择上。在最终下单前,也做一番调查,发现大家基本上一窝蜂的倒向 Xbox 手柄手感比 PS4 强太多,本来我也打算确实是选择 Xbox,后来发现 Apple 官网上的广告宣传是 PS4,而且 B 站上众多 up 主也都基于 PS4 进行测评,想想最终还是选择了 PS4 手柄。

    到现在总共玩了大概三个小时不到,发现手柄和设备的连接非常稳当,个人认为无延迟,但 PS4 手柄还真出现了网友所说玩久了左手大拇指会疼,因为经常会使用半个大拇指去「搓」滑杆。

    但看到 PS4 手柄的颜值也就忍了,如果你也想买一个手柄且不是颜值党,可以考虑选择 Xbox。

    适配主机手柄

    手柄的差异

    apple 的官方文档中推荐我们的 app/游戏(下文统一使用 app )一定不能只支持手柄,其实也就是说,手柄只是一个提升体验的工具,而不是一个必须品,只要 apple 不下死命令,我们能够通过手柄体会到的游戏就会越往后拖。

    PS4 和 Xbox 的手柄可以理解为是两种风格,如下图所示:

    PS4 和 Xbox 手柄的 ABXY 分别使用不同的风格进行标识。但如果你有使用过这两个手柄,一定能够知道大部分的手柄都是基于「位置等量」的,也就是说,PS4 上的 X 会被「位置等量」到 Xbox 上的 A,二者对于开发者来说都是一样的功能。

    这一点同样被 Apple 所保留,但我们在使用时 Apple 并不会给我们识别出当前在 A 这个位置上到底是 X 还是 A,Apple 只有 A

    键冲突

    Apple 会自动协助我们在某个「固定时间」内保留最终的按键状态,而不是反复发送按键回调。

    UI

    这部分 Apple 要求得比较多,简单来说,要么全部统一,在 UI 上也做「位置等量」的标识,要不然就得通过连接设备的标识符来针对性的返回不同的 UI 资源。

    接入

    了解了以上主机手柄适配的前置内容,接下来就可以正式进入到手柄适配部分。我在适配的过程中,彻底被 Apple 的简洁所折服了!我以为对主机手柄各种硬件的调用过程会类似于 Photos 这种看似不应该那么复杂的却非常复杂的框架一样复杂,但简洁得令人感到惊讶。

    对主机手柄接入的所有工作只需要 GameController 即可完成,而且该框架与平台无关,也就是说,只要我们封装好一次对外暴露的主机手柄操作管理类,可以完全做到三平台全通吃!

    第一步:注册通知

    第一步,我们得先注册对手柄连接 /取消连接的事件通知。

    import GameController class GameController { init() { NotificationCenter.default.addObserver(self, selector: .didConnect, name: .GCControllerDidConnect, object: nil) NotificationCenter.default.addObserver(self, selector: .didConnect, name: .GCControllerDidDisconnect, object: nil) } } 

    第二步

    在通知的回调方法中完成对事件的筛选和处理。

    extension GameController { @objc fileprivate func gameControllerDidConnect() { for controller in GCController.controllers() { if controller.extendedGamepad != nil { setupControllerControls(controller: controller) } } } @objc fileprivate func gameControllerDidDisconnect() { } func setupControllerControls(controller: GCController) { controller.extendedGamepad?.valueChangedHandler = { (gamepad: GCExtendedGamepad, element: GCControllerElement) in self.controllerInput(gamePad: gamepad, element: element) } } private func controllerInput(gamePad: GCExtendedGamepad, element: GCControllerElement) { } } 

    第三步

    controllerInput 中处理手柄的 valueChangedHandler 回调事件。valueChangedHandler 这个方法需要我们在接收到手柄的连接通知时,传递给手柄控制对象一个回调方法,后续当手柄发生按键事件时,将通过我们传入的回调进行调用。

    因此,我们在 controllerInput 方法中处理手柄被按下时的各种事件值的改变处理。

    extension GameController { @objc fileprivate func gameControllerDidConnect() { for controller in GCController.controllers() { if controller.extendedGamepad != nil { setupControllerControls(controller: controller) } } } @objc fileprivate func gameControllerDidDisconnect() { } func setupControllerControls(controller: GCController) { controller.extendedGamepad?.valueChangedHandler = { (gamepad: GCExtendedGamepad, element: GCControllerElement) in self.controllerInput(gamePad: gamepad, element: element) } } private func controllerInput(gamePad: GCExtendedGamepad, element: GCControllerElement) { if (gamePad.leftThumbstick == element) { if (gamePad.leftThumbstick.yAxis.value != 0 && !movingY && !movingX) { movingY = true isSelectY?(gamePad.leftThumbstick.yAxis.value > 0) return } else if (gamePad.leftThumbstick.yAxis.value == 0) { movingY = false } if (gamePad.leftThumbstick.xAxis.value != 0 && !movingX && !movingY) { isSelectX?(gamePad.leftThumbstick.xAxis.value > 0) movingX = true return } else if (gamePad.leftThumbstick.xAxis.value == 0) { movingX = false } } if (gamePad.buttOnA== element) { if (gamePad.buttonA.value != 0) { isTapButtonA?() } } // ... } 

    valueChangedHandler 回调只是当数值发生变化时的回调,我们还需要手动处理例如手柄摇杆上从按住到归位的单次计数,当然,我做这个是为了适配我的游戏,这部分还没来得及完善,如果你想持续的监听到摇杆的持续按住事件,完全可以把数值对外暴露,通过系数倍乘的方式来达到例如对赛车的油门控制等好玩的事情。

    对外,可以通过注册的回调进行传递单次按键事件。

    class GameController { var movingX = false var movingY = false var isSelectX: ((Bool) -> ())? var isSelectY: ((Bool) -> ())? var isTapButtonA: (() -> ())? //... } // ... 

    总结

    我确实没想到对手柄的第一步适配工作居然不到 80 行代码搞定了。手柄是一个玩家的武器,同时也是一个标志,希望大家都能够正视在自己的游戏中支持手柄操作,提供更好的游戏体验。

    如果你对我的第一个适配手柄的游戏「能否关个灯」感兴趣,可以在 github 上找到这个项目,如果你想跟我一起通过 Swift 进行游戏开发,可以在小专栏上找到《 Swift 游戏开发》进行。

    github 地址:Swift 游戏开发

    小专栏地址:Swift 游戏开发

    参考链接

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     980 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 22:36 PVG 06:36 LAX 15:36 JFK 18:36
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86