如何爲你的 iOS App/遊戲快速適配主機手柄?

iOS 13 支持主機手柄鏈接了!!!須要注意的是,並非手柄去支持 iOS 13,而是 Apple 終於容許自家平臺「發現」手柄了......ios

前言

在 WWDC19 上,apple 正式對外宣稱 iOS,macOS,tvOS 三大平臺正式支持接入遵照 MFi 協議的手柄進行操做,接入方式爲「藍牙」。git

所以,咱們能夠推測出從此在 Apple 生態圈中游戲發佈在以上三個平臺時都會必定去考慮對手柄的支持。仔細觀察目前在 Apple 生態圈中的遊戲,尤爲是 iOS 上的遊戲,或者說是移動平臺上的遊戲,幾乎都逃不掉左邊一個虛擬滾輪,右邊三四個虛擬按鍵。github

能夠說,這些虛擬操做都是在模擬最初手柄對用戶已經養成的習慣,這些習慣既然是從實體手柄時代遺傳下來的,而且在 iOS 13 以前 Apple 並不容許接入其餘手柄,那如今已經容許接入的前提下,咱們爲什麼不順勢而爲呢?swift

還有一種說法,部分遊戲會故意經過虛擬按鍵位置的排布來消減玩家的遊戲體驗,以下降玩家完成某個要求的速度和質量,這就是 QWERTY 的現代玩法。app

選擇合適的手柄

我已經有了 Switch,想固然的再入一個 NS Pro Controller,但幸好事先作了調查,發現 NS Pro Controller 和 joy-Controller 均不支持鏈接,我的猜想老任並無爲其手柄申請 MFi 認證。框架

所以,只能回落到 PS4 仍是 Xbox 的手柄選擇上。在最終下單前,也作一番調查,發現你們基本上一窩蜂的倒向 Xbox 手柄手感比 PS4 強太多,原本我也打算確實是選擇 Xbox,後來發現 Apple 官網上的廣告宣傳是 PS4,並且 B 站上衆多 up 主也都基於 PS4 進行測評,想一想最終仍是選擇了 PS4 手柄。ide

到如今總共玩了大概三個小時不到,發現手柄和設備的鏈接很是妥當,我的認爲無延遲,但 PS4 手柄還真出現了網友所說玩久了左手大拇指會疼,由於常常會使用半個大拇指去「搓」滑桿。工具

但看到 PS4 手柄的顏值也就忍了,若是你也想買一個手柄且不是顏值黨,能夠考慮選擇 Xbox。spa

適配主機手柄

手柄的差別

apple 的官方文檔中推薦咱們的 app/遊戲(下文統一使用 app)必定不能只支持手柄,其實也就是說,手柄只是一個提高體驗的工具,而不是一個必須品,只要 apple 不下死命令,咱們可以經過手柄體會到的遊戲就會越日後拖。code

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 遊戲開發

參考連接

相關文章
相關標籤/搜索