Apple Engine

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

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

iOS 用のバーチャルパッドについて見てゆく。

今回は Swift ファイルの構成と振る舞いについて。

 

ファイルの構成

ファイル名 内容
ButtonOverlay.swift ControlOverlay.swift で使用するボタン用の ButtonOverlay クラス
PadOverlay.swift ControlOverlay.swift で使用するヴァーチャルパッド用の PadOverlay クラス
ControlOverlay.swift ボタンとヴァーチャルパッドで設定し Overlay.swift ボタンでこちらを設定する

   

ButtonOverlay.swift

ButtonOverlay クラスは SpriteKit の SKNode で設定され A、B ボタンで使い回せる様につくられており、
ButtonOverlay.swift は最終的に GameController で操作できる様にするデリゲート ButtonOverlayDelegate と ButtonOverlay クラスで構成されている。

 

ButtonOverlayDelegate

ボタンが押されたかをデリゲートで渡す関数設定されており、最終的に GameController クラスでこちらが設定されている。
ここでは ButtonOverlay クラスでタッチイベントからアクセスしている。

func willPress(_ button: ButtonOverlay)
func didPress(_ button: ButtonOverlay)

 

ButtonOverlay クラス

クラスの中身を見てゆく。

 

初期設定

  • ボタンのサイズを決める
  • デリゲートの設定
  • UITouch からタッチの取得
  • inner 変数で SKShapeNode を使いボタン自体を設定。タップ時の色変え用
  • background 変数で SKShapeNode を使いボタンの背景を設定
  • label 変数で SKShapeNode を使いボタン内のテキストを設定
var size = CGSize.zero {
    didSet {
        if size != oldValue {
            updateForSizeChange()
        }
    }
}

weak var delegate: ButtonOverlayDelegate?

private var trackingTouch: UITouch?
private var inner: SKShapeNode!
private var background: SKShapeNode!
private var label: SKLabelNode?

 

イニシャライズ

  • サイズを 40 で設定
  • 透明度を 0.7
  • ユーザー操作を許可
  • buildPad 関数でボタンを作成、引数の文字列を渡しボタンに文字を表示する
init(_ text: NSString) {
    super.init()
    
    size = CGSize(width: CGFloat(40), height: CGFloat(40))
    alpha = 0.7
    isUserInteractionEnabled = true
    buildPad(text)

}

 

ボタンの作成

背景の background ノード > タップ時に色を変える inner ノート > 文字を表示する label ノードの重なり順で設定されている。

background と inner は SKShapeNode に CGPath の円が渡され描画しており、 label SKShapeNode で文字を表示している。

func buildPad(_ text: NSString) {
    let backgroundRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width), height: CGFloat(size.height))
    background = SKShapeNode()
    background.path = CGPath( ellipseIn: backgroundRect, transform: nil)
    background.strokeColor = SKColor.black
    background.lineWidth = 3.0
    addChild(background)
    
    inner = SKShapeNode()
    inner.path = CGPath( ellipseIn: CGRect( x: 0, y: 0, width: innerSize.width, height: innerSize.height ), transform: nil)
    inner.lineWidth = 1.0
    inner.fillColor = SKColor.white
    inner.strokeColor = SKColor.gray
    addChild(inner)
    
    label = SKLabelNode()
    label!.fontName = UIFont.boldSystemFont(ofSize: 24).fontName
    label!.fontSize = 24
    label!.fontColor = SKColor.black
    label?.verticalAlignmentMode = .center
    label?.horizontalAlignmentMode = .center
    label?.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0 + 1.0)
    label?.text = text as String
    addChild(label!)
}

 

updateForSizeChange

ボタンの大きさがに変更がかかった場合こちらが呼ばれ backgroundRect、innerRect、label の大きさと配置が変更される。

処理の初めでこのボタンに背景の background ノードがなかった場合、この処理を抜ける様になっている。

func updateForSizeChange() {
    guard let background = background  else { return }

    let backgroundRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width), height: CGFloat(size.height))
    background.path = CGPath(ellipseIn: backgroundRect, transform: nil)
    let innerRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width / 3.0), height: CGFloat(size.height / 3.0))
    inner.path = CGPath(ellipseIn: innerRect, transform: nil)
    
    label!.position = CGPoint(x: size.width / 2.0 - label!.frame.size.width / 2.0, y: size.height / 2.0 - label!.frame.size.height / 2.0)
}

 

ボタンのサイズ取得

ボタンのサイズ取得様に設定したボタンの大きさを返す。

var innerSize: CGSize {
    return CGSize(width: CGFloat(size.width), height: CGFloat(size.height))
}

 

操作の初期化

最初にタッチされたイベントを保持する trackingTouch を空にして、inner の塗りつぶしを元に戻し、delegate の didPress に関数を渡す。

func resetInteraction() {
    trackingTouch = nil
    inner.fillColor = SKColor.white
    delegate!.didPress(self)
}

 

タッチイベント

touchesBegan、touchesEnded、touchesCancelled が設定されている。

touchesBegan では、自身を初めにタッチされたものの保持、inner の fillColor を暗くして押されたh状態にして、willPress 関数に自身を渡す

touchesEnded と touchesCancelled は処理は同じで、resetInteraction を呼び出している。

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    trackingTouch = touches.first
    inner.fillColor = SKColor.black
    delegate!.willPress(self)
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    if touches.contains(trackingTouch!) {
        resetInteraction()
    }
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    if touches.contains(trackingTouch!) {
        resetInteraction()
    }
}

 

 

