Apple Engine

Apple, iPhone, iOS, その周辺のことについて

WWDC 2017 の SceneKit サンプル Fox 2 を調べる その35

引き続き GameController クラスの残りの関数を見てゆく。

今回はゲームコントローラーやゲーム操作部分の関数について。

 

その前にゲームコントローラーとは?

販売されている MFi のアクセサリのゲームコントローラーは 、上下左右の方向キー、ボタン4つ、アナログスティック2本とその押し込みの入力、LRボンタン各2つの計4つ、ポーズ(ホーム)ボタンの入力となっているが、
Apple はゲームコントローラーの種類は以下のプロファイルで定義されている。

  • Extended Gamepad プロファイル
  • Micro Gamepad プロファイル
  • Motion プロファイル

 

Extended Gamepad

拡張されたゲームパッドで Bluetooth で接続されたもので iOS, tvOS, macOS で使用できる。
Lightning で接続するゲームパッドもこれにあたり iOS のみで使用可能。

無線のものはバッテリ残量を表示するインジケーターが必要。

f:id:x67x6fx74x6f:20180604030928p:plain f:id:x67x6fx74x6f:20180604031029p:plain

 

Micro Gamepad

Apple TV の Siri Remote を指す。Extended Gamepad と操作できるものの数が違うため併用するが難しくなることがある。
ゲームパッドのドキュメントでは tvOS アプリで Extended Gamepad のみの対応でも可能としているが、できるだけ Siri Remote ので操作できるようにすべしと書かれている。

f:id:x67x6fx74x6f:20180604031131p:plain

 

Motion

加速度センサーなどのコントロール。Siri Remote もこれを使用できる。

 

その他

現在 MFi のコントローラーで販売されておらず、非推奨の Gamepad プロファイルがある。 以前は Logicool(Logitech) で販売を行なっており、上下左右の方向キー、ボタン4つ、LRボンタン各1つの計2つ、ポーズ(ホーム)ボタンとなっている。

 

それではコードの方を見てゆく。

 

ゲームコントローラーからの通知

ゲームコントローラーは、無線、有線にかかわらず端末との接続、切断が行われるので通知の処理が行われる。

ゲームコントローラーが接続された場合、registerGameController 関数に GCController を渡す処理。 ヴーチャルパッドが無ければ消し、GCController が無ければ処理をしない。

@objc
func handleControllerDidConnect(_ notification: Notification) {
    if gamePadCurrent != nil {
        return
    }
    guard let gameController = notification.object as? GCController else {
        return
    }
    registerGameController(gameController)
}

 

ゲームコントローラーの接続が切れたら処理を止める。 他のコントローラーが接続されたら registerGameController で処理をし直す。

@objc
func handleControllerDidDisconnect(_ notification: Notification) {
    guard let gameController = notification.object as? GCController else {
        return
    }
    if gameController != gamePadCurrent {
        return
    }

    unregisterGameController()

    for controller: GCController in GCController.controllers() where gameController != controller {
        registerGameController(controller)
    }
}

 

ゲームコントローラーの登録処理

ゲームパッドの処理は以下のようになっている

  • buttonA、buttonB を GCControllerButtonInput として設定。
  • Extended Gamepad、Gamepad、Micro Gamepad のプロファイルで分岐
  • Gamepad と Micro Gamepad のプロファイルではプレイヤーキャラクター移動の gamePadLeft 変数に dpad(Directional Pad/方向キー)、buttonA、buttonB 変数に A/B(X) ボタンが割り当てされる
  • Extended Gamepad では gamePadLeft 変数に leftThumbstick、視点操作の gamePadRight 変数に rightThumbstick を割り当てられている
  • GameController 自身である self を weak で宣言する weakController を設定
  • gamePadLeft、buttonA、buttonB と gamePadRight があれば、各 valueChangedHandler で strongController に weakController を入れ、キャラクターの移動、ジャンプ、攻撃やカメラ視点変更を設定する。GCController のから変更があると strongController の各命令にその値が渡される。
  • #if で iOS の場合で gamePadLeft が空でないの場合、ゲームコントローラーが接続されているので、Overlay からヴァーチャルパッドを非表示にする