PadOverlay.swift

PadOverlay クラスは SpriteKit の SKNode で設定され A、B ボタンで使い回せる様につくられており、
ButtonOverlay.swift は最終的に GameController で操作できる様にするデリゲート ButtonOverlayDelegate と ButtonOverlay クラスで構成されている。

 

PadOverlayDelegate

ButtonOverlay 同様にヴァーチャルパッドの操作をデリゲートで渡す関数設定されており、最終的に GameController クラスでこちらが設定されている。
ここでは PadOverlay クラスでタッチイベントからアクセスしている。

func padOverlayVirtualStickInteractionDidStart(_ padNode: PadOverlay)
func padOverlayVirtualStickInteractionDidChange(_ padNode: PadOverlay)
func padOverlayVirtualStickInteractionDidEnd(_ padNode: PadOverlay)

 

 

PadOverlay クラス

クラスの中身を見てゆく。

 

初期設定

  • 全体のサイズ
  • ヴァーチャルパッドのスティック部分の初期値。-1 〜 1 の間の値
  • デリゲートの設定
  • UITouch からタッチの取得
  • startLocation で初期位置
  • stick 変数で SKShapeNode を使いスティックを設定
  • background 変数で SKShapeNode を使いボタンの背景を設定
var size = CGSize.zero {
    didSet {
        if size != oldValue {
            updateForSizeChange()
        }
    }
}

var stickPosition = CGPoint.zero {
    didSet {
        if stickPosition != oldValue {
            updateStickPosition()
        }
    }
}

weak var delegate: PadOverlayDelegate?

private var trackingTouch: UITouch?
private var startLocation = CGPoint.zero
private var stick: SKShapeNode!
private var background: SKShapeNode!

 

イニシャライズ

  • サイズを 150 で設定
  • 透明度を 0.7
  • ユーザー操作を許可
  • buildPad 関数でパッドを作成し表示する
init(_ text: NSString) {
    super.init()
    size = CGSize(width: CGFloat(150), height: CGFloat(150))
    alpha = 0.7
    isUserInteractionEnabled = true
    buildPad()
}

 

buildPad

background で背景として CGPath で円をかいた SKShapeNode が設定されており、 stick にも CGPath で円をかいた SKShapeNode が設定されている。

コメントアウトで #if os( OSX ) が設定されているが macOS ではヴァーチャルパッドが使われることはない。

func buildPad() {
    let backgroundRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: 
    CGFloat(size.width), height: CGFloat(size.height))

    background = SKShapeNode()
    background.path = CGPath( ellipseIn: backgroundRect, transform: nil )
    background.strokeColor = SKColor.black
    background.lineWidth = 3.0
    addChild(background)

    var stickRect = CGRect.zero
    stickRect.size = stickSize
    stick = SKShapeNode()
    stick.path = CGPath( ellipseIn: stickRect, transform: nil)
    stick.lineWidth = 2.0

    //#if os( OSX )
    stick.fillColor = SKColor.white
    //#endif

    stick.strokeColor = SKColor.black

    addChild(stick)
    updateStickPosition()
}

 

stickSize

スティックのサイズを全体の 1/3 として返し設定させる。

var stickSize: CGSize {
    return CGSize( width: size.width / 3.0, height: size.height / 3.0)
}

 

update 関連

ボタンと同様にサイズが変更されると各パーツが変更される。
また updateStickPosition はスティックの移動と位置の変更値を stickPosition に渡す。

  • updateForSizeChange は全体と背景となる円。
  • updateStickPosition はスティックの SKNode の初期位置
  • updateStickPosition はスティック stickPosition 変数が変更される
func updateForSizeChange() {
    guard let background = background else { return }

    let backgroundRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width), height: CGFloat(size.height))
    background.path = CGPath( ellipseIn: backgroundRect, transform: nil)
    let stickRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width / 3.0), height: CGFloat(size.height / 3.0))
    stick.path = CGPath( ellipseIn: stickRect, transform: nil)
}

func updateStickPosition() {
    let stickSize: CGSize = self.stickSize
    let stickX = size.width / 2.0 - stickSize.width / 2.0 + size.width / 2.0 * stickPosition.x
    let stickY = size.height / 2.0 - stickSize.height / 2.0 + size.width / 2.0 * stickPosition.y
    stick.position = CGPoint(x: stickX, y: stickY)
}

func updateStickPosition(forTouchLocation location: CGPoint) {
    var l_vec = vector_float2( x: Float( location.x - startLocation.x ), y: Float( location.y - startLocation.y ) )
    l_vec.x = (l_vec.x / Float( size.width ) - 0.5) * 2.0
    l_vec.y = (l_vec.y / Float( size.height ) - 0.5) * 2.0
    if simd_length_squared(l_vec) > 1 {
        l_vec = simd_normalize(l_vec)
    }
    stickPosition = CGPoint( x: CGFloat( l_vec.x ), y: CGFloat( l_vec.y ) )
}

 

操作の初期化

スティックとスティックの初期位置

func resetInteraction() {
    stickPosition = CGPoint.zero
    trackingTouch = nil
    startLocation = CGPoint.zero
    delegate!.padOverlayVirtualStickInteractionDidEnd(self)
}

 

タッチイベント: タッチ

ヴァーチャルパッドの触った際の処理

  • trackingTouch に最初のタッチを保持する
  • trackingTouch からタッチされた位置を取り、startLocation に渡す
  • 中心が円の真ん中にあるため、タッチ位置を中心にせってするため width と height 割った数を startLocation の X、Y へ渡す。
  • updateStickPosition 関数に位置を渡し stickPosition を更新する
  • padOverlayVirtualStickInteractionDidStart に自身を設定する。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    trackingTouch = touches.first
    startLocation = trackingTouch!.location(in: self)

    startLocation.x -= size.width / 2.0
    startLocation.y -= size.height / 2.0

    updateStickPosition(forTouchLocation: trackingTouch!.location(in: self))

    delegate!.padOverlayVirtualStickInteractionDidStart(self)
}

 

タッチイベント: ムーブ

ヴァーチャルパッドのスティックを移動させた際の処理。

タッチイベントの touches に trackingTouch があれば、updateStickPosition を実行し trackingTouch の位置を渡している。
updateStickPosition で startLocation と比較し移動の値を渡すことになる。

padOverlayVirtualStickInteractionDidChange に自身を設定する。

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    if touches.contains(trackingTouch!) {
        updateStickPosition(forTouchLocation: trackingTouch!.location(in: self))
        delegate!.padOverlayVirtualStickInteractionDidChange(self)
    }
}

 

タッチイベント: エンド、キャンセル

エンド、キャンセル共に処理は同じ。

タッチイベントの touches に trackingTouch があれば、resetInteraction 関数を実行し、スティック等をリセットする。

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    if touches.contains(trackingTouch!) {
        resetInteraction()
    }
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    if touches.contains(trackingTouch!) {
        resetInteraction()
    }
}

 

ControlOverlay.swift

ControlOverlay クラスのみのファイル。

ボタンのマージンを 25 として、
キャラクターの移動を leftPad と カメラ視点変更を rightPad として PadOverlay クラスに設定。
ジャンプを buttonA、攻撃を buttonB として ButtonOverlay をボタンとして設定している。

イニシャライズは画面サイズを引数として取り、それを元に上記の leftPad、rightPad、buttonA、buttonB の位置を設定している。

class ControlOverlay: SKNode {
    
    let buttonMargin = CGFloat( 25 )
    
    var leftPad = PadOverlay()
    var rightPad = PadOverlay()
    var buttonA = ButtonOverlay("A")
    var buttonB = ButtonOverlay("B")

    init(frame: CGRect) {
        super.init()

        leftPad.position = CGPoint(x: CGFloat(20), y: CGFloat(40))
        addChild(leftPad)
        
        rightPad.position = CGPoint(x: CGFloat(frame.size.width - 20 - rightPad.size.width), y: CGFloat(40))
        addChild(rightPad)
        
        let buttonDistance = rightPad.size.height / CGFloat( 2 ) + buttonMargin + buttonA.size.height / CGFloat( 2 )
        let center = CGPoint( x: rightPad.position.x + rightPad.size.width / 2.0, y: rightPad.position.y + rightPad.size.height / 2.0 )
        
        let buttonAx = center.x - buttonDistance * CGFloat(cosf(Float.pi / 4.0)) - (buttonB.size.width / 2)
        let buttonAy = center.y + buttonDistance * CGFloat(sinf(Float.pi / 4.0)) - (buttonB.size.height / 2)
        buttonA.position = CGPoint(x: buttonAx, y: buttonAy)
        addChild(buttonA)
        
        let buttonBx = center.x - buttonDistance * CGFloat(cosf(Float.pi / 2.0)) - (buttonB.size.width / 2)
        let buttonBy = center.y + buttonDistance * CGFloat(sinf(Float.pi / 2.0)) - (buttonB.size.height / 2)
        buttonB.position = CGPoint(x: buttonBx, y: buttonBy)
        addChild(buttonB)
    }