func registerGameController(_ gameController: GCController) {

    var buttonA: GCControllerButtonInput?
    var buttonB: GCControllerButtonInput?

    if let gamepad = gameController.extendedGamepad {
        self.gamePadLeft = gamepad.leftThumbstick
        self.gamePadRight = gamepad.rightThumbstick
        buttonA = gamepad.buttonA
        buttonB = gamepad.buttonB
    } else if let gamepad = gameController.gamepad {
        self.gamePadLeft = gamepad.dpad
        buttonA = gamepad.buttonA
        buttonB = gamepad.buttonB
    } else if let gamepad = gameController.microGamepad {
        self.gamePadLeft = gamepad.dpad
        buttonA = gamepad.buttonA
        buttonB = gamepad.buttonX
    }

    weak var weakController = self

    gamePadLeft!.valueChangedHandler = {(_ dpad: GCControllerDirectionPad, _ xValue: Float, _ yValue: Float) -> Void in
        guard let strongController = weakController else {
            return
        }
        strongController.characterDirection = simd_make_float2(xValue, -yValue)
    }

    if let gamePadRight = self.gamePadRight {
        gamePadRight.valueChangedHandler = {(_ dpad: GCControllerDirectionPad, _ xValue: Float, _ yValue: Float) -> Void in
            guard let strongController = weakController else {
                return
            }
            strongController.cameraDirection = simd_make_float2(xValue, yValue)
        }
    }

    buttonA?.valueChangedHandler = {(_ button: GCControllerButtonInput, _ value: Float, _ pressed: Bool) -> Void in
        guard let strongController = weakController else {
            return
        }
        strongController.controllerJump(pressed)
    }

    buttonB?.valueChangedHandler = {(_ button: GCControllerButtonInput, _ value: Float, _ pressed: Bool) -> Void in
        guard let strongController = weakController else {
            return
        }
        strongController.controllerAttack()
    }

#if os( iOS )
    if gamePadLeft != nil {
        overlay!.hideVirtualPad()
    }
#endif

}

 

端末とゲームコントローラーの接続が切れたら、gamePadLeft、gamePadRight、gamePadCurrent を空にして、ヴァーチャルパッドを表示する。

func unregisterGameController() {
        gamePadLeft = nil
        gamePadRight = nil
        gamePadCurrent = nil
#if os( iOS )
        overlay!.showVirtualPad()
#endif
    }

 

iOS の場合のバーチャルパッドの操作設定。 プロトコルから設定した PadOverlayDelegate のデリゲートで バーチャルパッド(スティック)の操作の始まりと変更、変更後の際の操作を設定する

ButtonOverlayDelegate のデリゲートでバーチャルパッドでのボタンを押した際と押し終えた処理を行う。 処理はジャンプと攻撃だが、押し終えた場合ジャンプの2度しないように止めている。

#if os( iOS )
func padOverlayVirtualStickInteractionDidStart(_ padNode: PadOverlay) {
    if padNode == overlay!.controlOverlay!.leftPad {
        characterDirection = float2(Float(padNode.stickPosition.x), -Float(padNode.stickPosition.y))
    }
    if padNode == overlay!.controlOverlay!.rightPad {
        cameraDirection = float2( -Float(padNode.stickPosition.x), Float(padNode.stickPosition.y))
    }
}

func padOverlayVirtualStickInteractionDidChange(_ padNode: PadOverlay) {
    if padNode == overlay!.controlOverlay!.leftPad {
        characterDirection = float2(Float(padNode.stickPosition.x), -Float(padNode.stickPosition.y))
    }
    if padNode == overlay!.controlOverlay!.rightPad {
        cameraDirection = float2( -Float(padNode.stickPosition.x), Float(padNode.stickPosition.y))
    }
}

func padOverlayVirtualStickInteractionDidEnd(_ padNode: PadOverlay) {
    if padNode == overlay!.controlOverlay!.leftPad {
        characterDirection = [0, 0]
    }
    if padNode == overlay!.controlOverlay!.rightPad {
        cameraDirection = [0, 0]
    }
}

func willPress(_ button: ButtonOverlay) {
    if button == overlay!.controlOverlay!.buttonA {
        controllerJump(true)
    }
    if button == overlay!.controlOverlay!.buttonB {
        controllerAttack()
    }
}

func didPress(_ button: ButtonOverlay) {
    if button == overlay!.controlOverlay!.buttonA {
        controllerJump(false)
    }
}
#endif

 

次回は Overlay.swift を見てゆく。