    override init() {
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

これで一応、Fox2 のプロジェクトの設定はおしまい。

WWDC 2018 後、しばらくしてから、シーンファイルのノードに設定されている Shader Modifier について解説するかもしれない。

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

2D の UI である Overlay クラスを見てゆく。
SceneKit の overlay の機能を使い SpriteKit を SKScene を表示している。

 

Overlay クラスの中身

表示される要素

  • 右上のプレイヤーキャラクター Max のアイコン
  • 宝石や鍵のアイコン
  • クリア時の Congratulations の表示部分
  • iOS 用のヴァーチャルパッド
  • デモ用のメニュー

 

以下、コードを見てゆく。

 

import

SpriteKit が追加でインポートされている。

import Foundation
import SceneKit
import SpriteKit

 

クラスの宣言

class Overlay: SKScene {
    ...
}

以下、クラスの中身を見てゆく。

 

メンバー定数 / 変数

定数 / 変数 説明
overlayNode このシーンのルートになるノード
congratulationsGroupNode クリア時に表示されるものをひとまとめにしたノード
collectedKeySprite 取得した鍵のノード
collectedGemsSprites 取得した宝石のノード
demoMenu Menu表示用の関数
controlOverlay ヴァーチャルパッドを表示するためのノード
private var overlayNode: SKNode
private var congratulationsGroupNode: SKNode?
private var collectedKeySprite: SKSpriteNode!
private var collectedGemsSprites = [SKSpriteNode]()

private var demoMenu: Menu?
    
#if os( iOS )
    public var controlOverlay: ControlOverlay?
#endif

 

イニシャライズ

宣言

画面サイズと GameController 受け取って初期化される。

init(size: CGSize, controller: GameController) {
    ...
}

 

初期設定

  • 全体で使用する overlayNode に SKNode を設定
  • w、h に引数 CGSize から weight、height を渡す。
  • collectedGemsSprites に空の配列を設定
  • シーンを scaleMode を resizeFill を使用し画面にフィットさせる。
  • シーンに overlayNode を適応する
  • overlayNode は X 軸 0、Y 軸 画面の高さにして左下を支点にする
overlayNode = SKNode()
super.init(size: size)

let w: CGFloat = size.width
let h: CGFloat = size.height

collectedGemsSprites = []

// Setup the game overlays using SpriteKit.
scaleMode = .resizeFill

addChild(overlayNode)
overlayNode.position = CGPoint(x: 0.0, y: h)

 

右上のプレイヤーキャラクター Max のアイコン

MaxIcon.png を SKSpriteNode の characterNode に入れ、別で設定されている Button クラスの引数を menuButton に入れ、overlayNode に配置している。

setClickedTarget で toggleMenu 関数を呼びデモ用のメニューを開く

let characterNode = SKSpriteNode(imageNamed: "MaxIcon.png")
let menuButton = Button(skNode: characterNode)
menuButton.position = CGPoint(x: 50, y: -50)
menuButton.xScale = 0.5
menuButton.yScale = 0.5
overlayNode.addChild(menuButton)
menuButton.setClickedTarget(self, action: #selector(self.toggleMenu))

 

宝石

collectableBIG_empty.png を SKSpriteNode の gemNode に入れ、overlayNode に配置している。

collectedGemsSprites の配列に gemNode に入れている。 for 分を使っているが今回使用しているのは1つしかなく1回しかループがまわらない。

for i in 0..<1 {
    let gemNode = SKSpriteNode(imageNamed: "collectableBIG_empty.png")
    gemNode.position = CGPoint(x: 125 + i * 80, y: -50)
    gemNode.xScale = 0.25
    gemNode.yScale = 0.25
    overlayNode.addChild(gemNode)
    collectedGemsSprites.append(gemNode)
}

 

key_empty.png を SKSpriteNode の collectedKeySprite に入れ、overlayNode に配置している。

collectedKeySprite = SKSpriteNode(imageNamed: "key_empty.png")
collectedKeySprite.position = CGPoint(x: CGFloat(195), y: CGFloat(-50))
collectedKeySprite.xScale = 0.4
collectedKeySprite.yScale = 0.4
overlayNode.addChild(collectedKeySprite)

 

iOS の場合の処理

controlOverlay からヴァーチャルパッド ControlOverlay クラスを設定しデリゲートを 引数の controller を渡して、シーンに追加している。

#if os( iOS )
    controlOverlay = ControlOverlay(frame: CGRect(x: CGFloat(0), y: CGFloat(0), width: w, height: h))
    controlOverlay!.leftPad.delegate = controller
    controlOverlay!.rightPad.delegate = controller
    controlOverlay!.buttonA.delegate = controller
    controlOverlay!.buttonB.delegate = controller
    addChild(controlOverlay!)
#endif

 

デモ用の UI

デモ用の UI を設定している。 isHidden 指定しているため表示されない。

右上の Max アイコンをタップすると toggleMenu 関数が呼ばれて、このデモ用の UI が表示されるのだが、この UI は画面外に位置しているため表示する際は Menu クラスの設定変更する必要がある。

demoMenu = Menu(size: size)
demoMenu!.delegate = controller
demoMenu!.isHidden = true
overlayNode.addChild(demoMenu!)

 

デモ用の UI

SceneKit 上で操作するのでインタラクションを切る。

isUserInteractionEnabled = false

 

layout2DOverlay 関数

クリア時の Congratulations 表示設定を congratulationsGroupNode で設定する。

func layout2DOverlay() {
    overlayNode.position = CGPoint(x: 0.0, y: size.height)

    guard let congratulationsGroupNode = self.congratulationsGroupNode else { return }
    
    congratulationsGroupNode.position = CGPoint(x: CGFloat(size.width * 0.5), y: CGFloat(size.height * 0.5))
    congratulationsGroupNode.xScale = 1.0
    congratulationsGroupNode.yScale = 1.0
    let currentBbox: CGRect = congratulationsGroupNode.calculateAccumulatedFrame()
    
    let margin: CGFloat = 25.0
    let bounds = CGRect(x: 0, y: 0, width: size.width, height: size.height)
    let maximumAllowedBbox: CGRect = bounds.insetBy(dx: margin, dy: margin)
    
    let top: CGFloat = currentBbox.maxY - congratulationsGroupNode.position.y
    let bottom: CGFloat = congratulationsGroupNode.position.y - currentBbox.minY
    let maxTopAllowed: CGFloat = maximumAllowedBbox.maxY - congratulationsGroupNode.position.y
    let maxBottomAllowed: CGFloat = congratulationsGroupNode.position.y - maximumAllowedBbox.minY
    
    let `left`: CGFloat = congratulationsGroupNode.position.x - currentBbox.minX
    let `right`: CGFloat = currentBbox.maxX - congratulationsGroupNode.position.x
    let maxLeftAllowed: CGFloat = congratulationsGroupNode.position.x - maximumAllowedBbox.minX
    let maxRightAllowed: CGFloat = maximumAllowedBbox.maxX - congratulationsGroupNode.position.x
    
    let topScale: CGFloat = top > maxTopAllowed ? maxTopAllowed / top: 1
    let bottomScale: CGFloat = bottom > maxBottomAllowed ? maxBottomAllowed / bottom: 1
    let leftScale: CGFloat = `left` > maxLeftAllowed ? maxLeftAllowed / `left`: 1
    let rightScale: CGFloat = `right` > maxRightAllowed ? maxRightAllowed / `right`: 1
    
    let scale: CGFloat = min(topScale, min(bottomScale, min(leftScale, rightScale)))
    
    congratulationsGroupNode.xScale = scale
    congratulationsGroupNode.yScale = scale
}

 

宝石と鍵の取得処理

宝石の collectedGemsCount は Int の値を入れ、鍵は didCollectKey 関数を実行すると、各取得時の画像に変更され、アニメーションが行われる。

var collectedGemsCount: Int = 0 {
    didSet {
        collectedGemsSprites[collectedGemsCount - 1].texture = SKTexture(imageNamed:"collectableBIG_full.png")
        
        collectedGemsSprites[collectedGemsCount - 1].run(SKAction.sequence([
            SKAction.wait(forDuration: 0.5),
            SKAction.scale(by: 1.5, duration: 0.2),
            SKAction.scale(by: 1 / 1.5, duration: 0.2)
            ]))
    }
}

func didCollectKey() {
    collectedKeySprite.texture = SKTexture(imageNamed:"key_full.png")
    collectedKeySprite.run(SKAction.sequence([
        SKAction.wait(forDuration: 0.5),
        SKAction.scale(by: 1.5, duration: 0.2),
        SKAction.scale(by: 1 / 1.5, duration: 0.2)
        ]))
}

 

iOS での分岐

ヴァーチャルパッドの表示 / 非表示を行う。
主に GameController クラスで呼び出される。

#if os( iOS )
func showVirtualPad() {
    controlOverlay!.isHidden = false
}

func hideVirtualPad() {
    controlOverlay!.isHidden = true
}
#endif

 

クリア時の表示

congratulationsNode を設定しつつ、アニメーション設定を行い再生する。

func showEndScreen() {
    let congratulationsNode = SKSpriteNode(imageNamed: "congratulations.png")
    
    let characterNode = SKSpriteNode(imageNamed: "congratulations_pandaMax.png")
    characterNode.position = CGPoint(x: CGFloat(0.0), y: CGFloat(-220.0))
    characterNode.anchorPoint = CGPoint(x: CGFloat(0.5), y: CGFloat(0.0))
    
    congratulationsGroupNode = SKNode()
    congratulationsGroupNode!.addChild(characterNode)
    congratulationsGroupNode!.addChild(congratulationsNode)
    addChild(congratulationsGroupNode!)
    
    layout2DOverlay()
    
    congratulationsNode.alpha = 0.0
    congratulationsNode.xScale = 0.0
    congratulationsNode.yScale = 0.0
    congratulationsNode.run( SKAction.group([SKAction.fadeIn(withDuration: 0.25),
                                SKAction.sequence([SKAction.scale(to: 1.22, duration: 0.25),
                            SKAction.scale(to: 1.0, duration: 0.1)])]))
    
    characterNode.alpha = 0.0
    characterNode.xScale = 0.0
    characterNode.yScale = 0.0
    characterNode.run(SKAction.sequence([SKAction.wait(forDuration: 0.5),
                        SKAction.group([SKAction.fadeIn(withDuration: 0.5),
                        SKAction.sequence([SKAction.scale(to: 1.22, duration: 0.25),
                    SKAction.scale(to: 1.0, duration: 0.1)])])]))
}

 

その他

トグルで toggleMenu 関数から demoMenu の isHidden 呼び非表示にする。

@objc
func toggleMenu(_ sender: Button) {
    demoMenu!.isHidden = !demoMenu!.isHidden
}

 

次回は 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 を見てゆく。

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

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

今回はクリア時の演出、レンダリングのクオリティ、デバッグの関数について。

 

クリア時の演出

クリア時に Music_victory.mp3 の BGM を 鳴らし、 2D UI の overlay の showEndScreen() 呼び、ロゴと Congratulation の画像を出す。

func showEndScreen() {
    guard let victoryMusic = SCNAudioSource(named: "audio/Music_victory.mp3") else { return }
    victoryMusic.volume = 0.5

    self.scene?.rootNode.addAudioPlayer(SCNAudioPlayer(source: victoryMusic))

    self.overlay?.showEndScreen()
}

 

レンダリングクオリティ設定

いくつかの関数を設定後、イニシャライズ時に呼ぶ configureRenderingQuality の関数でそれらを呼ぶ。
EXR などの tvOS で使用できなものや負荷が多いのを切っている。

以下、設定する関数。

 

SCNMaterialProperty で EXR の拡張子を png にする。

func turnOffEXRForMAterialProperty(property: SCNMaterialProperty) {
    if var propertyPath = property.contents as? NSString {
        if propertyPath.pathExtension == "exr" {
            propertyPath = ((propertyPath.deletingPathExtension as NSString).appendingPathExtension("png")! as NSString)
            property.contents = propertyPath
        }
    }
}

 

バックグラウンドのテクスチャで turnOffEXRForMAterialProperty を使い EXR を止め、全てのマテリアルで自己発光を止める。

func turnOffEXR() {
    self.turnOffEXRForMAterialProperty(property: scene!.background)
    self.turnOffEXRForMAterialProperty(property: scene!.lightingEnvironment)

    scene?.rootNode.enumerateChildNodes { (child: SCNNode, _: UnsafeMutablePointer<ObjCBool>) in
        if let materials = child.geometry?.materials {
            for material in materials {
                self.turnOffEXRForMAterialProperty(property: material.selfIllumination)
            }
        }
    }
}

 

全てのノーマルマップを止める。

func turnOffNormalMaps() {
    scene?.rootNode.enumerateChildNodes({ (child: SCNNode, _: UnsafeMutablePointer<ObjCBool>) in
        if let materials = child.geometry?.materials {
            for material in materials {
                material.normal.contents = SKColor.black
            }
        }
    })
}

 

全ての HDR の処理を止める。

func turnOffHDR() {
    scene?.rootNode.enumerateChildNodes({ (child: SCNNode, _: UnsafeMutablePointer<ObjCBool>) in
        child.camera?.wantsHDR = false
    })
}

 

カメラの被写界深度の効果を止める。

func turnOffDepthOfField() {
    scene?.rootNode.enumerateChildNodes({ (child: SCNNode, _: UnsafeMutablePointer<ObjCBool>) in
        child.camera?.wantsDepthOfField = false
    })
}

 

全てのライトの Shadow Sample を 上限 1 にする。

func turnOffSoftShadows() {
    scene?.rootNode.enumerateChildNodes({ (child: SCNNode, _: UnsafeMutablePointer<ObjCBool>) in
        if let lightSampleCount = child.light?.shadowSampleCount {
            child.light?.shadowSampleCount = min(lightSampleCount, 1)
        }
    })
}

 

全てのポストプロセスを止める。
シャドウキャスケード(カメラ位置によるリアルタイムでのシャドウマップのクオリティの変更)、シャドウマップのサイズを 1024x1024 にする。

func turnOffPostProcess() {
    scene?.rootNode.enumerateChildNodes({ (child: SCNNode, _: UnsafeMutablePointer<ObjCBool>) in
        if let light = child.light {
            light.shadowCascadeCount = 0
            light.shadowMapSize = CGSize(width: 1024, height: 1024)
        }
    })
}

 

画面前面の 2D UI 表示を消す。

func turnOffOverlay() {
    sceneRenderer?.overlaySKScene = nil
}

 

全ての Shader Modifiers を取得し、Geomety のエントリーポイントのシェーダー(Vertex Shader の処理)を消す。

func turnOffVertexShaderModifiers() {
    scene?.rootNode.enumerateChildNodes({ (child: SCNNode, _: UnsafeMutablePointer<ObjCBool>) in
        if var shaderModifiers = child.geometry?.shaderModifiers {
            shaderModifiers[SCNShaderModifierEntryPoint.geometry] = nil
            child.geometry?.shaderModifiers = shaderModifiers
        }

        if let materials = child.geometry?.materials {
            for material in materials where material.shaderModifiers != nil {
                var shaderModifiers = material.shaderModifiers!
                shaderModifiers[SCNShaderModifierEntryPoint.geometry] = nil
                material.shaderModifiers = shaderModifiers
            }
        }
    })
}

 

植物の表示を止める。

func turnOffVegetation() {
    scene?.rootNode.enumerateChildNodes({ (child: SCNNode, _: UnsafeMutablePointer<ObjCBool>) in
        guard let materialName = child.geometry?.firstMaterial?.name as NSString? else { return }
        if materialName.hasPrefix("plante") {
            child.isHidden = true
        }
    })
}

 

tvOS 用に表示クオリティを変えるため、これまで作成した関数を実行する。
SCNView を渡しているが、内部で使用していない。

func configureRenderingQuality(_ view: SCNView) {
        
#if os( tvOS )
    self.turnOffEXR()
    self.turnOffNormalMaps()
    self.turnOffHDR()
    self.turnOffDepthOfField()
    self.turnOffSoftShadows()
    self.turnOffPostProcess()
    self.turnOffOverlay()
    self.turnOffVertexShaderModifiers()
    self.turnOffVegetation()
#endif
}

 

デバッグ処理。
WWDC 2017 のデモ用でそのままでは使用できない。 カメラの F 値、焦点距離、ボタンを押すとカメラの表示位置が変わる。

func fStopChanged(_ value: CGFloat) {
    sceneRenderer!.pointOfView!.camera!.fStop = value
}

func focusDistanceChanged(_ value: CGFloat) {
    sceneRenderer!.pointOfView!.camera!.focusDistance = value
}

func debugMenuSelectCameraAtIndex(_ index: Int) {
    if index == 0 {
        let key = self.scene?.rootNode .childNode(withName: "key", recursively: true)
        key?.opacity = 1.0
    }
    self.setActiveCamera("CameraDof\(index)")
}

 

次回は GameController の最終部分、ゲームコントローラーやゲーム操作部分の関数について見てゆく。

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

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

今回はフレーム毎に更新される関数について。

 

Fox2 で使用しているフレーム毎に更新される関数

GameController のプロトコルで設定している様に SCNSceneRenderer と SCNPhysicsContact の2つのデリゲートが行なっている。

 

SCNSceneRendererDelegate

フレーム毎の描画更新で呼ばれる関数。
以下、行われる内容。

 

  • deltaTime で現在の経過時間を保持し、lastUpdateTime で最終に取得した経過時間を保持する
  • friendsAreFree が true なら updateFriends 呼び、仲間が解放され歩行するアニメーションを開始する。
  • playingCinematic が true ならこれ以降の処理を飛ばす
  • character クラスの update を呼び状態を更新する
  • 全ての GKEntity の update 関数を更新する

 

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    if lastUpdateTime == 0 {
        lastUpdateTime = time
    }
    let deltaTime: TimeInterval = time - lastUpdateTime
    lastUpdateTime = time

    if friendsAreFree {
        updateFriends(deltaTime: deltaTime)
    }

    if playingCinematic == true {
        return
    }

    character!.update(atTime: time, with: renderer)

    for entity: GKEntity in gkScene!.entities {
        entity.update(deltaTime: deltaTime)
    }
}

 

SCNPhysicsContactDelegate

フレーム毎の物理判定で呼ばれる関数
以下、行われる内容。

 

  • nodeA, nodeB の categoryBitMask が Bitmask.trigger であった場合、trigger 関数に該 当するノードを渡し関数を呼び出す。
  • nodeA, nodeB の categoryBitMask が Bitmask.collectable であった場合、collect 関数に該当するノードを渡し関数を呼び出す。

 

func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
    if contact.nodeA.physicsBody!.categoryBitMask == Bitmask.trigger.rawValue {
        trigger(contact.nodeA)
    }
    if contact.nodeB.physicsBody!.categoryBitMask == Bitmask.trigger.rawValue {
        trigger(contact.nodeB)
    }

    if contact.nodeA.physicsBody!.categoryBitMask == Bitmask.collectable.rawValue {
        collect(contact.nodeA)
    }
    if contact.nodeB.physicsBody!.categoryBitMask == Bitmask.collectable.rawValue {
        collect(contact.nodeB)
    }
}

 

次回はクリア時の表示、レンダリングのクオリティ設定の関数について見てゆく。

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

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

今回はプレイヤーキャラクター Max のコントロールについて。

 

コントロールについて

キャラクターのコントロールはバーチャルパッド、ゲームコントローラー、キーボードからの入力から以下の関数が呼ばれる。

 

ジャンプ

バーチャルパッドの A ボタンが押された時の処理で Max がジャンプする。
Bool 値が設定されているのは、ジャンプ中にもう一度ジャンプしないため。

func controllerJump(_ controllerJump: Bool) {
    character!.isJump = controllerJump
}

 

攻撃

バーチャルパッドの B ボタンが押された時の処理で Max が攻撃する。
攻撃状態を調べ、アニメーションが終わるまで次の攻撃ができない。

func controllerAttack() {
    if !self.character!.isAttacking {
        self.character!.attack()
    }
}

 

移動

バーチャルパッドやカーソルで移動を行う。
X、Y 軸の移動をを与える。

get で Max の現在の方向を取得可能。

var characterDirection: vector_float2 {
    get {
        return character!.direction
    }
    set {
        var direction = newValue
        let l = simd_length(direction)
        if l > 1.0 {
            direction *= 1 / l
        }
        character!.direction = direction
    }
}

 

カメラ視点変更

バーチャルパッドやキーで視点変更を行う。 操作は 移動と同様。

ここでは設定されていないが特定のカメラでは視点変更ができない。

var cameraDirection = vector_float2.zero {
    didSet {
        let l = simd_length(cameraDirection)
        if l > 1.0 {
            cameraDirection *= 1 / l
        }
        cameraDirection.y = 0
    }
}

 

次回はフレーム毎に更新される関数について見てゆく。

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

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

今回は鍵を開けるなどゲームでのアクションについて。

 

鍵を開ける

unlockDoor 関数も長いので中身を別で見てゆく。

func unlockDoor() {
    ...
}

 

扉が開いているか調べる

friendsAreFree が true の場合はこの関数を実行しない。 開いても プレイヤーキャラクター Max が少し操作でき、この関数を呼ぶトリガーを弾いてしまうため。

if friendsAreFree {
    return
}

 

シーンを止める

宝石のアイテム取得時の演出と同様に startCinematic() を使ってシーンを止める

startCinematic()

 

鍵を開ける音を鳴らす

playSound(AudioSourceKind.unlockDoor)

 

扉が開く演出

SCNTransaction に対して completionBlock が2つあるため、3段階でアニメーションされる。
以下、アニメーション内容。

 

1段階目

扉が開くトリガーが引かれると即時で setActiveCamera でカメラ移動のアニメーションが行われる

 

2段階目
  • ドアのノード取得
  • ドアのパーティクル配置用のノードを探しパーティクルを置く
  • 扉が開いた際の音を鳴らす
  • ドアを半透明にする

 

3段階目
  • animateFriends() 呼び仲間を初期状態アニメーションさせる
  • friendsAreFree を ture にして SceneRenderer のデリゲートで設定している歩行アニメーションの実行を許可にする
  • DispatchQueue から showEndScreen() を呼び、クリア時の画面を表示する。

 

SCNTransaction.begin()
SCNTransaction.animationDuration = 0.0
SCNTransaction.completionBlock = {() -> Void in
    let door: SCNNode? = self.scene!.rootNode.childNode(withName: "door", recursively: true)
    let particle_door: SCNNode? = self.scene!.rootNode.childNode(withName: "particles_door", recursively: true)
    self.addParticles(with: .unlockDoor, withTransform: particle_door!.worldTransform)

    self.playSound(.collectBig)

    //add friends
    SCNTransaction.begin()
    SCNTransaction.animationDuration = 0.0
    self.addFriends(GameController.NumberOfFiends)
    SCNTransaction.commit()

    //open the door
    SCNTransaction.begin()
    SCNTransaction.animationDuration = 1.0
    SCNTransaction.completionBlock = {() -> Void in
        self.animateFriends()

        self.friendsAreFree = true

        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() +
            Double(Int64(1.0 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: {() -> Void in
            self.showEndScreen()
        })
    }
    door!.opacity = 0.0
    SCNTransaction.commit()
}

setActiveCamera("CameraCinematic02", animationDuration: 1.0)
SCNTransaction.commit()

 

鍵のアイテムを表示する

鍵のアイテムを表示する処理をする関数。

  • 鍵表示用のフラグを true にする
  • 鍵のノードを探し、key に渡す
  • 鍵を表示した時の音を鳴らす
  • 鍵を表示する前のパーティクルを設定し再生
  • 鍵のノードの透明度を 1 にして出現させる
  • completionBlock から DispatchQueue で keyDidAppear() 呼び処理を終える。
func showKey() {
    keyIsVisible = true

    let key: SCNNode? = scene!.rootNode.childNode(withName: "key", recursively: true)

    playSound(AudioSourceKind.collectBig)

    addParticles(with: .keyApparition, withTransform: key!.worldTransform)

    SCNTransaction.begin()
    SCNTransaction.animationDuration = 1.0
    SCNTransaction.completionBlock = {() -> Void in
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() +
            Double(Int64(2.5 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: {() -> Void in
            self.keyDidAppear()
        })
    }

    key!.opacity = 1.0
    SCNTransaction.commit()
}

 

鍵のアイテムを表示後の処理

鍵のアイテムを表示後、execTrigger でカメラを元の位置に戻し、 stopCinematic() を一時停止しているシーンを再生する。

func keyDidAppear() {
    execTrigger(lastTrigger!, animationDuration: 0.75)
    stopCinematic()
}

 

鍵のアイテムが出現する事前処理

先ほどの showKey 関数が呼ばれる前に keyShouldAppear が呼ばれる。

startCinematic() を呼び、画面を一時停止。
SCNTransaction のアニメーションから setActiveCamera を呼び CameraCinematic01 のカメラに移動。 completionBlock で showKey 関数が呼ぶ。

func keyShouldAppear() {
    startCinematic()

    SCNTransaction.begin()
    SCNTransaction.animationDuration = 0.0
    SCNTransaction.completionBlock = {() -> Void in
        self.showKey()
    }
    setActiveCamera("CameraCinematic01", animationDuration: 3.0)
    SCNTransaction.commit()
}

 

アイテムを取る処理

physicsWorld のデリゲートで Max がアイテムに接触した際呼ばれる関数。

このノードの physicsBody がある場合処理開始。
引数で渡されるノード collectable の名前が "key" なら鍵、"CollectableBig" なら宝石。

 

鍵の取得

  • keyIsVisible が true でなく表示されていない場合は処理を抜ける。(透明度 0 にしているだけなので物理判定は通るため)
  • アイテム取得のサウンドを再生
  • UI である overlay の didCollectKey 関数を呼び画面左上に鍵を表示する。
  • collectedKeys を1増やす

 

宝石の取得

  • collectedGems を1増やす
  • アイテム取得のサウンドを再生
  • UI である overlay の collectedGemsCount 関数を呼び画面左上に宝石を表示する。
  • collectedGems が 1 の場合 DispatchQueue から keyShouldAppear() を呼ぶ。

 

その後の処理

collectable の physicsBody を nil にして物理判定を削除し、keyApparition のパーディクルを表示して、collectable ノードをシーンから消す。

 

func collect(_ collectable: SCNNode) {
    if collectable.physicsBody != nil {

        if collectable.name == "key" {
            if !self.keyIsVisible {
                return
            }

            // play sound
            playSound(AudioSourceKind.collect)
            self.overlay?.didCollectKey()

            self.collectedKeys += 1
        }

        //the gems
        else if collectable.name == "CollectableBig" {
            self.collectedGems += 1

            // play sound
            playSound(AudioSourceKind.collect)

            // update the overlay
            self.overlay?.collectedGemsCount = self.collectedGems

            if self.collectedGems == 1 {
                //we collect a gem, show the key after 1 second
                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() +
                    Double(Int64(0.5 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: {() -> Void in
                    self.keyShouldAppear()
                })
            }
        }

        collectable.physicsBody = nil //not collectable anymore

        // particles
        addParticles(with: .keyApparition, withTransform: collectable.worldTransform)

        collectable.removeFromParentNode()
    }
}

 

次回はプレイヤーキャラクターのコントロールをする関数を見てゆく。